/** * Password hashing and validation utilities. * Uses Node.js built-in crypto (scrypt) — no external dependencies. */ import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto'; import { promisify } from 'node:util'; const scryptAsync = promisify(scrypt); const SALT_LENGTH = 32; const KEY_LENGTH = 64; /** * Hash a password using scrypt. * Returns "salt:hash" in hex encoding. */ export async function hashPassword(password: string): Promise { const salt = randomBytes(SALT_LENGTH); const derived = (await scryptAsync(password, salt, KEY_LENGTH)) as Buffer; return `${salt.toString('hex')}:${derived.toString('hex')}`; } /** * Verify a password against a stored hash. */ export async function verifyPassword(password: string, storedHash: string): Promise { const [saltHex, hashHex] = storedHash.split(':'); if (!saltHex || !hashHex) return false; const salt = Buffer.from(saltHex, 'hex'); const stored = Buffer.from(hashHex, 'hex'); const derived = (await scryptAsync(password, salt, KEY_LENGTH)) as Buffer; return timingSafeEqual(stored, derived); } /** * Password complexity requirements: * - Min 8 characters * - At least 1 uppercase letter * - At least 1 lowercase letter * - At least 1 digit */ export function validatePasswordComplexity(password: string): { valid: boolean; errors: string[] } { const errors: string[] = []; if (password.length < 8) errors.push('Password must be at least 8 characters'); if (!/[A-Z]/.test(password)) errors.push('Password must contain at least 1 uppercase letter'); if (!/[a-z]/.test(password)) errors.push('Password must contain at least 1 lowercase letter'); if (!/\d/.test(password)) errors.push('Password must contain at least 1 digit'); return { valid: errors.length === 0, errors }; }