156 lines
5.9 KiB
JavaScript
156 lines
5.9 KiB
JavaScript
// 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
|
|
}; |