Scan a QR code once, and your phone can generate login codes forever—no internet required. The codes change every 30 seconds, yet somehow both your phone and the server always agree on the correct value. There’s no cloud synchronization, no API calls, no real-time communication of any kind. The math just works.

This isn’t magic. It’s the TOTP (Time-based One-Time Password) algorithm, defined in RFC 6238, and understanding how it works reveals one of the most elegant applications of cryptographic hash functions in everyday use.

The Shared Secret Problem

TOTP solves a fundamental challenge: how can two parties independently generate the same sequence of values without communicating? The answer lies in something they share in advance—a secret key.

When you enable 2FA on a website, the server generates a random secret, typically 160 bits (20 bytes) of cryptographically secure random data. This secret gets encoded in Base32—using only the characters A-Z and 2-7—and embedded in a QR code. The QR code follows a specific URI format:

otpauth://totp/Service:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Service&algorithm=SHA1&digits=6&period=30

The choice of Base32 isn’t about security. It’s about reliability. Base32 avoids visually similar characters (like 0 and O, or 1 and l) and remains case-insensitive, making manual entry less error-prone. The QR code itself contains no encryption—the secret is plainly visible to anyone who scans it. This is why you should never share screenshots of setup QR codes.

Time as a Counter

TOTP builds on an earlier algorithm called HOTP (HMAC-based One-Time Password), defined in RFC 4226. HOTP uses a counter that increments each time a code is generated. The problem? If you generate codes without using them, your counter drifts out of sync with the server’s.

TOTP’s innovation is simple but brilliant: replace the counter with time. Specifically, the number of 30-second intervals that have elapsed since Unix epoch (January 1, 1970, 00:00:00 UTC):

$$T = \lfloor \frac{\text{CurrentTime} - T_0}{X} \rfloor$$

Where $T_0$ is typically 0 (Unix epoch) and $X$ is the time step (30 seconds by default). At the moment I’m writing this, the Unix timestamp is approximately 1,709,000,000 seconds. Dividing by 30 gives a counter value around 56,966,666.

Both your phone and the server perform this calculation independently. As long as their clocks are reasonably synchronized, they’ll compute the same counter value and thus the same code.

The HMAC Core

With the counter determined, TOTP computes an HMAC-SHA1 hash. HMAC (Hash-based Message Authentication Code) combines a secret key with a message using a cryptographic hash function:

$$\text{HMAC}(K, m) = H((K' \oplus opad) \| H((K' \oplus ipad) \| m))$$

For TOTP, the message is the 8-byte big-endian representation of the time counter, and the key is the shared secret. The HMAC-SHA1 output is 20 bytes (160 bits), but we need to turn this into a 6-digit code that a human can type.

Here’s where the algorithm gets interesting.

Dynamic Truncation: The Ingenious Step

Converting 160 bits into a 6-digit number isn’t as simple as taking the first few bits. That would create predictable patterns. Instead, TOTP uses “dynamic truncation”—a method that extracts a different portion of the hash for each time step.

The last byte of the HMAC output is examined. Its last 4 bits (a value from 0 to 15) become an offset. Four bytes are then extracted starting at that offset:

offset = hmac[19] & 0x0f  # Last nibble, value 0-15
binary = (hmac[offset]   & 0x7f) << 24 |
         (hmac[offset+1] & 0xff) << 16 |
         (hmac[offset+2] & 0xff) << 8  |
         (hmac[offset+3] & 0xff)

The & 0x7f operation masks the most significant bit, ensuring we get a 31-bit positive integer (maximum value: 2,147,483,647). This avoids signed/unsigned integer confusion across different platforms.

Finally, we take this value modulo $10^6$ to get a 6-digit code:

$$\text{TOTP} = \text{binary} \mod 10^6$$

The result is zero-padded if necessary. A value of 42 becomes “000042”.

sequenceDiagram
    participant User
    participant App as Authenticator App
    participant Server
    
    Note over User,Server: Setup Phase
    Server->>Server: Generate secret key (160 bits)
    Server->>User: Display QR code (otpauth:// URI)
    User->>App: Scan QR code
    App->>App: Store Base32 secret
    
    Note over User,Server: Authentication Phase
    User->>App: Open authenticator
    App->>App: T = floor(current_time / 30)
    App->>App: HMAC-SHA1(secret, T)
    App->>App: Dynamic truncation → 6 digits
    App->>User: Display code (e.g., "847291")
    User->>Server: Enter username, password, code
    Server->>Server: Compute expected code (same algorithm)
    Server->>Server: Compare codes (constant-time)
    Server->>User: Authentication success

Why No Network Is Needed

The elegance of TOTP lies in its statelessness. Your authenticator app stores only the shared secret and uses the device’s clock. The server stores the same secret and uses its clock. Neither needs to contact the other during code generation or verification.

This offline capability makes TOTP robust against network outages and immune to interception during code transmission. The code itself reveals nothing about the secret—even if an attacker captures a thousand consecutive codes, they cannot reverse-engineer the key due to the one-way nature of HMAC.

The Time Drift Challenge

Clocks drift. Your phone might gain or lose a few seconds per month. Server clocks can be similarly imperfect. TOTP handles this through a validation window.

Most implementations accept codes from the current time step plus one step before and after—a 90-second window with the default 30-second period. Some servers dynamically track successful authentication times and adjust their accepted window accordingly.

For hardware tokens with batteries that can last years, clock drift becomes a significant concern. A token that drifts by 2 minutes would be unusable with a strict 30-second window. RFC 6238 recommends servers implement resynchronization mechanisms that can detect and compensate for drift.

Security Trade-offs

TOTP isn’t without vulnerabilities. The most significant: it provides no protection against real-time phishing. If you enter your code into a convincing fake login page, an attacker can immediately relay it to the real site within the validity window.

Research from Pulse Security demonstrated that without proper rate limiting, TOTP can be brute-forced surprisingly quickly. A 6-digit code has only 1,000,000 possible values. If a server accepts 10 guesses per second without lockout:

from scipy.stats import binom
# Probability of success in 5 hours with 9 valid codes (±4 steps)
1 - binom.pmf(k=0, n=10 * 3600 * 5, p=9 / 1000000)
# Result: ~80% chance of success

This is why proper TOTP implementations must enforce rate limiting—typically 3-5 failed attempts followed by a temporary lockout. Without this protection, 2FA becomes security theater.

Why SHA-1 Still Works Here

SHA-1 has been considered cryptographically broken for collisions since 2017. Yet TOTP still uses it. Why?

The attacks on SHA-1 exploit collision resistance—the ability to find two inputs that produce the same hash. HMAC-SHA1’s security depends on preimage resistance (finding an input that produces a specific output) and the secret key. Collision attacks don’t apply because the attacker cannot control both the key and the message.

NIST has approved HMAC-SHA1 through 2030 for this reason. However, modern implementations increasingly support SHA-256 and SHA-512, specified via the algorithm parameter in the otpauth URI.

Implementation Considerations

Several critical implementation details are often overlooked:

Constant-time comparison: When validating a code, use a constant-time comparison function. String comparison that short-circuits on the first mismatch creates timing side channels that leak information about correct digits.

Replay protection: Servers should track recently used codes within the validity window to prevent the same code from being used twice.

Secure secret storage: The shared secret should be encrypted at rest. For high-security applications, hardware security modules (HSMs) should manage key storage and HMAC computation.

Issuer consistency: The issuer parameter should appear in both the label and the query string, and they must match. Inconsistent values can lead to confused users and potential security issues.

The OATH Initiative

TOTP emerged from the Initiative for Open Authentication (OATH), an industry consortium formed in 2004. The goal was interoperability: a standard algorithm that any hardware manufacturer or software developer could implement without licensing fees.

RFC 4226 (HOTP) was published in December 2005. RFC 6238 (TOTP) followed in May 2011. These standards enabled the ecosystem of authenticator apps and hardware tokens we use today, from software implementations on smartphones to dedicated hardware tokens with battery lives measured in years.

When TOTP Falls Short

TOTP represents a balance between security, usability, and deployability. For most users, it provides meaningful protection against credential theft and password reuse. But it’s not the strongest 2FA available.

FIDO2/WebAuthn security keys provide superior protection by being phishing-resistant. The authentication is bound to the origin domain—the key won’t sign a challenge from a fake site, even if the user is convinced they’re on the real one.

Push-based authentication offers better UX but introduces dependency on internet connectivity and the authentication provider’s infrastructure. SMS-based 2FA is notably weaker due to SIM-swapping attacks and SS7 vulnerabilities.

TOTP occupies a middle ground: stronger than SMS, more convenient than hardware keys, and critically, works offline. For threat models that don’t include sophisticated real-time phishing, it remains an excellent choice.

The 30-Second Design Decision

Why 30 seconds? The time step represents a trade-off between security and usability. A shorter window reduces the opportunity for code theft or relay attacks but increases frustration when users can’t type fast enough. A longer window is more forgiving but gives attackers more time.

Thirty seconds emerged as the practical balance. It’s long enough to read a code, switch apps, and type it, but short enough that a captured code becomes useless within a minute. Some high-security applications use 60 seconds; banking tokens sometimes extend to 90 seconds or more.

The counter increments discretely, not continuously. At 30.0 seconds, you’re in time step $N$. At 30.1 seconds, you’re in time step $N+1$. This is why codes “change” instantly rather than gradually—the underlying counter is an integer.


References

  1. M’Raihi, D., et al. (2011). RFC 6238: TOTP: Time-Based One-Time Password Algorithm. IETF. https://datatracker.ietf.org/doc/html/rfc6238

  2. M’Raihi, D., et al. (2005). RFC 4226: HOTP: An HMAC-Based One-Time Password Algorithm. IETF. https://datatracker.ietf.org/doc/html/rfc4226

  3. Kelsey, J., et al. (1997). RFC 2104: HMAC: Keyed-Hashing for Message Authentication. IETF. https://datatracker.ietf.org/doc/html/rfc2104

  4. Pulse Security. (2021). Brute Forcing TOTP Multi-Factor Authentication is Surprisingly Realistic. https://pulsesecurity.co.nz/articles/totp-bruting

  5. NIST. (2024). SP 800-63B: Digital Identity Guidelines: Authentication and Lifecycle Management. https://pages.nist.gov/800-63-3/sp800-63b.html

  6. Google Authenticator Wiki. Key Uri Format. https://github.com/google/google-authenticator/wiki/Key-Uri-Format

  7. OATH Initiative for Open Authentication. https://en.wikipedia.org/wiki/Initiative_for_Open_Authentication

  8. Plant, L. (2019). 6 digit OTP for Two Factor Auth (2FA) is brute-forceable in 3 days. https://lukeplant.me.uk/blog/posts/6-digit-otp-for-two-factor-auth-is-brute-forceable-in-3-days/