first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
# `recovery/verify`
## Purpose
Verify a user-supplied recovery code against a list of stored hashes.
Constant-time over the full set — every entry is evaluated even after a
match is found so loop-position timing does not leak which code matched.
On success the returned `codeIndex` lets the caller mark that hash as used
(recovery codes are single-use).
## Features
- Constant-time across the full hash list.
- Argon2id re-derivation using the per-hash stored parameters.
- Returns `{ valid, codeIndex }` discriminated union.
- Input normalisation — separators stripped, upper-cased — so `aaaa-bbbb`
matches the original `AAAABBBB` form regardless of how the user types it.
## Usage: Basic Example
```ts
import { verifyRecoveryCode } from "@suaveplan/otp";
const r = await verifyRecoveryCode("aaaa-bbbb", user.recoveryHashes);
if (r.valid) markUsed(user.recoveryHashes[r.codeIndex]);
```
## Usage: Advanced Example
```ts
import { verifyRecoveryCode } from "@suaveplan/otp";
// Drop the matched hash from the list atomically; subsequent attempts will
// miss even if the same code is replayed.
const r = await verifyRecoveryCode(input, hashes);
if (r.valid) {
await db.transaction(async (tx) => {
await tx.removeHash(user.id, hashes[r.codeIndex]);
});
}
```
## API Reference
- `verifyRecoveryCode(input, hashedCodes): Promise<RecoveryVerifyResult>` —
returns `{ valid: true; codeIndex }` on match, `{ valid: false; codeIndex: null }`
otherwise.
## Implementation Notes
Argon2id is intentionally slow — verifying a list of N hashes runs N derivations.
The constant-time loop completes regardless of where the match is so timing
does not reveal list position. Production callers typically cap the list at
10 codes; below that the wall-clock cost is bounded and acceptable for an
interactive recovery flow.
Each list entry carries its own Argon2id parameters in the encoded form so
parameter rotation works without re-hashing the entire user base: as the
threat model evolves, new codes can be hashed with stronger parameters
while older codes verify against their original parameters. The hash
format is self-describing precisely so this rotation is not a breaking
change. Callers should still re-hash on next-use to migrate stragglers
forward.