fix(auth): close username-enumeration timing side-channel in login

fix(auth): close username-enumeration timing side-channel in login

#6 in tonybierman/arium — merged 2026-05-25

The vulnerability

verify_password_user only ran the (deliberately expensive) Argon2 verify when the email matched an existing account. For a nonexistent email it returned Invalid immediately after the DB lookup; for an unparseable stored hash it bailed the same way. The error message was already identical for both — but the response time was not.

Measured against the dioxus example, a wrong-password attempt on a real account took ~0.57s (Argon2 runs) while a login for a nonexistent email returned in ~0.02s — a ~27× gap. An attacker can enumerate which emails have accounts purely by timing, defeating the indistinguishability VerifyOutcome::Invalid is documented to promise.

The fix

Burn an equivalent Argon2 verify against a fixed dummy hash on both early-return branches (no-such-account, unparseable-hash), so every path costs roughly the same regardless of whether the account exists. The dummy hash is built once via LazyLock with the default Argon2 params, matching a real verify's cost.

Test

Adds unknown_email_is_not_faster_than_wrong_password (next to the existing message-equality test, whose comment had warned about exactly this oracle). It asserts the not-found path spends at least half the time the wrong-password path does — pre-fix it was ~4%, so the test fails decisively without the fix.

Verification

End-to-end against a rebuilt server, the enumeration-timing ratio dropped from 27.28× → 1.01×. Unit tests pass stably (3× runs), cargo fmt --check and clippy clean.

🤖 Generated with Claude Code

Last updated 2026-05-26