/** * Hashes and checks passwords, for sign-up, sign-in, password changes and the * admin that `db/startup.ts` seeds. It reads no config, so the startup tasks * can use it before the server's settings are checked. * * @module */ import { randomBytes, scrypt, timingSafeEqual } from "node:crypto"; import { Buffer } from "node:buffer"; /** * Hashes `password` with scrypt and a random salt, as `hash.salt` in hex, for * `users.password_hash`. */ export function hashPassword(password: string): Promise { const salt = randomBytes(16).toString("hex"); return new Promise((resolve, reject) => { scrypt(password, salt, 64, (err, derivedKey) => { if (err) reject(err); else resolve(`${derivedKey.toString("hex")}.${salt}`); }); }); } /** Checks `password` against a hash made by {@link hashPassword}. */ export function verifyPassword( password: string, storedHash: string, ): Promise { const [hash, salt] = storedHash.split("."); // scrypt would throw on a missing salt, and a malformed hash matches nothing. if (!hash || !salt) return Promise.resolve(false); return new Promise((resolve, reject) => { scrypt(password, salt, 64, (err, derivedKey) => { if (err) reject(err); else { const expected = Buffer.from(hash ?? "", "hex"); resolve( expected.length === derivedKey.length && timingSafeEqual(expected, derivedKey), ); } }); }); }