// --------------------------------------------------------------------------- // Password hashing for rp_auth. // // scrypt with per-password random salt. The cost parameters are stored inside // the hash string, so they can be raised later without invalidating existing // accounts - verify() always uses the parameters the hash was made with. // // Format: scrypt$N$r$p$$ // --------------------------------------------------------------------------- const crypto = require('crypto'); const N = 16384; // CPU/memory cost const R = 8; // block size const P = 1; // parallelisation const KEYLEN = 64; const MAXMEM = 96 * 1024 * 1024; // scrypt needs ~128*N*r bytes = 16MB here function derive(password, salt, n, r, p, cb) { crypto.scrypt(password, salt, KEYLEN, { N: n, r, p, maxmem: MAXMEM }, cb); } global.exports('hashPassword', (password, cb) => { if (typeof password !== 'string' || password.length === 0 || password.length > 200) { return cb(null, 'invalid password'); } const salt = crypto.randomBytes(16); derive(password, salt, N, R, P, (err, dk) => { if (err) return cb(null, err.message); cb(`scrypt$${N}$${R}$${P}$${salt.toString('base64')}$${dk.toString('base64')}`, null); }); }); global.exports('verifyPassword', (password, stored, cb) => { if (typeof password !== 'string' || typeof stored !== 'string') return cb(false, null); const parts = stored.split('$'); if (parts.length !== 6 || parts[0] !== 'scrypt') return cb(false, 'unrecognised hash format'); const n = parseInt(parts[1], 10); const r = parseInt(parts[2], 10); const p = parseInt(parts[3], 10); if (!n || !r || !p) return cb(false, 'corrupt hash parameters'); let salt, expected; try { salt = Buffer.from(parts[4], 'base64'); expected = Buffer.from(parts[5], 'base64'); } catch (e) { return cb(false, 'corrupt hash encoding'); } derive(password, salt, n, r, p, (err, dk) => { if (err) return cb(false, err.message); // constant time: a wrong password must not be distinguishable by timing const ok = dk.length === expected.length && crypto.timingSafeEqual(dk, expected); cb(ok, null); }); }); // Opaque tokens (character session handles, drop ids, ...) global.exports('randomToken', (bytes) => crypto.randomBytes(Math.min(Math.max(bytes || 16, 8), 64)).toString('hex'));