// scrypt password hashing for the standalone fallback UI (design §9/§10.2). Format: // `scrypt$$`. Verification is constant-time. Only used by the standalone // server; the console-hosted path has no password of its own. import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto"; const KEYLEN = 32; /** Hash a password for storage in `config.ui.passwordHash`. */ export const hashPassword = (password: string): string => { const salt = randomBytes(16); const hash = scryptSync(password, salt, KEYLEN); return `scrypt$${salt.toString("base64url")}$${hash.toString("base64url")}`; }; /** Constant-time verify a password against a stored `scrypt$...` hash. */ export const verifyPassword = (password: string, stored: string): boolean => { const parts = stored.split("$"); if (parts.length !== 3 || parts[0] !== "scrypt" || !parts[1] || !parts[2]) return false; const salt = Buffer.from(parts[1], "base64url"); const expected = Buffer.from(parts[2], "base64url"); let actual: Buffer; try { actual = scryptSync(password, salt, expected.length); } catch { return false; } return actual.length === expected.length && timingSafeEqual(actual, expected); };