Files
skills/docs/security-stack/security/otp/src/recovery/verify.md
2026-05-29 08:20:56 +01:00

2.2 KiB

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

import { verifyRecoveryCode } from "@suaveplan/otp";

const r = await verifyRecoveryCode("aaaa-bbbb", user.recoveryHashes);
if (r.valid) markUsed(user.recoveryHashes[r.codeIndex]);

Usage: Advanced Example

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.