How TOTP Two-Factor Codes Actually Work Under the Hood

You open Google Authenticator, glance at the six-digit code for your bank, and type it in before it rolls over. The server checks it. Green checkmark. But how does the server — sitting in a data center thousands of kilometers away, completely isolated from your phone — produce the exact same six digits at the exact same moment? No network call. No shared session. Just math.

This is TOTP: Time-based One-Time Password, standardized in RFC 6238. Once you understand the mechanics, you'll never look at that little countdown circle the same way again.

The Shared Secret: The One Thing Both Sides Know

When you set up 2FA on any service — scanning a QR code, or copying a base32 string like JBSWY3DPEHPK3PXP — what's actually happening is the server is handing you a shared secret. It's typically 20 bytes (160 bits) of random data, encoded in base32 so it's human-typeable if needed. Your authenticator app stores this secret. The server stores this secret. Neither side ever transmits it again.

That secret is the seed for everything. Lose it and you lose your 2FA access. Expose it and an attacker can generate codes forever. This is why backup codes matter — if your phone dies, that secret is gone with it unless you've exported or printed your setup codes.

Unix Time as a Clock Signal

Here's where it gets clever. Both your phone and the server have access to one more shared piece of information that requires zero communication: the current time.

TOTP works by dividing Unix time — seconds elapsed since January 1, 1970, UTC — into 30-second buckets. The formula is almost insultingly simple:

T = floor(current_unix_time / 30)

At the moment I'm writing this, if the Unix timestamp is, say, 1750000200, then T = floor(1750000200 / 30) = 58333340. Both your phone and the server compute this same integer, independently, at the same moment. This integer T is called the time counter. It increments by 1 every 30 seconds, which is why your authenticator code changes every 30 seconds on the dot.

The 30-second window is a deliberate engineering tradeoff. Shorter windows (say, 10 seconds) would make codes harder to steal in a real-time phishing attack but would also cause more failed logins due to clock drift. Longer windows are more forgiving but give attackers more time to reuse a stolen code. RFC 6238 recommends 30 seconds, and that's what virtually every implementation uses. Most servers also accept codes from the adjacent windows (T-1 and T+1) to absorb small clock differences between your phone and the server.

HMAC-SHA1: Turning Two Inputs Into a Fingerprint

Now you have two things: the shared secret K and the time counter T. The next step is to combine them cryptographically. TOTP delegates this to its older sibling, HOTP (RFC 4226), which uses HMAC-SHA1.

HMAC stands for Hash-based Message Authentication Code. It's a way to use a cryptographic hash function (in this case SHA-1) together with a secret key to produce a fixed-length digest that proves both the message and the key were involved. The structure looks like this:

HMAC-SHA1(K, T_bytes)

where T_bytes is the time counter encoded as an 8-byte big-endian integer. SHA-1 produces a 20-byte (160-bit) output. So now you have 20 bytes of data that is completely determined by your shared secret and the current 30-second window. Anyone with the same secret and the same clock will produce the same 20 bytes.

One thing worth addressing: SHA-1 has well-known collision vulnerabilities, which is why it's been deprecated for certificate signing and similar uses. But HMAC-SHA1 is still considered secure for this application. The attacks on SHA-1 exploit weaknesses in finding two inputs that hash to the same output — they don't undermine HMAC's security model, which relies on SHA-1's preimage resistance, not collision resistance. TOTP over HMAC-SHA256 or HMAC-SHA512 also exist (and are supported by some apps), but HMAC-SHA1 is the default in RFC 6238 and what you'll encounter nearly everywhere.

Dynamic Truncation: Carving Six Digits From 20 Bytes

You've got 20 bytes. You need 6 decimal digits. This is where dynamic truncation comes in, and it's one of the more interesting parts of the algorithm because it has an element of self-referential cleverness.

Here's how it works, step by step:

  1. Take the last byte of the HMAC-SHA1 output.
  2. Take the low-order 4 bits of that byte (i.e., last_byte & 0x0F). Call this offset. It will be a number between 0 and 15.
  3. Extract 4 bytes from the HMAC output starting at position offset.
  4. Mask the most significant bit of those 4 bytes to zero (to avoid signed integer issues): bytes[offset] & 0x7F.
  5. Interpret the result as a 31-bit unsigned integer.
  6. Take mod 1,000,000 to get your 6-digit code (zero-padded if needed).

In Python, this looks roughly like:

import hmac, hashlib, struct, time

def totp(secret_bytes):
    T = int(time.time()) // 30
    T_bytes = struct.pack('>Q', T)  # 8-byte big-endian
    mac = hmac.new(secret_bytes, T_bytes, hashlib.sha1).digest()
    offset = mac[-1] & 0x0F
    code = struct.unpack('>I', mac[offset:offset+4])[0] & 0x7FFFFFFF
    return str(code % 1_000_000).zfill(6)

The "dynamic" part of dynamic truncation is that the offset itself comes from the hash output — it varies with every new time window, since the hash output changes. This prevents any fixed-offset shortcut from leaking partial information about the HMAC output over many observations.

Why This Is Actually Secure

Let's say an attacker intercepts a valid TOTP code. What can they do with it? Not much, for a few reasons:

Single use within a window. Good TOTP implementations track which codes have been used. Even if you enter a code with 25 seconds left on the clock, that same code should be rejected if submitted again — it's been consumed.

30 seconds is a tight window. Real-time phishing attacks (where a fake login page forwards your credentials to the real site instantly) can defeat TOTP, but this requires the attacker to be active and fast. Classic credential stuffing — where stolen password dumps are tested days or weeks later — is completely neutralized. The code is dead before the attacker can use it.

The shared secret never travels on the wire after setup. Every time you enter a code, you're proving you possess the secret without revealing the secret itself. An attacker who intercepts a code learns nothing about future codes, because the secret is never derived from or exposed by any individual code.

Brute force is impractical in 30 seconds. There are one million possible 6-digit codes. Servers enforce rate limiting and lockouts. Statistically, a lucky guess has a one-in-a-million shot. Even if an attacker tries a few hundred combinations before getting blocked, they're nowhere close.

The Clock Drift Problem (And How Servers Handle It)

One practical wrinkle: your phone's clock might be slightly off from the server's clock. NTP keeps most devices synchronized to within a second or two, but edge cases exist — airplane mode for days, battery-dead devices, old hardware. A 30-second window helps, but it's not unlimited.

Servers typically implement a small tolerance window: they check not just the current time step T, but also T-1 and T+1. This means a code can remain valid for up to 90 seconds in the worst case (30 seconds into T-1, then through all of T, then into the first second of T+1). RFC 6238 explicitly recommends this approach and suggests logging when drift is detected so the server can compensate over time.

Some enterprise implementations also maintain a per-user clock offset, so if your phone consistently runs 15 seconds ahead, the server learns this and adjusts its window accordingly. This is invisible to you as a user — you just keep scanning codes that work.

What the QR Code Actually Contains

The QR code you scan when setting up TOTP encodes a URI in a standardized format defined by Google but now universally adopted:

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

Everything is there: the secret (base32-encoded), the hash algorithm, the digit count, and the time period. Your authenticator app parses this URI, stores the secret, and from that moment forward it never needs to talk to the server again to generate codes. The cryptography does the coordination.

The Bigger Picture

TOTP is elegant because it achieves something philosophically interesting: two parties prove they share a secret without ever transmitting that secret during authentication. Every 30-second code is a cryptographic proof-of-possession, timestamped and single-use. The RFC 6238 algorithm — shared secret plus Unix time counter, fed through HMAC-SHA1, then dynamically truncated to six digits — is small enough to implement from scratch in an afternoon, yet robust enough to protect hundreds of millions of accounts worldwide.

Next time you glance at that rotating code before it ticks over, you'll know exactly what's happening in the milliseconds between your phone's screen and the server's database: a 20-byte HMAC output, a 4-byte slice, a modulo, and a handshake that required no network call to complete.