# `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` — 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.