Close the recovery lockout-DoS hole on /auth/recovery-complete
The original spec stored only `kek_salt`, `wrapped_dek_pw`+nonce,
`rec_salt`, and `wrapped_dek_rec`+nonce. Under that model, anyone who
knew a user's email could POST to /auth/recovery-complete with junk
material and overwrite the password-side wrap, locking the legitimate
user out. The data stayed safe (the attacker couldn't decrypt
anything) but the account was effectively DoS'd until the user dug up
their recovery code.
Fix: add a recovery-side verifier mirroring the password-side one.
Storage: two new columns on `users`:
- rec_auth_salt BLOB NOT NULL — independent of rec_salt
- rec_auth_verifier_hash TEXT NOT NULL — Bun.password.hash output
The migration adds them via ensureColumn() for forward-compat with
scaffold DBs that pre-date this commit; new tables get them via the
CREATE TABLE statement.
Wire protocol:
- SignupRequest gains rec_auth_salt + rec_auth_verifier
- RecoveryChallengeResponse gains rec_auth_salt
- RecoveryCompleteRequest gains rec_auth_verifier
Server (server/auth.ts):
- signup hashes the recovery verifier alongside the auth verifier
and stores both
- recovery-challenge returns rec_auth_salt so the client can derive
the verifier; refuses with 409 for pre-fix accounts that have a
NULL rec_auth_salt
- recovery-complete calls Bun.password.verify against the stored
hash BEFORE touching any state. Always runs verify even for
unknown emails (against a dummy hash) so timing doesn't leak
existence — same pattern we already used for /auth/login.
Client (frontend/src/lib/auth.ts):
- signup() generates a fourth salt and derives the recovery
verifier from the recovery code
- recover() fetches the new rec_auth_salt and submits the derived
verifier as part of recovery-complete
Recovery.svelte distinguishes the new 401 ("Feil gjenopprettingskode")
and 409 ("Denne kontoen mangler gjenopprettingsverifikator") cases.
Regression test (tests/auth.test.ts) asserts the gate is real:
- junk recovery verifier → 401, no state changes
- unknown email → 401 (constant-time)
- challenge response includes rec_auth_salt
- correctly-derived verifier passes the gate
SECURITY.md is updated to describe four salts instead of three, the
new key-model storage, and the closed lockout DoS. CLAUDE.md flags
the rec_auth_* columns as load-bearing — removing them re-opens the
hole.
This is the only deviation from the spec's stated storage model;
documented as such in both SECURITY.md and CLAUDE.md.
This commit is contained in:
parent
47963c9225
commit
add76be486
9 changed files with 414 additions and 72 deletions
21
CLAUDE.md
21
CLAUDE.md
|
|
@ -92,13 +92,24 @@ If we ever want to keep them strictly separate, the change goes in
|
|||
|
||||
## What's deferred (documented in SECURITY.md)
|
||||
|
||||
- Recovery-code lockout-DoS. The recovery endpoint has no server-side proof
|
||||
of the recovery code; an attacker who knows the email can lock out the
|
||||
user (but **not** read their data). Mitigations: rate limiting and email
|
||||
confirmation. Out of scope for the scaffold.
|
||||
- Server-side rate limiting in general.
|
||||
- Server-side rate limiting on auth/recovery endpoints. The recovery
|
||||
verifier closes the lockout-DoS; rate limiting reduces the online
|
||||
brute-force surface on top of that.
|
||||
- CSP / SRI for the SPA.
|
||||
|
||||
## Recovery verifier — deviation from the spec
|
||||
|
||||
The original spec stored only `kek_salt`, `wrapped_dek_pw`+nonce, `rec_salt`,
|
||||
and `wrapped_dek_rec`+nonce. We additionally store `rec_auth_salt` and
|
||||
`rec_auth_verifier_hash` so the server can verify the caller knows the
|
||||
recovery code before `/auth/recovery-complete` writes anything. This is the
|
||||
only deviation from the spec's stated storage model — documented in
|
||||
SECURITY.md.
|
||||
|
||||
If you find yourself "simplifying away" the rec_auth_* columns or the verifier
|
||||
check, **stop**: that re-opens the lockout DoS. See the test in
|
||||
`tests/auth.test.ts` for the regression case.
|
||||
|
||||
## Run / test / typecheck
|
||||
|
||||
- `bun install`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue