How Time Based Otp Work Python

Jun 11th, 2021 - written by Kimserey with .

One-time passwords (OTP) are a great way to provide a second factor of authentication to an application. They are commonly distributed through channels like SMS, voice call, email, or physical token generator - common with banks. Although very useful, each of those distribution channels have limitations on both side; for the user and for the application developer. For example, for the user, SMS or voice call needs network connectivity which isn’t always available, and from the application developer, we need to rely on third party SMS gateway or voice call gateway which don’t support all countries, making 2FA unavailable for some. HMAC-based OTP (HMAC) and later on Time-based OTP (TOTP) were algorithms invented to address those problems providing a way for a prover to generate a valid OTP without the need of the verifier to send it to the prover. In today’s post we will see an example of TOTP implementation and look at how it is constructed.

Generate TOTP in Python

To generate TOTP we can use pyotp package which is an implementation of the HOTP and TOTP algorithms.

1
pip install pyotp

The basis of TOTP is a shared secret between prover and verifier:

1
2
3
4
>>> import pyotp
>>> secret = pyotp.random_base32(26)
>>> secret
'JGNF6HRGQGTZIXI6BYUXNZMFQ3'

We generate a 26 digits base32 secret which will be known by prover and verifier. Then using that secret we are able to generate TOTP:

1
2
3
4
>>> totp = pyotp.TOTP(secret)
>>> token = totp.now()
>>> token
'703537'

Just by knowing the secret, we are able generate the OTP.

From the users side, they will be using a token generator such as Google Authenticator which knows the secret and is able to generate the OTP in the same way on the users devices. Then the users are able to send the token that they have generated on their own, and from the application, we are able to verify it by generating the same token on our end.

1
2
>>> totp.verify(token)
True

Token are meant to be valid by steps of 30 seconds since epoch, meaning T=0 for 29 seconds, T=1 for 40 seconds, T=2 for 65 seconds, etc.. We can verify that by delaying the verification by 30 seconds:

1
2
3
>>> time.sleep(30)
>>> totp.verify(token)
False

This means that the authenticator device must be in sync with the server implementation in term of the step used (the default being 30 seconds).

Coming back to the secret, the verifier generates it and shares it to the prover. This step only happens once, once the secret is known by the prover, they can both indepedently generate tokens. The most common way to share the secret is via QR code. We can generate the QR code URL with pyotp:

1
2
>>> pyotp.TOTP(secret).provisioning_uri(name='[email protected]', issuer_name='My App')
'otpauth://totp/My%20App:alice%40gmail.com?secret=JGNF6HRGQGTZIXI6BYUXNZMFQ3&issuer=My%20App'

We can see that the URL provides all the details necessary for token authenticators to save the information, we have the scheme otpauth, the type totp (as opposed to hotp), the application name, the prover name and the secret. Using this URL we can generate the following QR code:

git log double dot

And we can scan this with Google Authenticator to be able to register the secret and generate tokens in interval of 30 seconds. We can then compare those tokens with the tokens generated by totp.now() and we should see that they match.

How are TOTP Generated

Time-based OTP are a derivation of HMAC-based OTP. HMAC-based OTP are OTP generated based on the HMAC of the secret plus a counter while Time-baed OTP are OTP generated based on the HMAC of the secret plus the unix timestamp. The counter is replaced by the unix timestamp, this makes it easier to ensure synchronization between prover and verifier as they are more likely to always be in sync if looking at the unix timestamp. Message Authentication Code (MAC) are used to authenticate a message. HMAC is a hash-based MAC, where the message is hashed with the secret in order to hide the secret. HMAC can be used for signature for example HMAC-SHA256 is used in JWT tokens.

In the context of OTP, the following steps are the steps used to created the OTP:

  1. hash the secret with the current counter value (HOTP) or unix timestamp in 30s steps (TOTP),
  2. apply dynamic truncating to the hash to reduce it,
  3. convert the resulting bytes to decimal and truncate the lower digits to get the OTP.

If we define:

  • K as the secret,
  • T as the timestamp by 30s step,
  • C as the counter,

the steps can be abbreviated to:

1
2
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
TOTP = HOTP(K, T)

where the hash is truncated to provide HOTP where the counter is replaced by the timestamp for TOTP.

In pyotp, the hash of the secret with the timestamp is simply executed using hmac module:

1
hmac.new(secret, input, hashlib.sha1)

which represents the first step HS(SECRET, time/30).

Next the truncation is executed by taking the following steps:

  1. take the last nibble representing the offset,
  2. take 4 bytes starting at the offset while masking the most significant bit (MSB) to avoid confusion with signed bits,
  3. convert to decimal and truncate to the necessary digits by executing a modulo with 10 to the power of digits requested.

In pyotp, we can see this logic executed as followed:

1
2
3
4
5
6
7
8
digits = 6

offset = hmac_hash[-1] & 0xf
code = ((hmac_hash[offset] & 0x7f) << 24 |
        (hmac_hash[offset + 1] & 0xff) << 16 |
        (hmac_hash[offset + 2] & 0xff) << 8 |
        (hmac_hash[offset + 3] & 0xff))
str_code = str(code % 10 ** digits)

offset = hmac_hash[-1] & 0xf will get the offset by getting the last byte of the bytearray and masking the first nibble to keep the last nibble only constituting the offset.

Using the offset, 4 bytes are selected from the hash, and appended together:

  1. (hmac_hash[offset] & 0x7f) << 24: select the byte at the offset and execute a bitewise AND with 0x7f (0111 1111) which mask the MSB to avoid possible confusion with signed bits, bitwise left shift by 24 bits to compose the first part of the resulting bytes,
  2. (hmac_hash[offset + 1] & 0xff) << 16: then continue to select the next byte with left shift plus OR until the last one,
  3. code % 10 ** digits: modulo the resulting decimal value to 10 to the power of the needed digits - an efficient way of truncating a number as module 10^2 will keep the first two digits.

So if we take the example from RFC4226 where the hmac is 0x1f8698690e02ca16618550ef7f19da8e945b555a, we get the following byte array:

1
2
3
4
5
6
7
8
9
   -------------------------------------------------------------
   | Byte Number                                               |
   -------------------------------------------------------------
   |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|
   -------------------------------------------------------------
   | Byte Value                                                |
   -------------------------------------------------------------
   |1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a|
   -------------------------------***********----------------++|
  1. We identify the offset byte as 5a hence the offset as a = 10,
  2. We take the 4 bytes from 10 onward resulting in 0x50ef7f19 and we mask the MSB, but for 0x50 (01010000) the MSB is already 0 so it remains 0x50ef7f19,
  3. then convert 0x50ef7f19 to decimal 1357872921 and modulo 10^6, to the power of six because we want our OTP to be 6 digits and we get the result 872921.

The resulting OTP will be 872921.

Conclusion

In today’s post we looked at Time-based OTP, how we could generate them in Python and how the token was generated. TOTP is a great way of enhancing security without any compromises, it allows developers to bake into their applications 2FA without needing to be tied to a third party and it allows users to generated the tokens from devices like phones or tablets from simple interfaces. I hope you liked this post and I see you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.