@fernclark/four-word-recovery
v0.1.0
Published
Four-word evocative recovery phrase generation, validation, and constant-time comparison for anonymous-first identity systems.
Maintainers
Readme
@fernclark/four-word-recovery
Four-word evocative recovery phrase generation, validation, and constant-time comparison for anonymous-first identity systems.
Built for miracles.live — a daily contemplative practice that is anonymous by default and never asks for an email address.
Why four words?
256^4 ≈ 4.3 billion combinations (32 bits of entropy). Combined with per-IP throttling on the claim endpoint, this is well above the practical brute-force ceiling for a low-stakes account-recovery surface.
The wordlist is deliberately tone-aligned — words like amber, forest, tide, and solace — so the phrase feels like part of a practice rather than a random password. No homophones. No offensive words. All unique, 4–7 letter lowercase.
Installation
npm install @fernclark/four-word-recoveryUsage
import {
generateRecoveryPhrase,
normalizePhrase,
isValidPhrase,
newSalt,
hashPhrase,
safeHashEqual,
} from "@fernclark/four-word-recovery";
// Generate a phrase
const phrase = generateRecoveryPhrase();
// => "amber-forest-stone-tide"
// Normalize user input (accepts spaces, commas, or dashes)
const canonical = normalizePhrase("Amber Forest Stone Tide");
// => "amber-forest-stone-tide"
// Validate before storing
if (!isValidPhrase(canonical)) throw new Error("Invalid phrase");
// Hash for storage (never store the canonical phrase)
const salt = newSalt();
const hash = hashPhrase(canonical, salt);
// Store { salt, hash } — never { canonical }
// Verify on claim
const match = safeHashEqual(hash, hashPhrase(normalizePhrase(userInput), salt));API
generateRecoveryPhrase(): string
Generates a random four-word phrase using crypto.randomInt (no modulo bias). Returns words joined by -.
normalizePhrase(raw: string): string
Normalizes user input: lowercase, replaces spaces/commas with dashes, collapses repeated separators, trims. Call before isValidPhrase and hashPhrase.
isValidPhrase(canonical: string): boolean
Validates a normalized phrase: exactly four words, every word present in the wordlist.
newSalt(): string
Returns a 16-byte cryptographically random hex string for use as a hash salt.
hashPhrase(canonical: string, salt: string): string
SHA-256 hash of salt:canonical. Store this alongside the salt; never store the canonical phrase.
safeHashEqual(a: string, b: string): boolean
Constant-time comparison of two hex hash strings. Prevents timing oracle attacks.
RECOVERY_WORDLIST: readonly string[]
The 256-word wordlist, exported for inspection or extension.
Security notes
- Always throttle the claim endpoint by IP (12 attempts / hour is a reasonable ceiling).
- Never log or return the canonical phrase after it has been shown to the user once.
- The hash is SHA-256 with a random salt — not bcrypt or argon2 — because the entropy in the wordlist (32 bits) is the rate-limiting factor, not the hash speed.
safeHashEqualusestimingSafeEqualto prevent timing side-channels.
License
MIT — FeRn Clark / miracles.live
