Last active
February 2, 2019 20:31
-
-
Save mizhi/dcae5471530932686be76d2e1f54095c to your computer and use it in GitHub Desktop.
An implementation of OATH-HOTP and OATH-TOTP as a learning exercise
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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