// password.ts // // Password hashing using Node.js scrypt (no external dependencies). // Format: $scrypt$N$r$p$salt$hash (all base64) import { randomBytes, type ScryptOptions, scrypt, timingSafeEqual, } from "node:crypto"; // Configuration const SALT_LENGTH = 32; const KEY_LENGTH = 64; const SCRYPT_PARAMS: ScryptOptions = { N: 16384, // CPU/memory cost parameter (2^14) r: 8, // Block size p: 1, // Parallelization }; // Promisified scrypt with options support function scryptAsync( password: string, salt: Buffer, keylen: number, options: ScryptOptions, ): Promise { return new Promise((resolve, reject) => { scrypt(password, salt, keylen, options, (err, derivedKey) => { if (err) reject(err); else resolve(derivedKey); }); }); } async function hashPassword(password: string): Promise { const salt = randomBytes(SALT_LENGTH); const hash = await scryptAsync(password, salt, KEY_LENGTH, SCRYPT_PARAMS); const { N, r, p } = SCRYPT_PARAMS; return `$scrypt$${N}$${r}$${p}$${salt.toString("base64")}$${hash.toString("base64")}`; } async function verifyPassword( password: string, stored: string, ): Promise { const parts = stored.split("$"); if (parts[1] !== "scrypt" || parts.length !== 7) { throw new Error("Invalid password hash format"); } const [, , nStr, rStr, pStr, saltB64, hashB64] = parts; const salt = Buffer.from(saltB64, "base64"); const storedHash = Buffer.from(hashB64, "base64"); const computedHash = await scryptAsync(password, salt, storedHash.length, { N: parseInt(nStr, 10), r: parseInt(rStr, 10), p: parseInt(pStr, 10), }); return timingSafeEqual(storedHash, computedHash); } export { hashPassword, verifyPassword };