import { randomBytes, scrypt, timingSafeEqual } from "node:crypto"; const N = 16384; const R = 8; const P = 1; const KEY_BYTES = 32; const SALT_BYTES = 16; const MAX_MEMORY = 64 * 1024 * 1024; function derive(password: string, salt: Buffer, n: number, r: number, p: number): Promise { return new Promise((resolve, reject) => { scrypt(password.normalize("NFKC"), salt, KEY_BYTES, { N: n, r, p, maxmem: MAX_MEMORY }, (error, key) => error ? reject(error) : resolve(key), ); }); } /** `scrypt$N$r$p$salt$hash`, base64url fields. */ export async function hashSitePassword(password: string): Promise { const salt = randomBytes(SALT_BYTES); const hash = await derive(password, salt, N, R, P); return `scrypt$${N}$${R}$${P}$${salt.toString("base64url")}$${hash.toString("base64url")}`; } export async function verifySitePassword(password: string, stored: string): Promise { const [scheme, n, r, p, salt, hash] = stored.split("$"); if (scheme !== "scrypt" || !salt || !hash) return false; const cost = Number(n); if (cost !== N || Number(r) !== R || Number(p) !== P) return false; const expected = Buffer.from(hash, "base64url"); const actual = await derive(password, Buffer.from(salt, "base64url"), cost, R, P); return expected.length === actual.length && timingSafeEqual(expected, actual); }