Skip to content

Instantly share code, notes, and snippets.

@mizhi
Last active February 2, 2019 20:31
Show Gist options
  • Select an option

  • Save mizhi/dcae5471530932686be76d2e1f54095c to your computer and use it in GitHub Desktop.

Select an option

Save mizhi/dcae5471530932686be76d2e1f54095c to your computer and use it in GitHub Desktop.
An implementation of OATH-HOTP and OATH-TOTP as a learning exercise
import base64
from hashlib import sha1
import hmac
import time
class OTP:
"""Implemets the HOTP and TOTP algorithms.
https://tools.ietf.org/html/rfc4226 (HOTP)
and
https://tools.ietf.org/html/rfc6238 (TOTP)
These algorithms are the basis for most 2FA applications
(e.g., Google authenticator).
"""
def __init__(self, K, algorithm = sha1, code_size = 6):
"""Initialize the OTP algorithm.
The algorithm is implemented without assuming anything about the
encoding of the key (K). Some implementations expect certain
encodings, for example Google authenticator implements HOTP using
base32 encoded keys. Thus, to mimic google authenticator, the key
would need to be first decoded before initialization.
K -- [bytes] is a byte string representing the secret seed.
algorithm -- [function] One of the hashlib one-way hashing
algorithms. SHA1 is default.
code_size -- [Integer] how big the code should be, in digits.
Default is 6.
"""
self._K = K
self._algorithm = algorithm
self._code_size = code_size
def hotp(self, C):
"""Compute a HOTP code.
The HOTP (HMAC-Based One-Time Password) algorithm works by
computing the HMAC of a key and an integer.
See: https://tools.ietf.org/html/rfc4226 (HOTP)
https://tools.ietf.org/html/rfc2104 (HMAC)
The HOTP algorithm requires both the server and client to remember
the counter (C) for validating codes. As such, the algorithm is
used a basis for TOTP, which tends to be easier to synchronize.
C -- [Integer] the counter to pass into the hmac algorithm.
returns [String] The code generated by the HOTP algorithm.
"""
hm = hmac.new(self._K, self._int_to_bytes(C), self._algorithm)
digest_bytes = bytearray(hm.digest())
truncated_bits = self._truncate(digest_bytes)
return str(truncated_bits % 10**self._code_size).rjust(self._code_size, "0")
def totp(self, time_start = 0, time_unit = 30):
"""Compute a TOTP code.
The TOTP (Time-Based One-Time Password) algorithm is a variant on
the HOTP algorithm that uses the number of time_unit segments
since time_start.
In almost all applications, time_start is 0 (usually the Unix
Epoch (00:00:00 UTC on 1 January 1970). time_unit is usually 30,
meaning that we are dividing the number of seconds since the Unix
Epoch into 30 second chunks. The resulting number is floored.
time_start -- [Integer] The time from which to begin the time
segment count. Default is 0.
time_unit -- [Integer] The size of the time segment to divide
the duration between now and the start of time.
returns [String] The code generated by the TOTP algorithm.
"""
time_step = int(time.time() - time_start) // time_unit
return self.hotp(time_step)
def _truncate(self, digest_bytes):
"""Computes a truncated version of the digest.
This is obtained by examining the 4 LSBs (Least Significant
Bits) in the digest.
This produces an integer that represents the offset (in bytes)
from the start of the digest bytes. From this offset, 31 bits (4
bytes) are extracted from the digest.
digest_bytes [bytes] A digest obtained from hmac
returns 31 bit integer that is the truncated version of the digest.
(Note: In most architectures, this will be a 32-bit memory space.
However, to avoid issues with signed integers, the
specification stipulates 31-bits.)
"""
# offset (in bytes) is obtained from the 4 LSBs of the digest.
# We get this by masking with 0xF.
byte_offset = digest_bytes[-1] & 0xf
# next we offset into the byte array, mask and shift
return ((digest_bytes[byte_offset] & 0x7f) << 24) | \
((digest_bytes[byte_offset + 1] & 0xff) << 16) | \
((digest_bytes[byte_offset + 2] & 0xff) << 8) | \
(digest_bytes[byte_offset + 3] & 0xff)
def _int_to_bytes(self, c):
"""Convert an integer to a bytes representation.
This function converts an integer into a bytes representation
that can be used in the HMAC function (which expects an iterable
of bytes).
c -- [Integer] the integer to convert
returns [bytes] An 8 byte array of the integer bits.
"""
try:
# The int.to_bytes(...) function was introduced in Python
# 3.2+, so we use it if available.
return c.to_bytes(8, byteorder='big')
except AttributeError:
# If that method is not available, we build a bytearray by
# repeatedly extracting the last 8 bits of the integer,
# shifting the integer bits to the right, and so on until
# the integer becomes 0. We then reverse this list, convert
# it to a byte array, right pad it until it is 8 bytes long,
# and then convert it to a bytes representation.
bs = bytearray()
while c > 0:
bs.append(c & 0xFF)
c >>= 8
return bytes(bytearray(reversed(bs)).rjust(8, b"\0"))
def padded_base32decode(s):
"""This can be used to turn a Base-32 string (the kind that Google
authenticator uses) into a suitable bytes for the key.
This function ignores case (Base32 is technically only uppercase). In the
case where it is not an even multiple of 8, which is required by Base-32,
it is padded out. There are certain cases where it can fail, specifically
when the padding is equal to 2, 5, or 7.
See: https://tools.ietf.org/html/rfc4648 and
https://github.com/python/cpython/blob/master/Lib/base64.py#L234
s -- [String] The base32 encoded string to convert.
returns the Base-32 decoded bytes.
"""
s += '=' * (-len(s) % 8)
return base64.b32decode(s, casefold=True)
# google compatible secret. Google authenticator uses a base32
# encoded byte string for the secret.
SECRET = padded_base32decode("base32secret3232")
# This implies no special encoding and matches reference codes in
# https://tools.ietf.org/html/rfc4226#appendix-D
# SECRET = "12345678901234567890".encode()
gen = OTP(SECRET)
for i in range(0, 10):
print(i, gen.hotp(i))
print(gen.totp())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment