How TOTP Codes Work: The Math Behind Your 6-Digit 2FA Code
How TOTP Codes Work: The Math Behind Your 6-Digit 2FA Code
You open your authenticator app, a six-digit number appears, and a little timer counts down. Thirty seconds later the number changes. Your phone did not ask a server for it, and it may not even have a signal. Yet the website you are logging into somehow agrees on exactly the same six digits.
That is not magic. It is an open standard called TOTP, short for Time-based One-Time Password. Once you see how it works, the odd behaviour of 2FA codes (why they expire, why a wrong clock breaks them) makes sense.
Two standards: HOTP and TOTP
TOTP is defined in RFC 6238, published by the IETF in 2011. It is built on top of an older standard, RFC 4226, which defines HOTP, the HMAC-based One-Time Password algorithm.
The relationship between them is simple:
- HOTP turns a secret key plus a counter into a short numeric code. Every time you need a new code, the counter goes up by one.
- TOTP is HOTP where the counter is not something you press a button to increase. Instead, the counter is derived from the current time.
So to understand TOTP, you really only need to understand two things: how the time becomes a counter, and how HOTP turns that counter into six digits.
The ingredient everything depends on: the shared secret
When you turn on two-factor authentication for an account, the website generates a random secret key. It stores one copy on its server and shows you the other copy, usually as a QR code, with a text version underneath for manual entry.
That text version looks something like JBSWY3DPEHPK3PXP. It is written in Base32, an encoding defined in RFC 4648 that uses only the letters A to Z and the digits 2 to 7. Base32 is not encryption. It is just a way of writing raw binary data with characters that are easy to read aloud and type, avoiding look-alikes such as 0 and O or 1 and I.
Each Base32 character carries 5 bits. A 16-character key therefore holds 80 bits of randomness, and a 32-character key holds 160 bits. RFC 4226 requires a secret of at least 128 bits and recommends 160 bits, although in practice you will see a range of lengths in the wild.
After the QR code is scanned, the website and your app hold the same secret, and they never need to talk to each other again to produce matching codes.
Step 1: turning the time into a counter
Computers commonly measure time as Unix time: the number of seconds since 1 January 1970 (UTC). Your phone knows this number, and so does the server.
TOTP divides that number into fixed windows called time steps. The standard default, and what nearly every consumer service uses, is 30 seconds. The counter is calculated like this:
T = floor((current Unix time - T0) / X)
Here T0 is the starting point (normally 0) and X is the step length (normally 30). In plain words: count how many complete 30-second blocks have passed since 1970, and use that whole number as the counter.
Because every device on Earth that knows the correct time will compute the same T during the same 30-second window, this is how your phone and the server stay in step without communicating. It is also why the countdown in your app resets on the minute and the half-minute: those are the boundaries of the time steps.
Step 2: mixing the secret and the counter with HMAC
Next, the counter is written as an 8-byte big-endian number and fed, together with the secret key, into HMAC (Hash-based Message Authentication Code). By default the underlying hash is SHA-1.
HMAC-SHA1 produces a 20-byte (160-bit) output. You can think of it as a fingerprint of "this secret combined with this counter". Two properties matter here:
- Change even one bit of the counter and the output looks completely different.
- Seeing the output does not let anyone work backwards to the secret.
You may have read that SHA-1 is broken. That refers to collision attacks on SHA-1 as a plain hash; HMAC-SHA1 relies on different properties and is still regarded as acceptable here. RFC 6238 also allows SHA-256 and SHA-512, but SHA-1 remains the most widely supported default.
Step 3: dynamic truncation, from 20 bytes to 6 digits
Twenty bytes is far too long to type. HOTP shrinks it using a method called dynamic truncation:
- Look at the last byte of the HMAC output and take its lowest 4 bits. That gives a number between 0 and 15. Call it the offset.
- Starting at that offset, read 4 consecutive bytes from the HMAC output.
- Clear the very top bit of those 4 bytes, so the result is always treated as a positive 31-bit integer. This avoids differences in how programming languages handle signed numbers.
- Take that integer modulo 1,000,000 (10 to the power of 6). The remainder is your code. If it has fewer than six digits, pad it with leading zeros.
The offset is "dynamic" because it depends on the HMAC output itself, so which 4 bytes get picked changes from code to code. Padding is why codes like 004521 are perfectly valid.
For an 8-digit code, the last step uses modulo 100,000,000 instead. Everything else is identical.
A worked example, step by step
The RFCs include official test values, which makes for a reliable example. RFC 6238 uses the ASCII text 12345678901234567890 as its SHA-1 test secret. Written in Base32, the form you would type into an app, that secret is GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ.
- Pick a time. The RFC's first test case uses Unix time 59 seconds, a moment in the first minute of 1970.
- Compute the counter. 59 divided by 30 is 1.97, which rounds down to
T = 1. - Run HMAC-SHA1 over the 8-byte counter value 1, using the secret.
- Truncate dynamically as described above, then reduce the result to the required number of digits.
RFC 6238 lists the 8-digit result for this case as 94287082. Taking the same truncated integer modulo 1,000,000 instead gives the 6-digit code 287082, which is exactly the HOTP value RFC 4226 lists for counter 1 with the same secret. That match is a nice demonstration that TOTP really is just HOTP with a time-based counter.
Why codes work offline
Look at the ingredients again: a secret that is already stored on your phone, and the current time from your phone's clock. Nothing in that list requires a network connection.
That is why an authenticator app keeps producing correct codes in airplane mode or abroad without roaming. Unlike SMS codes, nothing is sent to you, so there is no message in transit to intercept. The server simply does the same calculation when you submit a code.
Why the clock matters so much
Since time is one of the two inputs, a device with the wrong time computes the wrong counter, and therefore a wrong code, even though the secret is perfect.
To be forgiving about small differences and network delays, servers commonly accept codes from a small window around the current step, often one step before and sometimes one step after. RFC 6238 recommends allowing at most one previous step for transmission delay, but leaves the exact policy to the service. In practice this means that a clock a few seconds off is fine, while a clock off by a minute or more will usually fail.
The fix is almost always to let your device set its time automatically from the network rather than setting it by hand. Time zones, on the other hand, do not matter: Unix time is based on UTC, so a phone that shows the correct local time in the correct time zone already has the right Unix time.
What TOTP protects against, and what it does not
TOTP is a big step up from a password alone. A stolen password is not enough, because the attacker also needs a code that changes every 30 seconds and can only be computed with the secret.
It is not invincible, though. A fake login page can ask for your password and current code, then use both on the real site before the code expires. And anyone who obtains the secret key itself can generate valid codes forever, so treat the key like a password.
If you want to see the process in action, the 2FA-Auth code generator takes a Base32 secret and shows the current code with the time remaining in its window.
FAQ
Is a TOTP code random?
No. It looks random, but it is fully determined by the secret key and the current time step. Anyone with the same secret and the same time gets the same code, which is exactly what allows the server to verify it.
Why are most codes 6 digits and 30 seconds?
Those are the defaults in the standards and the most widely supported settings across authenticator apps. The algorithm also supports 8 digits, other step lengths and SHA-256 or SHA-512, but not every app handles non-default values.
Can someone guess my code?
A random guess has a one-in-a-million chance for a 6-digit code. Services make guessing impractical by limiting how many attempts you get, and the code changes every 30 seconds anyway.
Do I need internet for my authenticator app to work?
No. The app only needs the stored secret and an accurate clock. You need internet to log in to the website, of course, but not to generate the code.
New posts
Most viewed
- How to Set Up Two-Factor Authentication on Instagram 24
- How to Turn On 2-Step Verification for Your Google Account 21
- Managing 2FA for Many Accounts: A System That Doesn't Break 20
- Can Two-Factor Authentication Be Bypassed? Real Threats and How to Defend 20
- Lost Your Phone? How to Recover Access to Your 2FA Accounts 19