Repository Public
1import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";23const N = 1 << 15, R = 8, P = 1, KEYLEN = 32;4const derive = (password: string, salt: Buffer, n = N) => new Promise<Buffer>((res, rej) =>5 scrypt(password.normalize("NFKC"), salt, KEYLEN, { N: n, r: R, p: P, maxmem: 128 * n * R * 2 }, (e, k) => (e ? rej(e) : res(k))));67export async function hashPassword(password: string): Promise<string> {8 const salt = randomBytes(16);9 return `scrypt$${N}$${salt.toString("base64")}$${(await derive(password, salt)).toString("base64")}`;10}11export async function verifyPassword(password: string, stored: string): Promise<boolean> {12 const [alg, n, salt, hash] = stored.split("$");13 if (alg !== "scrypt" || !salt || !hash) return false;14 const expected = Buffer.from(hash, "base64");15 const got = await derive(password, Buffer.from(salt, "base64"), Number(n));16 return got.length === expected.length && timingSafeEqual(got, expected);17}18/** Passwords must be long; no composition rules (NIST 800-63B). */19export function passwordProblem(pw: string): string | null {20 if (pw.length < 12) return "Use at least 12 characters";21 if (pw.length > 200) return "Use at most 200 characters";22 if (/^(.)\1+$/.test(pw)) return "Choose a less predictable password";23 return null;24}25export const randomPassword = () => randomBytes(18).toString("base64url");26