HomeMethodThe window you cannot read
Why an expired 2FA code still works — and why no app can tell you for how long
The authenticator is the readable half: a 30-second period, six digits, SHA-1, floor(seconds / 30), HMAC, truncate, modulo. The project's own case study walks that path and stops at the other half — how many steps a verifier is willing to accept. That number is not in your app.
Scope. Run this against a verifier you operate, or one you have written permission to test: a self-hosted service, a dev instance, your own account inside a published bug-bounty scope. Do not probe someone else's login. Rate limiting and lockout are why windows are hard to map: you often get a handful of submissions before a delay starts, which is barely enough to test one step either side. Prefer code you can read.
What "valid for 30 seconds" does not mean
RFC 6238 (May 2011, Informational) defines the moving factor as a division, not a countdown.
T = floor((Current UNIX time − T0) / X) default X = 30 s, T0 = 0- A code can live about one second. The step number is what both sides compare, not the bar in the app. In the spec's example, at 59 seconds
T = 1and the 8-digit SHA-1 code is94287082; the 6-digit form is the tail,287082, because94287082 % 10^6is that tail. At 60 seconds,T = 2. - The clock that matters is the verifier's. A correct code on your device proves nothing about acceptance. Every window question is a clock question.
The three knobs, and who owns each
| Knob | Default / spec guidance | Where it lives | How you learn it |
|---|---|---|---|
| Time step X | 30 s. The spec recommends keeping this default; smaller steps raise the number of valid OTPs per period | Both sides — visible in the app | Static. The published APK trace shows setPeriod(30) |
| Digit count | RFC defines 6, 7 and 8. Verification systems are told to use at least 8 during generation and show 6–8 to users for high-value services | Both sides | Static. this(key, "SHA1", 6) in the same trace |
| Acceptance window | The spec recommends at most one time step as network delay. A larger window trades security for reliability | Verifier only | Behaviour only — see the table below |
The asymmetry. A verifier that tolerates two steps back also tolerates two steps forward unless it checks direction. "My old code still works" and "my future code already works" are often the same policy. Testing the forward half is what separates a measurement from a guess.
Resynchronisation
The spec's resynchronisation section describes a verifier that, having accepted a code from an off-by-N step, records the offset for that token and applies it later. Its worked example: with a 30-second step and tolerance of two backward steps, the maximum elapsed drift it can accept is about 89 seconds — 29 remaining in the calculated step plus 60 for the two extra — and reaching it costs three comparisons (current, minus one, minus two). It also recommends a limit on how far out of synch a user may be, in both directions.
| Behaviour you observe | Fixes the window as | Kills the rival hypothesis because |
|---|---|---|
| Backward accepted to −k, forward rejected | Window is one-sided (backward k) | A symmetric window would accept forward codes too |
| Backward −k and forward +k accepted | Symmetric ±k | Direction is not being checked |
| A shifted code is accepted after the window should have expired, but only following an off-window success | Drift is recorded per token | A fixed window cannot produce this, and re-enrolment should wipe the effect |
| Second use of the same accepted code rejected | Replay protection is live | This is the expected result |
| The same code accepted twice | A replay hole, not generosity | Replay inside one window is what verifiers are told to prevent |
Paste a base32 secret you enrolled yourself. Codes use the browser's own HMAC-SHA-1 via crypto.subtle. No key, code, or timestamp is sent anywhere.
current unix time: —
| Offset | Step T | Code | Meaning |
|---|---|---|---|
| Paste your own base32 secret to generate codes. | |||
// output appears hereHow to ask for the half you can read
Read the OTP defaults only, and stop before the UI. Report: time step,
digit count, hash algorithm, and the period's default value — each with
the class, method and source range. Do not explain two-factor auth, and
do not assume anything about the server. Then list every parameter you
found that the verifier could override at enrolment, because those are
the ones that cannot be measured from the app alone.The defaults are in the app. The enrolment payload — which may carry a different period or digit count per account — decides whether local codes match the server at all.
Findings and non-findings
| Claim | Evidence | Confidence | What it does not prove |
|---|---|---|---|
Step 30 s, six digits, SHA-1, floor(sec/period), dynamic truncation, modulo digit count, zero padding | Published case study on Aegis 3.4.3 (inspected with REA 6.1.0, 9 Oct 2026) | High | That APK version only, and only Aegis |
| The acceptance window cannot be recovered statically | The verifier runs server-side. No local file contains it | High | It does not say a window is hard to bound by observation |
| Window of at most one step is the spec's recommendation; about 89 s and 3 comparisons in the two-back example | RFC 6238 §§4.1, 5.2, 6 (May 2011) | High | What any given product actually implements |
| Most services accept one step back | Forum folklore, not measured by us | Not verified — excluded | — |
Next
How an authenticator generates a code
The generation side, published by the project itself.
rea.tools/showcase/aegis →Same shapeA write that no-ops
Another place where "the call succeeded" is not "it arrived".
Failure ladder →Play itDinosaur jump lab
A value you can pin down by measuring.
Open the lab →