Skip to content
2FAAuth.Work
Language: English
10 min read By 2faauth.work Editorial policy

How TOTP Works: The Math Behind Your 6-Digit Code

A plain-English walkthrough of how time-based one-time password codes are generated, from shared secret to the six digits you type.

What a TOTP Code Actually Is

Open your authenticator app and you'll see a 6-digit number counting down its 30-second lifespan. That number is a TOTP code: a time-based one-time password, standardised in RFC 6238 as an extension of the earlier HMAC-based scheme, HOTP.

The key idea: the code isn't a password. It's proof that you hold a shared secret — a value installed on your device when you first set up 2FA. Because the code is derived from that secret plus the current time, it changes every 30 seconds and is useless to anyone who glimpses it after you've typed it.

Nothing travels to make this work. No SMS, no connection to the login server, no network at all. Your app and the server perform the same calculation independently, on their own clocks. Same inputs, same math, same code — that's the whole trick.

This offline design is also why airplane mode doesn't break your authenticator, and why a stolen phone with the screen locked still can't show codes. The only inputs are the secret stored in the app and the device clock; take either away and the calculation simply stops.

It also means the code is predictable to anyone holding the secret — but only for one 30-second window. An attacker who steals yesterday's codes has yesterday's codes. Each one expires and never returns, unlike a reused password.

The Shared Secret: Where Everything Starts

When you enable two-factor authentication on an account, the server generates a random secret key — typically 20 bytes of cryptographic randomness. This single string of characters is the root of everything that follows.

The server hands it to you once, usually as a QR code. What looks like a pixel pattern is actually just encoded text: the secret itself, written in Base32 (letters A–Z and digits 2–7), plus the account name and issuer. Your authenticator app scans it, decodes it, and stores the secret locally.

Some sites also show the Base32 text directly, useful when you cannot scan — for example, on a desktop screen with no second camera. In that case, type it into your app manually, character by character. Base32 uses no ambiguous letters like 0, 1, 8, or 9, so transcription errors are less likely, but a single wrong character produces codes the server will reject. If every code fails, re-enroll rather than guessing.

From that moment, two copies of the same secret exist: one on the server, one in your app. Neither is ever sent over the network again. Every future code is derived from this shared value — which is why protecting it matters.

A screenshot of that QR code is a permanent copy of your key. Anyone who sees it can generate valid codes forever. Scan it, then delete it — including from cloud-synced photo backups and your phone's trash folder.

The Time Step: A Moving Counter

TOTP never uses the raw clock directly. Instead, both your app and the server take the current Unix timestamp (seconds since 1970) and divide it by 30, discarding the remainder:

counter = floor(unix_time / 30)

The result is a plain integer that ticks up by one every 30 seconds. At the moment of writing, that counter is some huge number — but the app only cares that both sides compute the same value. Same counter plus same secret means same code.

The 30-second window is itself a compromise. A shorter window would mean fresher codes but more typing frustration; a longer one would leave each code valid for more time. Most authenticator apps and servers settle on 30 seconds, and changing it on one side without the other breaks everything.

Why Clock Drift Breaks Codes

The whole scheme hinges on agreement. If your phone's clock is 40 seconds off, you compute counter N while the server is already on N+1 — different codes, "invalid code" error, even though you typed it perfectly.

Servers know clocks drift, so most accept the code from the previous or next time step as a grace window. Beyond that, resync your device clock (set it to automatic).

A common mistake: typing a code just as the timer rolls over. If 2 seconds remain, the server may already be validating against the next counter. Wait for a fresh code when the countdown is nearly empty. If codes fail repeatedly on one device only, that device's clock is the suspect — not the secret.

HMAC: Mixing the Secret and the Counter

At the heart of every TOTP code sits a single function: HMAC-SHA1. HMAC stands for Hash-based Message Authentication Code, and its job here is simple — blend two inputs into one unpredictable output.

The two inputs:

  • The shared secret — the key, known only to your phone and the server
  • The time counter — the number that changes every 30 seconds

Think of HMAC as a keyed hash. A plain hash scrambles data the same way every time. HMAC scrambles it differently depending on the key. Feed it the same secret with counter 48291 and you always get the same 20-byte result. Change the counter by one, and the output shifts into something that looks completely unrelated.

That determinism matters: your phone and the server never talk during login, yet both compute the identical 20 bytes independently. Same inputs, same output, no network call needed.

Crucially, the process only flows one way. Seeing millions of outputs tells an attacker nothing usable about the secret — that's the property making the whole scheme safe. An attacker who captures yesterday's codes learns nothing about today's, because each counter produces an independent-looking result.

Note the algorithm choice: SHA1 here is not a weakness. HMAC-SHA1 remains unbroken for this use; the hash's collision flaws don't apply to HMAC's construction.

Truncation: Getting From 20 Bytes to 6 Digits

HMAC-SHA-1 produces 20 bytes — far more than a display can sensibly show. TOTP compresses this with dynamic truncation:

  1. Take the last nibble (lowest 4 bits) of the HMAC output. This gives a number from 0 to 15.
  2. Use it as an offset: grab the 4 bytes starting at that position in the output.
  3. Mask off the top bit of those 4 bytes, leaving a 31-bit integer.

That integer is still too large to display, so it's reduced modulo 1,000,000. The remainder — anywhere from 0 to 999,999 — becomes the code, zero-padded to 6 digits. A remainder of 4,207 displays as 042207.

It's called dynamic because the offset comes from the data itself: each code samples a different slice of the HMAC output.

The top-bit mask exists for a practical reason: without it, the 4 bytes could land in a range some languages treat as a negative 32-bit integer, and a negative number breaks the modulo step. Clearing that bit keeps the result a plain positive value everywhere.

Zero-padding matters too — a server comparing strings must see 042207, not 42207, or valid sign-ins would fail one time in ten.

Some services use 7- or 8-digit codes, reducing modulo 10,000,000 or 100,000,000 instead — same math, larger code space, harder to guess.

Why the Code Is Safe to Type

A 6-digit code offers a million possible values — modest on paper. What makes it work is that you only get a handful of guesses. Servers rate-limit login attempts and reject repeated failures, so an attacker can't cycle through combinations. Rate limiting, not code length, is the real defense. An attacker seeing three failed attempts per window would need hundreds of thousands of tries; a server that locks out after five makes that impossible in practice.

Each code is also short-lived. It's tied to a 30-second window and accepted only briefly around it, so a stolen code expires before it can be reused. A code read over your shoulder at 9:00:50 is worthless by 9:01:30, even if the attacker tries immediately. Most implementations accept the previous and next window to absorb clock drift, but a code never stays valid for minutes.

Finally, seeing a code reveals nothing about the future. The code is the output of an HMAC computation using your secret key. HMAC is a one-way function: given the output, there's no way to reconstruct the input. An attacker who glimpses today's code can't derive the secret, and can't predict tomorrow's code — because they don't know the counter's pairing value underneath.

One caveat: this protection assumes the server enforces rate limits. If a site accepts unlimited attempts, a million-value space becomes brute-forceable — which is why phishers try to relay codes in real time rather than guess them.

Common Questions and Edge Cases

Why Codes Fail

The most common cause is clock drift. Your phone's clock and the server's clock must stay within the allowed window, so a phone showing the wrong time produces wrong codes. Even a few minutes of drift can push you outside the acceptance window. Fix: enable automatic time syncing, then test the code before relying on it.

A wrong secret also fails silently—every code looks valid but never matches. This usually happens when the QR code was scanned into a different account's setup, for example adding your work account's code to your personal entry. Label each entry in your authenticator app and delete duplicates.

Finally, QR codes can expire: many services invalidate the enrollment code after a short period or after you proceed past the screen, so an old screenshot won't work. If enrollment fails, restart the setup flow to get a fresh QR code rather than reusing a saved image.

Can a Code Be Reused?

Yes, technically—but most servers reject a code that was already accepted within the current time step. This blocks attackers who glimpse a code and replay it seconds later. Some servers track the last used time step and refuse anything at or below it.

If You Lose the Device

The shared secret lives only on your device, so losing it means losing access—unless you saved recovery codes when setting up 2FA. Store them offline, such as printed in a safe place, not in a note synced to the same account an attacker might target. Each one typically works once, so cross them off after use.

Putting It Together: The Full Flow

Everything reduces to one pipeline: shared secret → time counter → HMAC → truncation → 6 digits.

One 30-second cycle, start to finish:

  • During setup, you scan a QR code. Your app stores the shared secret. The QR is really just a text string containing the secret plus account details, so anyone who photographs it owns your codes.
  • At 12:04:30, the app computes the current time step (roughly 24,000 if counting from the Unix epoch in 30-second chunks). Seconds 12:04:30 through 12:04:59 all produce the same counter value—that's the grace window.
  • HMAC-SHA1 mixes the secret with that counter, producing 20 bytes.
  • Truncation picks 4 of those bytes, converts them to a number, and keeps the last 6 digits: say, 482913.
  • You type it. The server, holding the same secret, runs the identical calculation—allowing a step or two of drift for clock differences—and the codes match. Drift tolerance means a code typed at 12:05:05 may still be accepted if your clock lags slightly.

Common failure points are mundane, not mathematical: a phone restored from backup without the secret, a manual clock set a minute off, or a stale code typed after the step rolled over. If codes fail consistently, re-scan the QR rather than troubleshooting the math.

Two habits protect the whole chain: treat the QR code like a password, and keep your device clock set to automatic. Get those right, and the math takes care of itself.

Share this guide