Implementation of RFC 6238 - TOPT Algorithm

Time based One Time Password

...in python

Two factor authentication is a technique where a user needs to prove his identity with more than just a username and a psasword to login to an online service. This was found necessary for various reasons such as people using the same password for multiple sites and one of those sites gets hacked, their password database compromised. Even if the passwords are stored as hashes and not plain texts, an attacker could run the hashes against a database of pre-computed hashes to guess the password.

With two factor authentication, a one time password (OTP) is generated each time the user logs in. This OTP is useless after one use. The HOTP may be used to generate OTPs. However, this has a serious drawback. The HOTP (See HOTP Jupyter Notebook)[1-3] requries the value of the counters of the user and the service to remain in sync or very clase to one another. The server takes care of desynced counters by verifying the user supplied code against a sequence of codes within a window of counter values. If the user's HOTP generator is desynced beyond the counter window, the server will not recognize the OTPs provided by the user. This is a serious problem.

The HOTP problem is solved by Time Based One Time Passwords (TOPTs). The TOTP uses the UNIX time epoch (number of seconds since 1st January 1970) instead of the counter. Since this value would be the same across all computers, the OTPs generated by the server and the user would not be out of sync. The TOTP is defined as -

$$TOTP(K, T) = HOTP(K, T)$$

$K$ is the shared secret between the user and the server. $T$ is the Unix epoch time in seconds.

The TOTP algorithm is described in RFC-6238[1].

This notebook demonstrates the working of the RFC-6283 with intermediate steps output to the console.

In [1]:
import hashlib
import hmac
import base64
import time
In [2]:
# Control excessive output to console
debug = True
def dbg(data):
    if (debug):
        print(data)
In [3]:
# Prepare time - convert integer to byte
def i2b_time(int_time):
    return int_time.to_bytes(8, byteorder='big')
In [4]:
### Define SharedSecret, Block size, hashing algorithm, TOTP length and frequency
hash_algo = "sha1"
B = 64
# Generate a TOTP every 30 seconds
F = 30
# Shared Secret
shared_secret = b'BASE32SECRET2345AB=='
# OTP Length
Digits = 6
# Google Authenticator Compatibility (BASE-32)
key=base64.b32decode(shared_secret+b'='* (8 - len(shared_secret)%8))
dbg("Key Base32 Decode :")
dbg(key)
Key Base32 Decode :
b'\x08$M\xeaD\x14I=o\x9d\x00'
In [5]:
### Implement the HMAC Algorithm. For details see the rfc2104.ipynb at
# https://github.com/lordloh/OPT_algorithms/blob/master/rfc2104.ipynb

def my_hmac(key, message):
    trans_5C = bytes((x ^ 0x5C) for x in range(256))
    trans_36 = bytes((x ^ 0x36) for x in range(256))
    K_zpad=key.ljust(B,b'\0')    
    K_ipad=K_zpad.translate(trans_36)
    K_opad=K_zpad.translate(trans_5C)
    hash1 = hashlib.new(hash_algo, K_ipad+message).digest()
    hmac_hash = hashlib.new(hash_algo, K_opad + hash1).digest()
    return hmac_hash
In [6]:
### Dynamic Truncation
def dynamic_truncate(b_hash):
    hash_len=len(b_hash)
    int_hash = int.from_bytes(b_hash, byteorder='big')
    offset = int_hash & 0xF
    # Geterate a mask to get bytes from left to right of the hash
    n_shift = 8*(hash_len-offset)-32
    MASK = 0xFFFFFFFF << n_shift
    hex_mask = "0x"+("{:0"+str(2*hash_len)+"x}").format(MASK)
    P = (int_hash & MASK)>>n_shift   # Get rid of left zeros
    LSB_31 = P & 0x7FFFFFFF          # Return only the lower 31 bits
    return LSB_31
In [7]:
# function wrapper to run the HOTP algorithm multiple times for different counter value
def generate_TOTP(time_val):
    # %30 seconds
    T = i2b_time(int(time_val/F))
    # Same algorithm as HOTP
    hmac_hash = my_hmac(key,T)
    trc_hash = dynamic_truncate(hmac_hash)        # Get truncated hash (int)
    # Adjust TOTP length
    TOTP = ("{:0"+str(Digits)+"}").format(trc_hash % (10**Digits))
    #DEL dbg("\n***** ADJUST DIGITS *****\n"+str(trc_hash)+" % 10 ^ "+str(Digits)+"\nHOPT : "+HOTP)
    return TOTP
In [8]:
# Generate TOTP for current time
T0 = int(time.time())
T1 = T0 + F # F seconds later...
myTOTP0=generate_TOTP(T0)
In [9]:
# Lets see the example of generating HOTP for counter = 2
myTOTP1=generate_TOTP(T1)
In [10]:
# Similarly, lets generate HOTPs for counter = 3..10 without a lot of output messages.
debug=False
myTOTPs=[(generate_TOTP(x)) for x in range(T1+F,T1+F*5,F)]
myTOTPs.insert(0,myTOTP0)
myTOTPs.insert(1,myTOTP1)
In [11]:
print(myTOTPs)
['280672', '757502', '154326', '240371', '333692', '879303']

Compare with pyOTP Implementation

In [12]:
# Python
import pyotp
In [13]:
totp_pyOTP=pyotp.TOTP(shared_secret.decode('utf-8'))
In [14]:
# Generate 4 TOTP codes
pyTOTPs = []
for n in (range(0,3)):
    pyTOTP_val=totp_pyOTP.now()
    pyTOTPs.append(pyTOTP_val)
    print("OTP @ "+str(int(time.time()))+" = "+str(pyTOTP_val))
    print ('Waiting '+str(F)+'seconds',end='')
    for m in range (0,F):
        print(".",end='')
        time.sleep(1)
pyTOTP_val=totp_pyOTP.now()
pyTOTPs.append(pyTOTP_val)
print("OTP @ "+str(int(time.time()))+" = "+str(pyTOTP_val))
OTP @ 1535317397 = 280672
Waiting 30seconds..............................OTP @ 1535317427 = 757502
Waiting 30seconds..............................OTP @ 1535317457 = 154326
Waiting 30seconds..............................OTP @ 1535317487 = 240371
In [15]:
print(pyTOTPs)
['280672', '757502', '154326', '240371']

Compare with Google Authenticator

Google Authenticator Setup