TOTP Generator / Debugger
Paste a Base32 secret and see the live RFC 6238 code, the drift window and every step of the math — on this device only.
••••••| Step | Counter | Code | Valid (UTC) |
|---|---|---|---|
| −2 | — | — | — / — |
| −1 | — | — | — / — |
| 0 (now) current step | — | — | — / — |
| +1 | — | — | — / — |
| +2 | — | — | — / — |
Working
- Secret decodes to — bytes
- Counter (decimal)
- —
- Counter (8 bytes, big-endian hex)
- —
- HMAC-SHA-1 of the counter
- —
- Truncation offset: byte —, the low nibble of the last MAC byte
- Four selected bytes, high bit masked
- —
- Modulo — gives the code
- —
- This step runs — → —
Press ? for shortcuts · The secret stays in this tab — nothing is stored on this device and nothing is sent.
This page loads no third-party code — no ads, no trackers, no analytics. Verify in your Network tab.
Do it offline:$ oathtool --totp -b JBSWY3DPEHPK3PXP
How to use it
- Paste the Base32 secret (the setup key, typically 16 to 32 characters) or paste a whole otpauth:// URI copied from a QR code — the fields fill themselves and say which ones they changed.
- Match the server: digits 6, 7 or 8, step length in seconds, SHA-1 / SHA-256 / SHA-512, and T0 if the deployment moved it off the Unix epoch.
- Read the code and the ring: the ring counts the seconds left in the current step, and the code rolls over on the step boundary, not on a timer of its own.
- When a code is rejected, read the drift table: offsets −2 to +2 with the counter, the code and the wall-clock span each one covers.
- Copy with ⌘/Ctrl + C, or download the drift table as text. Share links carry the settings only — digits, period, algorithm, T0 — never the secret.
Paste the Base32 shared secret your authenticator app stores and this page computes the RFC 6238 code for the current 30-second step, the countdown to the next one, and the codes for two steps either side — the answer when a server says a valid-looking code is wrong. Every intermediate value is shown, and the secret never leaves this tab — no upload, no browser storage, no account.
How it works
Time becomes a counter. The engine takes the current time in seconds, subtracts T0 (0 by default, the Unix epoch RFC 6238 §4.1 specifies) and floor-divides by the step length: counter = floor((now − T0) / period). Floor, not truncate — a timestamp before T0 gives a correctly negative counter, and the counter is BigInt arithmetic throughout because the RFC's own T = 20000000000 test vector needs more than 32 bits. The counter is then written big-endian into exactly 8 bytes, which is the 16-hex-character value the drift table prints.
Those 8 bytes are the message and the decoded secret is the key for one keyed-MAC call — SHA-1 unless you change it, computed here by the same engine that powers the HMAC tool, so the two pages can never disagree. RFC 4226 calls this a one-way step for good reason: the MAC is the whole security of the scheme, which is why the code changes completely between adjacent steps instead of drifting by a digit.
The MAC is a 20-, 32- or 64-byte value and a code is 6 to 8 digits, so RFC 4226 §5.4 picks four bytes by dynamic truncation: read the low nibble of the LAST MAC byte to get an offset (0–15), take the four bytes starting there, and clear the top bit of the first one. Masking that bit is what keeps the value a positive 31-bit integer on platforms with signed 32-bit types — implementations that skip it produce a negative number and a wrong code on roughly half of all counters.
The last step is arithmetic: code = binary % 10^digits, zero-padded to the chosen width. Padding matters — RFC 6238 Appendix B's own vector at T = 1111111109 is 07081804, and a client that renders it as 7081804 will be rejected forever. A server does the same computation and then repeats it for a few neighbouring counters, because the two clocks are never identical; RFC 6238 §5.2 recommends at most one step of tolerance, real deployments usually accept ±1 or ±2. That acceptance window is what the drift table on this page reproduces, which is why it answers "why was my code rejected" directly: if the code the app showed appears at offset −3, the clock is the bug.
Examples
T = 59 · SHA-1 · 8 digits · GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ94287082 · counter 1 (0000000000000001) · offset 11 · 41397eea → 1094287082The first RFC 6238 Appendix B row, and the fastest way to check any implementation. The secret is the ASCII string 12345678901234567890 in Base32; 59 seconds into 1970 is counter 1. Offset 11 means the last MAC byte ended in b, so bytes 11 to 14 were taken.
T = 1111111109 · SHA-1 · 8 digits · leading-zero code07081804 · counter 37037036 (00000000023523ec) · offset 4 · 3610f84c → 907081804907081804 mod 10^8 keeps a leading zero, and the RFC prints it. Two seconds later the same secret gives 14050471, which is offset +1 in this row's drift table — Appendix B's next vector is this row's neighbour, so the table doubles as a conformance check.
T = 1111111111 · SHA-256 · the 32-byte seed67062674 · counter 37037037 (00000000023523ed) · offset 13 · 458ff692 → 1167062674The Appendix B table uses a DIFFERENT seed per algorithm — 20 ASCII bytes for SHA-1, 32 for SHA-256, 64 for SHA-512 — and the document states it in prose rather than in the table, so a single-seed reading is the classic reason a new implementation matches the SHA-1 rows and fails every other one.
T = 20000000000 · SHA-512 · counter past 2³²47863826 · counter 666666666 (0000000027bc86aa) · offset 6 · 3e752212 → 1047863826The RFC's deliberate stress row, dated year 2603. The step counter 666666666 still fits in 32 bits, but the seconds value does not, and the 8-byte counter field is 64 bits wide by specification — which is why this engine keeps the counter in BigInt and never in a 32-bit int. Offset 6 lands mid-MAC in a 64-byte value, so the 0–15 offset never reaches past the end.
jbsw y3dp ehpk 3pxp · as an app displays it358462 · counter 37037037 (00000000023523ed) · offset 3 · 2573fd3e → 628358462Setup keys are shown lowercase and grouped in fours for typing, so both are accepted and each one is reported as a chip rather than silently absorbed. This secret decodes to 10 bytes, under the RFC 4226 minimum, and the page says so — a real enrolment may be refused by the server even though codes compute here.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
A hyphen is not whitespace
Spaces and line breaks are ignored, but JBSW-Y3DP-EHPK-3PXP fails at the first hyphen: the standard alphabet has no such character. The decoder additionally probes sibling alphabets and may suggest Crockford, which does ignore hyphens — for an authenticator secret that suggestion is a false lead, because setup keys are standard RFC 4648 Base32. Delete the hyphens instead.
Trailing bits that are quietly discarded
An 18-character secret carries 90 bits, which is 11 bytes plus 2 left over. Those bits are dropped, and if they were not zero the page names the character and its canonical spelling — one keystroke too many while typing a setup key produces a secret that decodes cleanly and computes codes no server will ever accept.
A secret in hex is not a secret in Base32
Servers often print the shared secret as hex. Pasted here, a hex string usually fails on its first 0, 1, 8 or 9 — those digits are outside the alphabet. Convert the hex to Base32 first; a 40-hex string is 20 bytes and should become a 32-character secret.
Algorithm and digits are usually ignored by apps
The otpauth URI can carry algorithm=SHA256 and digits=8, and most authenticator apps read neither: they compute SHA-1 and six digits regardless. Change these only when the server documents them, and expect the app on the phone to disagree with both of you.
T0 is a whole-system agreement
Moving T0 off zero shifts every counter. The engine handles a negative counter correctly — before T0 the floor division goes below zero and the 8-byte field is written in two's complement, exactly as the reference implementation in RFC 6238 Appendix A does with a signed 64-bit value — but the server has to use the same T0 or no code will ever match.
One step of 1 second is legal and useless
The period accepts 1 to 3600 seconds because test rigs use short steps and a few systems use 60. At one second the drift table spans five seconds and the code is stale before it is read; that is not a bug on this page, it is what the setting means.
Nothing here is remembered
This tool writes no browser storage key at all — not an empty one, not a skipped one — so a reload starts from the defaults with an empty secret field. The permalink carries digits, period, algorithm and T0 and stops there, because a shared secret in a URL would outlive the tab in history, in logs and in whatever chat window it was pasted into.
Common errors
No secret to work with yet — TOTP needs the Base32 shared secret your authenticator app stores, such as JBSWY3DPEHPK3PXP. Paste the secret or an otpauth:// URI.- The secret field is empty, so there is no key to compute with; the settings alone produce nothing.
Invalid Base32: character '1' at position 5 — this input decodes as Crockford Base32; switch the variant to Crockford Base32.- A digit that does not exist in the standard alphabet, usually a 1 typed for I, a 0 for O or an 8 for B. The position is the exact character to fix; the variant suggestion comes from the shared Base32 decoder and does not apply to authenticator secrets.
Base32 length is invalid — a final group of 1 character can never occur (valid remainders are 2, 4, 5 and 7)- One character was lost or gained while copying. Base32 arrives in groups of eight, and a lone final character cannot encode any whole number of bytes, so the secret is provably incomplete.
Digits is 9, which RFC 4226 §5.3 does not define — choose 6, 7 or 8 (6 is the default and what almost every server expects).- Hand-edited share links are validated exactly like typed settings; the specification stops at eight digits.
Period is 0, which is not a whole number of seconds between 1 and 3600 — set it to 30 (the RFC 6238 default) unless your server documents another step.- A zero or fractional step has no counter, so the computation is refused rather than guessed at.
This URI is otpauth://hotp/, and this page only computes time-based (totp) codes — the counter-based HOTP row is a separate tool. You can still paste the secret on its own here.- HOTP advances a counter per use instead of per second, so a time-based page cannot answer for it — but the secret inside that URI works here on its own.
The otpauth:// URI has no secret parameter, which is the one part TOTP cannot work without — re-copy the URI from the QR code, or paste the Base32 secret on its own.- Issuer, label, digits and period are all optional; the secret is not. A URI without it is usually a truncated copy.
FAQ
Why was my code rejected when the app showed it?
Almost always a clock. Compare the code you sent with the drift table: at offset −1 or +1 the device is up to a step out and most servers still accept it, at −3 or worse nothing will. If the code appears nowhere in the table, the mismatch is a setting instead — digits, step length, algorithm, T0 — or the secret was mistyped.
How wide is a server's acceptance window?
RFC 6238 §5.2 recommends at most one step, so 30 seconds either side, and warns that each extra step widens the guessing surface. Deployments commonly allow one or two, and some track a device's measured offset instead. The table here shows two either side because that covers what nearly every implementation accepts.
Can I use SHA-256 or 8-digit codes with my authenticator app?
Only if you control both ends. The Key URI format carries algorithm and digits parameters, but the widely used apps ignore them and always compute SHA-1 with six digits, so a server configured for anything else will reject a phone that looks correctly set up. Verify with a code from the real app before rolling it out.
Is it safe to paste a TOTP secret into a web page?
Check instead of trusting: open the Network tab and compute — no request leaves, because this page loads nothing from a third party and holds no analytics on the secret field. Nothing is written to browser storage and the share link carries settings only. Even so, a live production secret belongs in the CLI line below; use this page for the enrolment you are debugging.
Why does the same secret give a different code in two tools?
Compare the working, not the codes. Four things drift silently: the step length, the digit count, the algorithm and T0. The decoded byte count is the fifth and most common — if one tool reads the secret as Base32 and another as raw text or hex, the keys differ and every code differs with them.
What does an otpauth:// URI actually contain?
The QR code your app scans is a URI: a type, a label such as ACME Co:alice@example.com, and query parameters for secret, issuer, algorithm, digits and period. Pasting one here fills the fields and lists what it changed, including any parameter this page does not use. It is an input convenience, not a full URI reference.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Recompute immediately instead of waiting for the next second.
- ⌘/Ctrl + S
- Download the drift table as a .txt file — the shortcut downloads here rather than saving the page.
- ⌘/Ctrl + C
- Copy the current code when nothing is selected; a selection copies itself.
- ⌘/Ctrl + Shift + C
- Copy the current code even while a selection is active.
- ? or ⌘/Ctrl + /
- Open the shortcut help dialog.