Those Little Six Digits: A Deep Dive into TOTP
What’s really going on behind the scenes of two-factor authentication apps
Introduction
A lot of people use TOTP every day but probably don’t know what the technical background for it is. Those little six-digit authentication codes you receive? That’s probably TOTP.
Time-based One-Time Passwords (TOTP) are a form of two-factor authentication (2FA) that improve account security by requiring both knowledge of a password and possession of a device capable of generating time-sensitive passcodes. TOTP is standardized under RFC 6238 and is based on the HMAC-based One-Time Password (HOTP) algorithm, with the addition of time-based validity to ensure that each generated code is only valid for a short duration, typically 30 seconds.
The Time-Based One-Time Password (TOTP) mechanism operates in an simple manner: a shared secret is generated and stored both on the server and the client device, such as a smartphone. This secret, in conjunction with the current time rounded to 30-second intervals, is processed through a cryptographic hash function, typically HMAC-SHA1, to produce a 6-digit code. As both the server and the client utilize the same secret and synchronize their clocks, they generate the identical code independently. This enables the server to authenticate the client without requiring the transmission of the code across the network.
Applications like Google Authenticator or Authy leverage TOTP to provide this second layer of authentication. When a user scans a QR code for a site or app, they're importing the shared secret encoded in a specific URI format. This URI typically includes the shared secret string, and for clarity and organization - the user account name, and the service provider itself. From that point on, their app can generate the correct TOTP code every 30 seconds, in sync with the server. (Note the service providers may have mechanisms to allow perhaps three valid TOTP codes, one interval forward and one interval behind in case clocks aren’t synced perfectly.) This technique is now the backbone of 2FA systems for everything from email accounts to banking apps, offering strong protection even if a user’s primary password is compromised.
In many cases, those familiar six-digit codes sent to your phone via SMS may also be based on a TOTP-like mechanism. The code is generated using a shared secret and a time window. However, it's worth noting that SMS-based 2FA can vary in implementation, and some providers may rely on alternative backend systems or simply generate codes server-side and push them out, rather than using the formal TOTP spec.
Speaking of being secure, consider a one-time tip to sonnik via Stripe’s secure payment options.
Is it Secure?
From my perspective, I consider TOTP more secure than SMS-based two-factor authentication. Both methods deliver a secondary code to the user, but SMS has a vulnerability: phone numbers may not be fully secured. Through social engineering and phone calls, attackers can hijack SIM cards, redirecting texts and calls (including 2FA codes) to themselves. While there are improvements being made to prevent SIM hijacking, the risk still exists. TOTP however relies on a shared cryptographic secret stored locally on the user's device, rather than a text message sent over a cellular network.
The security of TOTP relies on the secrecy and integrity of the shared secret. Once generated and stored (both on the authentication server and in your app) it must remain private. This is why apps like Google Authenticator do not allow you to view the secret again after setup. You can export all secrets to another device (usually through a secure migration process), but extracting individual secrets requires significant effort. This design ensures that if an unauthorized app or person could access the shared secret, they could clone the token generator and compromise the second factor.
It is important to note that while Time-based One-Time Password (TOTP) can be highly secure when implemented correctly, it is not infallible. Factors such as malware on your phone, poorly developed applications, or phishing sites that deceive you into entering your 6-digit code in real time can still compromise your session. Nevertheless, when TOTP is combined with robust operational practice (such as device encryption, the use of password managers, and avoiding password reuse) it provides a strong secondary layer of defense that significantly increases the difficulty for potential intruders.
One potential issue is the risk and inconvenience if your mobile device is lost. For instance, having Google Authenticator set up on both an iPhone and an iPad means that losing one device will still allow access to the codes via the other device. Additionally, some providers recommend downloading and printing “recovery keys”. This can be useful, but if a user has a folder with “secret recovery keys” on their desk, that’s risk. Overall, security effectiveness depends on the strength of all security measures employed.
How TOTP Works
Time-based One-Time Password (TOTP) authentication begins with a shared secret, which is a randomly generated string of characters, typically encoded in Base32. This secret is established once during the initial configuration of two-factor authentication (2FA) and is stored both on the authentication server and the user's device. The shared secret generally ranges from 16 to 32 characters in length and comprises uppercase letters and digits, excluding easily confused characters such as '0' and 'O'. The randomness and complexity of this secret are crucial; the more unpredictable it is, the more challenging it becomes for an attacker to successfully perform a brute-force attack or guess the code.
An essential aspect of TOTP’s reliability is time synchronization. The algorithm relies on both the client (your phone or tablet) and the server calculating the current time-based interval uniformly. Each interval usually lasts 30 seconds. Therefore, your device must be adequately synchronized with a time standard task most phones and tablets regularly accomplish through synchronization with network time servers, such as those managed by NIST via the Network Time Protocol (NTP). If there is a significant discrepancy in your device’s clock, the generated codes will not correspond to the server's expectations.
To generate a code, the current timestamp (rounded down to the nearest 30-second window) is converted into an 8-byte binary value. This timestamp is then combined with the shared secret in an HMAC (Hash-based Message Authentication Code) operation, typically using the SHA-1 hash algorithm. The result is a 20-byte (160-bit) hash, which is a long hex string representing the output of the cryptographic function.
In this process, a specific portion of the hash is extracted through dynamic truncation. The algorithm uses the last nibble (4 bits) of the hash to determine the starting point for slicing. It then takes 4 bytes from that offset and converts them into a 32-bit binary number. This binary value is transformed into a decimal number, which is then subjected to modulo 1,000,000 to generate a six-digit number. This produces the familiar six-digit TOTP code seen in the app.
This process repeats at 30-second intervals, generating a new code based on the subsequent timestamp window. Due to the shared secret remaining constant and only the timestamp varying, both the application and the server can independently generate the same value without needing direct communication. This is the fundamental advantage of TOTP: it is decentralized, time-based, mathematically deterministic, and ensures that your authentication factors remain secure and under your control.
Sample process (verbose output from the python script):
TOTP: 330626
Raw HMAC hash (hex): d6e254d5107c0f190134e6e0991714490882e5ee
Offset: 14
Truncated binary: 340330626
Final TOTP (mod 1,000,000): 330626
The last nibble of the hash is hexadecimal “e” – which produces the decimal 14. At offset 14, we have hex 14490882 – which converts to 340330626. We then take the modulo 1,000,000 of this number (basically truncating to the last six digits). This becomes the six-digit ‘checksum’ of sorts, confirming that you’re using the correct shared secret.
Using the Python Script
To generate a new shared secret and QR Code that you can test with Google Authenticator.
python totp.py –generate
The output will be similar to the following example. A sample QR Code and secret are provided here for testing purposes. This should not be used for an actual shared secret, as it is published in an online article.
Secret: PPE3NCWMO4YUPMCQWAT2SRQ47QIXS3AT
Scan this QR code with Google Authenticator:
Cut and paste the secret string from above to a notepad. We’ll need this for the next steps for testing. Again, be careful if you’re using this for any of your internal systems. Don’t leave real secrets just laying around for others to take. Also, you can install Google Authenticator from the Apple App Store, or Google Play for testing if you don’t have it already. You can make sure this python script produces the same six-digit numbers that Google Authenticator does.
To produce simple output (using my test shared secret from above):
python totp.py --showtotp PPE3NCWMO4YUPMCQWAT2SRQ47QIXS3AT
This should produce output that matches Google Authenticator.
For the verbose output …
python totp.py --showtotp PPE3NCWMO4YUPMCQWAT2SRQ47QIXS3AT –verbose
While drafting this article to explain the code, I noticed that the "time remaining" within the 30-second interval was not displayed. I added it in, so although it is not shown in the screenshot, it will be included in your output.
The Code and Possible Features
You can find this code in the TOTP directory on this Github repository. Alternatively, the code is available here.
Please note the following section. When a QR code is scanned into Google Authenticator, it usually includes the account name and the service. This example is left generic, but a potential future feature could allow for a command line parameter and argument to be included during QR code generation.
import argparse
import pyotp
import qrcode
import base64
import hashlib
import hmac
import time
# Random generation for testing PPE3NCWMO4YUPMCQWAT2SRQ47QIXS3AT
def generate_secret():
"""
Generate a shared secret and QR code for Google Authenticator.
:return:
"""
secret = pyotp.random_base32()
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(name="user@example.com", issuer_name="MyApp")
# Change above line to use your own email and app name to embed in the QR code
# Google Authenticator allows these values to be edited by swiping left
print(f"Secret: {secret}")
print("Scan this QR code with Google Authenticator:")
qr = qrcode.QRCode()
qr.add_data(uri)
qr.make()
qr.print_ascii()
def show_totp(secret, verbose=False):
"""
Show the current TOTP for the given shared secret.
:param secret: full string of initial secret
:param verbose: boolean if full process (hash, decimal, six digit truncation)
:return:
"""
totp = pyotp.TOTP(secret)
now = int(time.time())
time_remaining = 30 - (now % 30)
interval = now // 30 # 30 second intervals - same as Google Authenticator
key = base64.b32decode(secret, True)
msg = interval.to_bytes(8, 'big')
h = hmac.new(key, msg, hashlib.sha1).digest()
offset = h[19] & 0x0F
binary = ((h[offset] & 0x7f) << 24 |
(h[offset + 1] & 0xff) << 16 |
(h[offset + 2] & 0xff) << 8 |
(h[offset + 3] & 0xff))
otp = binary % 1000000
print(f"TOTP: {otp:06d} (valid for {time_remaining}s)")
if verbose:
print(f"Raw HMAC hash (hex): {h.hex()}")
print(f"Offset: {offset}")
print(f"Truncated binary: {binary}")
print(f"Final TOTP (mod 1,000,000): {otp:06d}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="TOTP Tool")
parser.add_argument('--generate', action='store_true', help="Generate a shared secret and QR code")
parser.add_argument('--showtotp', metavar='SECRET',
help="Show the current TOTP for the given shared secret")
parser.add_argument('--verbose', action='store_true', help="Show raw hash and extra data")
args = parser.parse_args()
if args.generate:
generate_secret()
elif args.showtotp:
show_totp(args.showtotp, args.verbose)
else:
parser.print_help()
I also didn’t put any effort into storing the secrets for later use. There are multiple reasons for this. For example, a user interface would have to be designed to find/select from multiple secrets. Additionally, writing or reading a properly encrypted data file with a master password is trivial, but once the data is read, it would be susceptible to exposure by processes with elevated privileges that can snoop through memory.
Tips
When you set up Two-Factor Authentication (2FA), some sites will give you an option to get the text secret. It may be hidden behind some link that says “unable to scan QR?” or “use another authentication app”. The account providers do not always offer this option readily. The account providers don’t always make this option obvious. However, with a little digging (like clicking links labeled ‘Can’t scan QR?’) you can often access the shared secret. Alternatively, if you have access to on-screen QR code readers on your Windows or Mac machine, you may be able to extract the shared secret. Please be extremely careful with online QR code readers that have you upload a PNG (screenshot) to decode; I’d recommend avoiding these altogether.
Conclusion
TOTP offers a smart balance between simplicity and security. It’s easy enough for everyday users to adopt, yet strong enough to ward off most opportunistic attacks. By leveraging time, a shared secret, and cryptographic hashing, it creates a system where codes are both short-lived and hard to predict. While not immune to every threat, especially in a world of phishing and malware, TOTP significantly raises the bar compared to passwords alone or even SMS-based verification. For those looking to secure their accounts without buying dedicated hardware, it remains one of the most practical and trusted tools in the authentication toolbox. Hopefully you find this code helpful in some manner; let me know in the comments below.