// ============================================================ // Unified password hashing — argon2id preferred, bcrypt fallback // Transparent migration: on successful login with a bcrypt hash, // the row is rehashed as argon2id and updated in place. // ============================================================ var bcrypt = require('bcryptjs'); var argon2 = null; try { argon2 = require('argon2'); } catch (e) { // argon2 optional — install with `npm install argon2` to enable. // Until then, registration + change-password fall back to bcrypt(12). console.warn('[passwords] argon2 not installed — using bcrypt only. Install argon2 to enable migration.'); } var BCRYPT_ROUNDS = 12; var ARGON2_OPTS = { type: 2, // argon2id memoryCost: 19456, // 19 MiB — OWASP 2023 recommended timeCost: 2, parallelism: 1 }; function isArgon2Hash(h) { return typeof h === 'string' && h.indexOf('$argon2') === 0; } function isBcryptHash(h) { return typeof h === 'string' && /^\$2[aby]\$/.test(h); } async function hash(password) { if (argon2) return argon2.hash(password, ARGON2_OPTS); return bcrypt.hash(password, BCRYPT_ROUNDS); } async function verify(password, storedHash) { if (!storedHash) return false; if (isArgon2Hash(storedHash)) { if (!argon2) throw new Error('argon2 hash encountered but argon2 package not installed'); return argon2.verify(storedHash, password); } if (isBcryptHash(storedHash)) { return bcrypt.compare(password, storedHash); } return false; } // Returns a new argon2 hash if migration is desired, else null. async function maybeRehash(password, storedHash) { if (!argon2) return null; if (isArgon2Hash(storedHash)) return null; // already argon2 return argon2.hash(password, ARGON2_OPTS); } module.exports = { hash: hash, verify: verify, maybeRehash: maybeRehash, hasArgon2: function(){ return !!argon2; } };