// Password hashing and session token helpers. const crypto = require('crypto'); const PASSWORD_ITERATIONS = 310000; const PASSWORD_KEY_LENGTH = 32; const PASSWORD_DIGEST = 'sha256'; const SCRYPT_OPTIONS = { N: 16384, r: 16, p: 1, keyLength: 64, maxmem: 128 * 16384 * 16 * 2 }; const SESSION_BYTES = 32; function createOneTimeToken() { return crypto.randomBytes(32).toString('hex'); } function validatePasswordStrength(password, options) { const value = String(password || ''); const requirements = options && options.policy ? getPasswordPolicyPreset(options.policy) : { minimumLength: Number(options && options.minimumLength) || 10, minimumCategories: Number(options && options.minimumCategories) || 3, requireLowercase: Boolean(options && options.requireLowercase), requireUppercase: Boolean(options && options.requireUppercase), requireNumber: Boolean(options && options.requireNumber), requireSymbol: Boolean(options && options.requireSymbol) }; const hasLowercase = /[a-z]/.test(value); const hasUppercase = /[A-Z]/.test(value); const hasNumber = /[0-9]/.test(value); const hasSymbol = /[^A-Za-z0-9]/.test(value); const categoryCount = [hasLowercase, hasUppercase, hasNumber, hasSymbol].filter(Boolean).length; const missingRequiredCategory = requirements.requireLowercase && !hasLowercase || requirements.requireUppercase && !hasUppercase || requirements.requireNumber && !hasNumber || requirements.requireSymbol && !hasSymbol; if (value.length < requirements.minimumLength || categoryCount < requirements.minimumCategories || missingRequiredCategory) { if (missingRequiredCategory) { const requiredCategories = []; if (requirements.requireLowercase) requiredCategories.push('lowercase'); if (requirements.requireUppercase) requiredCategories.push('uppercase'); if (requirements.requireNumber) requiredCategories.push('number'); if (requirements.requireSymbol) requiredCategories.push('symbol'); return `Password must be at least ${requirements.minimumLength} characters and include ${requiredCategories.join(', ')}.`; } if (requirements.minimumCategories === 4) { return `Password must be at least ${requirements.minimumLength} characters and include uppercase, lowercase, number, and symbol.`; } return `Password must be at least ${requirements.minimumLength} characters and include ${requirements.minimumCategories} of: uppercase, lowercase, number, and symbol.`; } return ''; } function getPasswordPolicyPreset(policy) { const normalizedPolicy = String(policy || 'standard').trim().toLowerCase(); return normalizedPolicy === 'strict' ? { minimumLength: 14, minimumCategories: 4 } : normalizedPolicy === 'strong' ? { minimumLength: 12, minimumCategories: 3 } : { minimumLength: 10, minimumCategories: 3 }; } function hashPassword(password, salt) { const safePassword = String(password || ''); const safeSalt = salt || crypto.randomBytes(16).toString('hex'); const hash = crypto.pbkdf2Sync(safePassword, safeSalt, PASSWORD_ITERATIONS, PASSWORD_KEY_LENGTH, PASSWORD_DIGEST).toString('hex'); return { salt: safeSalt, hash: hash, iterations: PASSWORD_ITERATIONS }; } function encodePasswordRecord(record) { return ['pbkdf2-sha256', Number(record.iterations), record.salt, record.hash].join('$'); } function verifyEncodedPassword(password, encodedPassword) { const [algorithm, iterations, salt, hash] = String(encodedPassword || '').split('$'); if (algorithm !== 'pbkdf2-sha256' || !iterations || !salt || !hash) { return false; } return verifyPassword(password, { password_hash: hash, password_salt: salt, password_iterations: Number(iterations) }); } function deriveScryptKey(password, salt) { return new Promise(function (resolve, reject) { crypto.scrypt(String(password || '').normalize('NFKC'), salt, SCRYPT_OPTIONS.keyLength, SCRYPT_OPTIONS, function (error, key) { if (error) return reject(error); resolve(key); }); }); } async function hashScryptPassword(password) { const salt = crypto.randomBytes(16).toString('hex'); const key = await deriveScryptKey(password, salt); return salt + ':' + key.toString('hex'); } async function verifyScryptPassword(password, encodedPassword) { const [salt, hash] = String(encodedPassword || '').split(':'); if (!/^[a-f0-9]{32}$/i.test(salt || '') || !/^[a-f0-9]{128}$/i.test(hash || '')) return false; const expected = Buffer.from(hash, 'hex'); const actual = await deriveScryptKey(password, salt); return crypto.timingSafeEqual(actual, expected); } async function verifyCredentialPassword(password, encodedPassword) { if (String(encodedPassword || '').startsWith('pbkdf2-sha256$')) { return verifyEncodedPassword(password, encodedPassword); } try { return await verifyScryptPassword(password, encodedPassword); } catch (_error) { return false; } } function verifyPassword(password, record) { if (!record || !record.password_salt || !record.password_hash) { return false; } const iterations = Number(record.password_iterations || PASSWORD_ITERATIONS); const safePassword = String(password || ''); const expected = crypto.pbkdf2Sync(safePassword, String(record.password_salt), iterations, PASSWORD_KEY_LENGTH, PASSWORD_DIGEST); const actual = Buffer.from(String(record.password_hash), 'hex'); return actual.length === expected.length && crypto.timingSafeEqual(actual, expected); } function createSessionToken() { return crypto.randomBytes(SESSION_BYTES).toString('hex'); } function hashSessionToken(token) { return crypto.createHash('sha256').update(String(token || '')).digest('hex'); } module.exports = { hashPassword, verifyPassword, encodePasswordRecord, verifyEncodedPassword, hashScryptPassword, verifyScryptPassword, verifyCredentialPassword, validatePasswordStrength, createSessionToken, hashSessionToken, createOneTimeToken };