visit
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 , 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
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)
def dynamic_truncation(raw_key: hmac.HMAC, length: int) -> str:
bitstring = bin(int(raw_key.hexdigest(), base=16))
# >> 0000000000000000000000101100011
last_four_bits = bitstring[-4:]
# >> 0011
offset = int(last_four_bits, base=2)
# >> 3
chosen_32_bits = bitstring[offset * 8 : offset * 8 + 32]
# >> 0010101110
full_totp = str(int(chosen_32_bits, base=2))
# >> 1147436206
return full_totp[-length:]
# >> 436206
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 .