TOTP and HOTP, Explained
After reading this you will know how a 6-digit authenticator code is computed from a shared secret, how to reproduce one by hand, and which mistakes silently break two-factor login.
A TOTP code is not random. It is a deterministic function of two inputs: a secret both sides know, and the current 30-second time step. Given the same secret and the same time, your phone and the server compute the same 6 digits independently, with no message passing between them. That is the whole trick.
Here is the hook. The secret JBSWY3DPEHPK3PXP is Base32 for the ASCII bytes Hello!\xDE\xAD\xBE\xEF. At Unix time 59 seconds, the standard RFC 6238 test vector produces the code 287082. Change the time to the next 30-second window and the code changes completely. Nothing about 287082 tells you the next code unless you hold the secret.
What HOTP and TOTP actually are
HOTP (RFC 4226) is the base construction: an HMAC-based one-time password driven by a counter. Each time you use a code, both sides increment the counter, so the same secret yields a different code each step. TOTP (RFC 6238) is HOTP with one substitution: instead of a manually incremented counter, it uses the current time divided into fixed windows.
- Shared secret
- A random key, usually 16 to 32 bytes, encoded in Base32 for display. Both the authenticator and the server store it.
- Counter
- For HOTP, an integer that increments on each use. For TOTP, the number of time steps since the Unix epoch.
- Time step
- The window length, almost always
30seconds, written T_0 = 0, X = 30. - Digits
- How many decimal digits to keep, usually
6, sometimes7or8.
When to use it, and when not
Use TOTP as a second factor: a code the user types in addition to a password. It defends against a leaked or guessed password, because the attacker also needs the current code, which needs the secret. It runs offline, which is why it survives on a phone in airplane mode.
Do not use TOTP as a single factor. A 6-digit code has one million possible values, and if you allow unlimited guesses an attacker can brute force one window. Rate limiting matters: 10 attempts per code window keeps the success probability at 10 / 10^6 = 10^{-5} per window. Do not use TOTP where you cannot protect the secret at rest, and do not treat SMS codes as equivalent, since those travel over a channel an attacker can intercept or redirect.
The secret is the entire security boundary. Anyone who reads it can generate valid codes forever, on any device, without touching your account until the moment they log in. Treat the Base32 string and the QR image with the same care as a password.
The formula and the intuition behind it
Start with the counter. For TOTP, the counter C at Unix time t is the number of whole time steps elapsed:
Here t is the current Unix time in seconds, T_0 is the epoch offset (0), and X is the step length (30). At t = 59 this gives \lfloor 59 / 30 \rfloor = 1.
Next, HMAC the 8-byte big-endian counter under the secret K:
H is a 20-byte digest. It looks random and cannot be reversed to recover K, which is why holding one code does not leak the secret. The last step, dynamic truncation, squeezes those 20 bytes down to a number.
Here d is the digit count (6). The truncation reads the low 4 bits of the last byte of H as an offset o between 0 and 15, takes 4 bytes starting at o, masks off the top bit to keep the result positive, then reduces modulo 10^6. The offset step is what makes truncation depend on the data itself rather than always reading the same position.
A worked example you can reproduce
Reproducing the RFC 6238 test vector at t = 59
The tool defaults use secret JBSWY3DPEHPK3PXP, SHA1, 6 digits, and a 30-second step. Pin the time to 59 seconds and follow along.
- Decode Base32.
JBSWY3DPEHPK3PXPbecomes the 10 bytes48 65 6C 6C 6F 21 DE AD BE EF, which isHello!followed byDE AD BE EF. - Compute the counter. C = \lfloor 59 / 30 \rfloor = 1, so the 8-byte block is
00 00 00 00 00 00 00 01. - HMAC-SHA1 the block under the key. The 20-byte digest ends with a byte whose low nibble is the offset.
- Truncate. Read the offset, take 4 bytes, mask the high bit, and reduce modulo
1000000. - The result is
287082. That matches the published RFC 6238 SHA1 vector for t = 59.
The same secret at later windows gives 359152 at t = 1111111109 and 969429 at t = 1111111111. If your tool reproduces these three numbers, its math is correct.
Reading the live code and its countdown
The tool shows the current code and a countdown. The countdown is 30 - (t \bmod 30) seconds. At t = 59 that is 30 - 29 = 1 second left, so the code 287082 is about to roll over to the code for counter 2.
| Unix time t | Counter floor(t/30) | Seconds left | Window |
|---|---|---|---|
| 0 | 0 | 30 | 0 to 29 |
| 29 | 0 | 1 | 0 to 29 |
| 30 | 1 | 30 | 30 to 59 |
| 59 | 1 | 1 | 30 to 59 |
| 60 | 2 | 30 | 60 to 89 |
Servers usually accept a small window of adjacent counters to absorb clock skew. A common setting accepts the previous and next step, so three windows total. That widens the brute-force target by a factor of 3, which is why the accepted window is kept small.
The otpauth URI and QR strings
Authenticator apps import a secret through an otpauth:// URI, which is what a QR code encodes. The shape is:
The secret parameter is the Base32 string, with no padding characters. The period is the step in seconds, digits is the code length, and algorithm can be SHA1, SHA256, or SHA512. Most apps default to SHA1 and 6 digits and quietly ignore other choices, so if you set algorithm=SHA256 and the app assumes SHA1, the codes will never match. Test the imported code against the server before you rely on it.
For HOTP, replace totp with hotp and add counter=0. HOTP codes advance only when used, so if the client and server counters drift apart (one increments, the other does not) the codes stop matching and the account needs a resync.
Common mistakes
The failure modes are almost always mechanical, not cryptographic.
- Wrong Base32. The secret is Base32 (RFC 4648, alphabet
A-Zand2-7), not Base64 and not hex. Feeding a hex string as if it were Base32 decodes to the wrong bytes and every code is wrong. - Clock skew. TOTP trusts the device clock. If a server is 40 seconds fast, it computes the next window's code and rejects the correct one. Keep both clocks synced to NTP.
- Algorithm mismatch. SHA1 remains the default for TOTP despite being weak for collision resistance, because HMAC-SHA1 has no practical break and interoperability matters. Do not switch to SHA256 unless both ends agree.
- Reusing a code. Codes are one-time within their window. A server that accepts the same code twice loses the replay protection that the counter provides.
Related tools
Once a user proves possession of a factor, a server often issues a signed session token. If you are building the token side of that flow, the JWT Builder & Signer creates and signs JSON Web Tokens with HS256, HS384, and HS512, which pairs naturally with a TOTP login step.
Frequently asked questions
Why do Google Authenticator and Authy show the same code as this tool?
Because all of them implement RFC 6238 with the same defaults: SHA1, 6 digits, and a 30-second step. Given the identical secret and a synced clock, the computation is deterministic, so every correct implementation produces the identical 6 digits.
Is a 6-digit code secure enough?
Only with rate limiting. There are 10^6 possible codes. Without limits an attacker averages 500000 guesses to hit one window, which is fast. With a cap of 5 attempts per window the per-window success chance is 5 \times 10^{-6}, and the second factor holds.
What happens if my phone clock is wrong?
TOTP uses the local clock to compute the counter. If your clock is off by more than the server's accepted window (often 30 to 60 seconds) your codes will be rejected. Enabling automatic network time fixes almost every "my codes stopped working" report.
Can someone recover my secret from a code they see?
No. The code is \text{HMAC-SHA1}(K, C) \bmod 10^6, and HMAC is a one-way function of the key. Seeing 287082 reveals nothing usable about K. The risk is exposure of the secret itself, not of any single code.
What is the difference between TOTP and HOTP in practice?
TOTP advances on the clock, so codes rotate every 30 seconds whether or not you log in. HOTP advances on a counter that moves only when a code is generated, which is useful for offline hardware tokens but requires resync if the client and server counters drift apart.