Before you go, check out these stories!

0
Hackernoon logoImplementing 2FA: How Time-Based One-Time Password Actually Works [With Python Examples] by@luizguilhermefr

Implementing 2FA: How Time-Based One-Time Password Actually Works [With Python Examples]

Author profile picture

@luizguilhermefrLuiz Rosa

Computer Scientist, Software Engineer @ Loadsmart, Machine Learning enthusiast

If you care about your security on the web, you probably use a Two-Factor authentication (2FA) method to protect your accounts. There are various 2FA methods available out there, a combination of password + fingerprint, for example, is one of them. However, since not so many people have a fingerprint reader available all the time, one of the most popular 2FA methods today is to use an authenticator app on your cellphone to generate a temporary password that expires within a minute or even less. But, how does this temporary password, called Time-Based One-Time Password (TOTP) works, and how can I implement that on my own service?

An abstract view

This kind of authentication is not hard. It consists basically of issuing a secret key on your server and reading it on your phone, or any other device (generally using a QR code), then using this secret key to generate the passwords. That's why it works even when your phone is offline, because the secret key is stored in your phone, and therefore it is perfectly capable of generating a TOTP for you.

Generating the shared secret key

The TOTP algorithm is defined on the IETF RFC 6238, where it says the shared key "should be chosen at random or using a cryptographically strong pseudorandom generator properly seeded with a random value". This key must be encrypted to be securely stored and should be decrypted only on two occasions: when validating a password that comes in and when exposing itself to be copied by another device, that should keep it encrypted too. To generate it, we can use Python's secrets.

import secrets


def generate_shared_secret() -> str:
    return secrets.token_hex(16)
    # >> e8fb1a2faf331bfffe8670ca20447fae

Note that this secret should be unique for every user on your database, this is what will guarantee that one user cannot generate a TOTP for another.

Generating and validating a one-time password

Now that we have a shared secret, we can generate and validate an OTP. The formula is simple: TOTP = HOTP(K, T), where K is the secret key we just generated and T is a time step. In other words, we will encrypt the timestamp with our shared secret, but a raw timestamp wouldn't work, because the timeframe for the user to read and input the password would be zero. For this reason, we use a "step" factor, so the user gets more time. RFC 6238 recommends a step of 30 seconds, that may be sufficient for usability and security constraints.

import hashlib
import hmac
import math
import time


def generate_totp(shared_key: str, length: int = 6) -> str:
    now_in_seconds = math.floor(time.time())
    step_in_seconds = 30
    t = math.floor(now_in_seconds / step_in_seconds)
    hash = hmac.new(
        bytes(shared_key, encoding="utf-8"),
        t.to_bytes(length=8, byteorder="big"),
        hashlib.sha256,
    )

    return dynamic_truncation(hash, length)

We could just return the HMAC hash, but the output is way too long for the user to type (even more when there are only 30 seconds to do this). For this reason, we use the dynamic truncation algorithm to get a sample of it, usually of six digits. It was developed for the predecessor of TOTP, at RFC 4226 and consists of four steps:

  1. Convert the hash (base 16) into a binary string (base 2)
  2. Get the last four bits as an integer (base 10)
  3. Use this integer as an offset and get the next 32 bits of the binary string
  4. Convert this 32 bits to integer and get the last X digits, where X is the length you want to use
def dynamic_truncation(raw_key: hmac.HMAC, length: int) -> str:
    bitstring = bin(int(raw_key.hexdigest(), base=16))
    # >> 11010100000110011101010100010001100100011111001010111010001010110110000010111101000101011110111111010111101100011101001111100001011111101100001011110111100111001111100000100010011001010110101111100010111001001000010000011000000010010111100110101100011
    last_four_bits = bitstring[-4:]
    # >> 0011
    offset = int(last_four_bits, base=2)
    # >> 3
    chosen_32_bits = bitstring[offset * 8 : offset * 8 + 32]
    # >> 01000100011001000111110010101110
    full_totp = str(int(chosen_32_bits, base=2))
    # >> 1147436206
    return full_totp[-length:]
    # >> 436206

For the example above, 436206 is the temporary password the user would use. Now, how to validate a password on the backend? It is exactly the same. We generate the TOTP on the server using the shared key and check if it matches the input.

def validate_totp(totp: str, shared_key: str) -> bool:
    return totp == generate_totp(shared_key)

Conclusion

Implementing 2FA is not hard, but must be taken seriously to avoid breaches. No security protocol is perfect, there is no silver bullet, but why not make invaders lives harder? Do a favor for you and your users: Don't implement this by hand. Instead, prefer using a library that is constantly updated with the best security practices. This article is to understand what is going on behind the scenes, hope you've enjoyed.

The code snippets are available at my GitHub.

Tags

Become a Hackolyte

Level up your reading game by joining Hacker Noon now!