Skip to content

Instantly share code, notes, and snippets.

@sr105
Last active September 28, 2018 17:33
Show Gist options
  • Save sr105/17910113360bdfd877f9c5d6bbb7e915 to your computer and use it in GitHub Desktop.
Save sr105/17910113360bdfd877f9c5d6bbb7e915 to your computer and use it in GitHub Desktop.
TOTP Authenticator Python Implementation
#!/usr/bin/env python3
import time
import urllib.parse
import base64
import hmac
import types
# https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
# https://git.coolaj86.com/coolaj86/botp.js/src/branch/master/index.js
# https://authenticator.ppl.family/
# https://git.coolaj86.com/coolaj86/browser-authenticator.js
# https://www.ietf.org/rfc/rfc6238.txt
def hotp_gen(options, counter):
"""Generate HOTP code for key and counter."""
# key is stored Base32 encoded
# counter is converted to a 64-bit unsigned integer (MSB)
digest = hmac.new(key=base64.b32decode(options.secret),
msg=counter.to_bytes(8, 'big'),
digestmod=options.algorithm) \
.digest()
# Take the last 4 bits of the digest and use it as a byte offset.
offset = digest[-1] & 0xf
# Make an unsigned 32-bit integer (MSB) from the 4 bytes at offset,
# masking the top bit.
v = int.from_bytes(digest[offset:offset + 4], 'big') & 0x7fffffff
# Return the last N base-10 digits
return str(v)[-options.digits:]
def hotp_verify(token, options, counter):
"""Returns time period offset if valid, else None"""
# 0, -1, 1, -2, 2, ...
for i in sum(([-i, i] for i in range(1, options.window + 1)), [0]):
if hotp_gen(options, counter + i) == token:
return i
return None
def totp_counter(options):
"""Return current number of time periods since the epoch."""
return int(time.time()) // options.period
def totp_gen(options):
"""Returns HOTP code for key and current time period count."""
return hotp_gen(options, totp_counter(options))
def totp_verify(token, options, window=6):
"""Returns time period offset if valid, else None"""
return hotp_verify(token, options, totp_counter(options))
def decode_otpauth_url(url):
"""Decodes OTP Auth URL into parameters."""
pr = urllib.parse.urlparse(url)
options = dict(urllib.parse.parse_qsl(pr.query))
options.setdefault('period', 30)
options.setdefault('digits', 6)
options.setdefault('window', 6)
options.setdefault('algorithm', 'sha1')
# options.setdefault('algorithm', 'sha256')
# options.setdefault('algorithm', 'sha512')
options['period'] = int(options['period'])
options['digits'] = int(options['digits'])
return types.SimpleNamespace(**options)
url = 'otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30'
options = decode_otpauth_url(url)
z = totp_gen(options)
# time.sleep(45)
j = totp_verify(z, options)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment