Google Authenticator implementation in Python

58,774

Solution 1

I wanted to set a bounty on my question, but I have succeeded in creating solution. My problem seemed to be connected with incorrect value of secret key (it must be correct parameter for base64.b32decode() function).

Below I post full working solution with explanation on how to use it.

Code

The following code is enough. I have also uploaded it to GitHub as separate module called onetimepass (available here: https://github.com/tadeck/onetimepass).

import hmac, base64, struct, hashlib, time

def get_hotp_token(secret, intervals_no):
    key = base64.b32decode(secret, True)
    msg = struct.pack(">Q", intervals_no)
    h = hmac.new(key, msg, hashlib.sha1).digest()
    o = ord(h[19]) & 15
    h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
    return h

def get_totp_token(secret):
    return get_hotp_token(secret, intervals_no=int(time.time())//30)

It has two functions:

  • get_hotp_token() generates one-time token (that should invalidate after single use),
  • get_totp_token() generates token based on time (changed in 30-second intervals),

Parameters

When it comes to parameters:

  • secret is a secret value known to server (the above script) and client (Google Authenticator, by providing it as password within application),
  • intervals_no is the number incremeneted after each generation of the token (this should be probably resolved on the server by checking some finite number of integers after last successful one checked in the past)

How to use it

  1. Generate secret (it must be correct parameter for base64.b32decode()) - preferably 16-char (no = signs), as it surely worked for both script and Google Authenticator.
  2. Use get_hotp_token() if you want one-time passwords invalidated after each use. In Google Authenticator this type of passwords i mentioned as based on the counter. For checking it on the server you will need to check several values of intervals_no (as you have no quarantee that user did not generate the pass between the requests for some reason), but not less than the last working intervals_no value (thus you should probably store it somewhere).
  3. Use get_totp_token(), if you want a token working in 30-second intervals. You have to make sure both systems have correct time set (meaning that they both generate the same Unix timestamp in any given moment in time).
  4. Make sure to protect yourself from brute-force attack. If time-based password is used, then trying 1000000 values in less than 30 seconds gives 100% chance of guessing the password. In case of HMAC-based passowrds (HOTPs) it seems to be even worse.

Example

When using the following code for one-time HMAC-based password:

secret = 'MZXW633PN5XW6MZX'
for i in xrange(1, 10):
    print i, get_hotp_token(secret, intervals_no=i)

you will get the following result:

1 448400
2 656122
3 457125
4 35022
5 401553
6 581333
7 16329
8 529359
9 171710

which is corresponding to the tokens generated by the Google Authenticator app (except if shorter than 6 signs, app adds zeros to the beginning to reach a length of 6 chars).

Solution 2

I wanted a python script to generate TOTP password. So, I wrote the python script. This is my implementation. I have this info on wikipedia and some knowledge about HOTP and TOTP to write this script.

import hmac, base64, struct, hashlib, time, array

def Truncate(hmac_sha1):
    """
    Truncate represents the function that converts an HMAC-SHA-1
    value into an HOTP value as defined in Section 5.3.

    http://tools.ietf.org/html/rfc4226#section-5.3

    """
    offset = int(hmac_sha1[-1], 16)
    binary = int(hmac_sha1[(offset * 2):((offset * 2) + 8)], 16) & 0x7fffffff
    return str(binary)

def _long_to_byte_array(long_num):
    """
    helper function to convert a long number into a byte array
    """
    byte_array = array.array('B')
    for i in reversed(range(0, 8)):
        byte_array.insert(0, long_num & 0xff)
        long_num >>= 8
    return byte_array

def HOTP(K, C, digits=6):
    """
    HOTP accepts key K and counter C
    optional digits parameter can control the response length

    returns the OATH integer code with {digits} length
    """
    C_bytes = _long_to_byte_array(C)
    hmac_sha1 = hmac.new(key=K, msg=C_bytes, digestmod=hashlib.sha1).hexdigest()
    return Truncate(hmac_sha1)[-digits:]

def TOTP(K, digits=6, window=30):
    """
    TOTP is a time-based variant of HOTP.
    It accepts only key K, since the counter is derived from the current time
    optional digits parameter can control the response length
    optional window parameter controls the time window in seconds

    returns the OATH integer code with {digits} length
    """
    C = long(time.time() / window)
    return HOTP(K, C, digits=digits)

Solution 3

By following the correct answer from @tadeck and @Anish-Shah, there is a simpler method to get the code without using struct and avoiding extra imports:

""" TOTP """
import hmac
import time


def totp(key: bytes):
    """ Calculate TOTP using time and key """
    now = int(time.time() // 30)
    msg = now.to_bytes(8, "big")
    digest = hmac.new(key, msg, "sha1").digest()
    offset = digest[19] & 0xF
    code = digest[offset : offset + 4]
    code = int.from_bytes(code, "big") & 0x7FFFFFFF
    code = code % 1000000
    return "{:06d}".format(code)

This works with Python 3.

You can get the current TOTP code by calling totp(key) where the "key" is a bytes (commonly the base 32 decoded key).

Share:
58,774
Tadeck
Author by

Tadeck

Write correct code. Some kids may read it some day.

Updated on November 26, 2021

Comments

  • Tadeck
    Tadeck over 2 years

    I am trying to use one-time passwords that can be generated using Google Authenticator application.

    What Google Authenticator does

    Basically, Google Authenticator implements two types of passwords:

    • HOTP - HMAC-based One-Time Password, which means the password is changed with each call, in compliance to RFC4226, and
    • TOTP - Time-based One-Time Password, which changes for every 30-seconds period (as far as I know).

    Google Authenticator is also available as Open Source here: code.google.com/p/google-authenticator

    Current code

    I was looking for existing solutions to generate HOTP and TOTP passwords, but did not find much. The code I have is the following snippet responsible for generating HOTP:

    import hmac, base64, struct, hashlib, time
    
    def get_token(secret, digest_mode=hashlib.sha1, intervals_no=None):
        if intervals_no == None:
            intervals_no = int(time.time()) // 30
        key = base64.b32decode(secret)
        msg = struct.pack(">Q", intervals_no)
        h = hmac.new(key, msg, digest_mode).digest()
        o = ord(h[19]) & 15
        h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
        return h
    

    The problem I am facing is that the password I generate using the above code is not the same as generated using Google Authenticator app for Android. Even though I tried multiple intervals_no values (exactly first 10000, beginning with intervals_no = 0), with secret being equal to key provided within the GA app.

    Questions I have

    My questions are:

    1. What am I doing wrong?
    2. How can I generate HOTP and/or TOTP in Python?
    3. Are there any existing Python libraries for this?

    To sum up: please give me any clues that will help me implement Google Authenticator authentication within my Python code.

  • Tadeck
    Tadeck over 12 years
    @burhan: If you need the code, I have uploaded it also to GitHub (here: https://github.com/tadeck/onetimepass), so it should be quite easy to use it within projects as separate module. Enjoy!
  • Chris Moore
    Chris Moore over 11 years
    I had a problem with this code because the 'secret' I was provided by the service I'm trying to log into was lowercase, not uppercase. Changing line 4 to read "key = base64.b32decode(secret, True)" fixed the problem for me.
  • Tadeck
    Tadeck over 11 years
    @ChrisMoore: I have updated the code with casefold=True so people should not have similar problems now. Thanks for your input.
  • osmosis
    osmosis over 11 years
    I was able to get the code pasted here to work for TOTP, but was not able to get the code on github to work for TOPT.
  • Tadeck
    Tadeck over 11 years
    @osmosis: What do you meant by TOPT and how it differs from TOTP? The code here is almost identical to GitHub version - do you say the version here worked properly, but GitHub version used exactly the same way did not work?
  • Chris Moore
    Chris Moore about 11 years
    I was just given a 23 character secret by a site. Your code fails with a "TypeError: Incorrect padding" when I give it that secret. Padding the secret, like this, fixed the problem: key = base64.b32decode(secret + '===='[:3-((len(secret)-1)%4)], True)
  • musashiXXX
    musashiXXX about 10 years
    Just out of curiosity, is there a difference between the output of binascii.a2b_base64 and base64.b64decode? Is there a reason to choose one over the other?
  • Tadeck
    Tadeck about 10 years
    @musashiXXX: According to docs.python.org/2/library/binascii.html: "The binascii module contains a number of methods to convert between binary and various ASCII-encoded binary representations. Normally, you will not use these functions directly but use wrapper modules like uu, base64, or binhex instead. The binascii module contains low-level functions written in C for greater speed that are used by the higher-level modules".
  • Tadeck
    Tadeck about 10 years
    Interesting, but you may wish to make it more understandable for the reader. Please make variable names more meaningful, or add docstrings. Also, following PEP8 may get you more support. Did you compare performance between these two solutions? Last question: is your solution compatible with Google Authenticator (as the question was about this specific solution)?
  • Anish Shah
    Anish Shah about 10 years
    @Tadeck I have added some comments. And I got my things done using this script. so yeah, it should work perfectly.
  • Orville
    Orville about 5 years
    for python 3: change: ord(h[19]) & 15 into : o = h[19] & 15 Thanks BTW
  • Francois
    Francois over 4 years
    for the padding issue ('TypeError: Incorrect padding'), you can use the following def add_required_padding(auth_key): multiples_of = 8 output = auth_key if len(output) % multiples_of: # not a multiple of (multiples_of), add padding: output += '=' * (multiples_of - len(output) % multiples_of) return output
  • Math Avengers
    Math Avengers almost 4 years
    @Francois I got the padding issue, do we use this "add_required_padding()" in the get_hotp_token()?
  • Noam
    Noam over 3 years
    I suggest to change get_totp_token() to always return a string of 6 digits. Replace return h with return '{:06}'.format(h)
  • catscoolzhyk
    catscoolzhyk over 3 years
    If you want to get the secret from QR code. Just scan the QR code with your smartphone and look at the link. You may found the secret in the args of that link. Example: otpauth://totp/somelink?secret=your_secret&digits=6&period=3‌​0&issuer=some_issuer‌​. In my case, I can't copy and look at the link, because I already installed otp app like Google Authenticator so it redirects to it. Try to delete or use another QR scanner to get the link.