// Password hashing and session token helpers. const crypto = require('crypto'); const PASSWORD_ITERATIONS = Number(process.env.PASSWORD_HASH_ITERATIONS || 310000); const PASSWORD_KEY_LENGTH = 32; const PASSWORD_DIGEST = 'sha256'; const SESSION_BYTES = 32; 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 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, validatePasswordStrength, createSessionToken, hashSessionToken };