// resources · research

Can Two 30-Second Codes Replace Your Password?

A feasibility study of passwordless login built entirely from two independent 8-digit, 30-second TOTP layers — the brute-force math, the restart rule that holds it together, and where it honestly breaks.

Why passwords are the attack surface

Strip away every exotic technique in the breach reports and the pattern underneath is boring: an attacker logged in with a credential they shouldn't have had. Users choose weak passwords, re-use them across systems, and fall for phishing pages that harvest them. When your authentication starts with a long-lived secret a human typed into a browser, every login you own starts with that same weakness. Microsoft's analysis of Azure AD account compromises is the number usually quoted: multi-factor authentication would have stopped 99.9% of them.

That observation suggests a radical simplification: if the password contributes so little, why keep it at all? Replace it entirely with the factor that actually defends the account. The scheme examined here does exactly that — a login built from two independent layers of 8-digit, 30-second time-based one-time passwords, with nothing else. The question is whether it is feasible: does it genuinely defeat brute forcing, and at what cost?

The scheme — two layers, one window

The building block is standard TOTP as defined in RFC 6238: a shared secret combined with the current time produces a numeric code. The 30-second time step is the RFC's own recommended default, and the standard explicitly supports 8-digit codes — its reference test vectors are 8 digits. The scheme enrolls two independent seeds per account (enrollment should place them on separate devices where possible, for reasons that matter later) and requires both at every login:

  • Layer one. The user submits a username, then token A — 8 digits from seed one.
  • Layer two. Token B — 8 digits from seed two, verified independently, against the same 30-second window.
  • Grant. Only when both layers verify does a session start.

Nothing about either layer is exotic. The security of the design comes from two properties: the short validity window, and the interaction rule between the layers. That rule is where most implementations would get it wrong.

The rule that holds it together: fail anywhere, start over

The interaction rule is simple to state: if any layer fails, the entire attempt dies and the user starts over from the username with two fresh tokens. A stale or previously seen code is never carried forward. This is not pedantry — it is the load-bearing wall. Consider what happens without it. An attacker who submits a correct layer-one code and then fails layer two would be allowed to stay "in" and keep guessing layer two for the remaining lifetime of their layer-one submission. The moment an implementation permits that, the second layer stops adding anything: online security collapses from the square of the code space back to a single 8-digit code.

The restart rule also removes two quieter leaks. It prevents an oracle that tells the attacker "layer one passed, layer two failed" — a message that confirms the username exists and that a live seed produced a correct code, halving the attacker's work and enabling account enumeration. And it forces single-use: because both codes must be correct within the same 30-second window, any captured token is worthless once the window closes. RFC 6238 requires verifiers to reject reuse of a code after a successful validation; this scheme rejects reuse after any attempt, successful or not.

verify-flow
# login flow — double-TOTP with full-restart rule

1. IDENTIFY    accept username   -> open attempt, no oracle about existence
2. LAYER ONE   verify token_a    (8 digits, 30 s step, ±1 step max)
               fail -> burn the attempt, restart at 1
3. LAYER TWO   verify token_b    (independent seed, same window)
               fail -> burn the attempt, restart at 1
4. SINGLE USE  consume both codes on every attempt — replay = restart
5. THROTTLE    per-account AND per-source caps, cool-down on abuse
6. GRANT       both layers passed -> issue session, notify user

The brute-force math

An 8-digit code has 108 — one hundred million — possible values per window. An online attacker gets to make a bounded number of guesses per window, because NIST SP 800-63B requires verifiers to limit consecutive failed attempts on a single account (its stated cap is 100). The expected time to break a single layer is the code space divided by the attempts allowed, multiplied by 30 seconds per window. For 6-digit codes the same math starts at one million values — which is why the 8-digit choice matters, and why rate limiting is mandatory rather than optional for anything with fewer than 64 bits of entropy.

Single layer, 8 digits, attempts allowed per 30-second window:

Attempts / windowOdds per windowExpected time
100 (NIST cap)1×10-6~1 year
101×10-7~9.5 years
3 (strict)3×10-8~32 years

Now the two-layer scheme with the restart rule. Both codes must be correct inside the same window, so the attacker's odds per full restart cycle are the product of the two layers — the code space effectively squares. Each failed cycle burns another 30 seconds, and the attacker is back at layer one with two guesses they can't carry forward:

Attempts / layer / cycleOdds per cycleExpected time
100 each1×10-12~950,000 years
3 each9×10-16~1 billion years

The honest footnote: accepting a one-window clock drift for resynchronization (the RFC recommends allowing at most one backward step) puts three codes in play at once, multiplying the attacker's odds by three. At 100 attempts per layer that turns 950,000 years into roughly 316,000. Online brute forcing is dead either way — which is the point, and it is also why the restart rule matters more than any digit count. Squaring only happens while both layers are live in the same window.

How it breaks — a threat assessment

The login flow survives online guessing overwhelmingly. A feasibility verdict, though, has to include the ways the scheme degrades in practice. Five gaps, in order of severity:

  • Two seeds can fall in one shot. Both factors are something you have — and OWASP and NIST are blunt that repeated instances of the same factor do not constitute true MFA. If both seeds live in the same authenticator app, one compromise of that app defeats both layers at once. Enrollment on separate devices (phone plus hardware token) restores meaningful independence and should be the default, not an option.
  • Recovery becomes the weakest link. Removing the password does not remove account recovery — it moves the whole "reset your password" problem to "re-enroll your seeds." Email-based seed re-issuance is the new password reset, and an attacker doesn't need to touch the beautiful login flow to use it. Recovery codes must be stored hashed, single-use, and generated at enrollment; identity-verified re-enrollment belongs to the same rigor as the login itself.
  • Phishing survives. TOTP codes are phishable: a real-time man-in-the-middle toolkit can relay a victim's live codes through the attacker within the same window. The 8-digit, two-layer design raises the bar — the attacker must relay both codes inside 30 seconds and beat single-use consumption — but passkeys and FIDO2/WebAuthn are the only common factors that are phishing-resistant by construction, not by friction.
  • Eight digits are not universally rendered. Most mainstream authenticator apps display 6 digits only — Google Authenticator included. Deployable 8-digit-capable options exist (Aegis, 2FAS, FreeOTP+, Bitwarden, OATH hardware tokens), but "bring your own authenticator" is a real constraint on the rollout plan, not a footnote.
  • Friction and lockout abuse. Sixteen digits typed per login is heavy, and frequent logins will make users resent it — long sessions and risk-based re-authentication are part of the design, not luxuries. Meanwhile the rate limiting that defeats brute forcing can be weaponized: attackers spamming failures can cool down or lock out legitimate users, so abuse caps need per-source limits alongside per-account ones.

Verdict-relevant detail on the first gap: NIST's assurance levels would treat this as a possession-based scheme, and the weakest form of it when both seeds sit on one unlocked device. The scheme's brute-force story is airtight; its assurance story depends entirely on how seeds are provisioned.

Implementation checklist

For implementers attempting this in anger, these are the requirements that separate the design from a vulnerable deployment:

  • Throttle per account — no more than 100 consecutive failures (NIST cap); 3–5 per window is a saner operating point. Add per-source caps for distributed guessing, and a cool-down that tells users how long to wait, per NIST's usability guidance.
  • Consume codes on every attempt — successful or failed — and reject any previously seen code. This is the restart rule expressed in state, and it is what makes captured codes worthless.
  • Uniform failure messages — every failure restarts at the username with the same generic error. No oracle, no enumeration.
  • Protect the seeds like keys — RFC 6238 recommends decrypting seed material only at verification time and re-encrypting immediately; HSM or hardware-backed storage where the deployment can afford it. Never log token values.
  • Bound the drift window — at most one time step backward, and record detected drift per seed rather than widening the window permanently.
  • Hash at rest — hashing OTPs does not defeat a database thief (a 27-bit code space falls instantly offline), but it limits blast radius of a briefly exposed live window and is basic hygiene per OWASP's OTP guidance.
  • Monitor and notify — alert users on failed cycles with time, source, and browser; every failure the user doesn't recognize is reconnaissance against them.

Verdict

Feasible — against the stated threat, unambiguously. Online brute forcing of this login flow is not merely impractical; at strict throttling it is measured in geological time, and the restart rule is what makes the second layer worth having. As a password replacement, the honest framing is narrower: it eliminates the long-lived typed secret that fuels credential stuffing, spraying, and reuse — but it is possession-only authentication, its seeds can fall to a single device compromise unless deliberately separated, its recovery path inherits every weakness the password reset used to have, and its codes remain relayable by real-time phishing. The brute-force math is airtight; the assurance story is a provisioning problem.

The pragmatic reading for an organization considering it: double-TOTP is a strong, deployable step toward passwordless login for estates that cannot roll FIDO2 everywhere — with passkeys as the destination, not the rival. And whichever factor combination an estate deploys, the architecture lesson generalizes: bounded attempts, single-use consumption, and a flow that restarts from zero on any failure are what turn "two codes" into "two layers."

Want your authentication stress-tested? Download PDF ← Back to resources