Node Crypto Scrypt Password Hashing

Hash passwords with the built-in scrypt using random per-user salts, explicit cost parameters, and timing-safe comparison on verification.

JavascriptNodeSecurity Boundaries

Code

snippet
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);

const PARAMS = { N: 16384, r: 8, p: 1, keylen: 64 };

async function hashPassword(password) {
  const salt = randomBytes(16);
  const derived = await scryptAsync(password.normalize('NFKC'), salt, PARAMS.keylen, PARAMS);
  return `scrypt:${PARAMS.N}:${PARAMS.r}:${PARAMS.p}:${salt.toString('base64')}:\
${derived.toString('base64')}`;
}

async function verifyPassword(password, stored) {
  const [scheme, N, r, p, saltB64, hashB64] = stored.split(':');
  if (scheme !== 'scrypt') return false;
  const salt = Buffer.from(saltB64, 'base64');
  const expected = Buffer.from(hashB64, 'base64');
  const actual = await scryptAsync(password.normalize('NFKC'), salt, expected.length, {
    N: Number(N), r: Number(r), p: Number(p),
  });
  return actual.length === expected.length && timingSafeEqual(actual, expected);
}

const record = await hashPassword('correct horse battery staple');
console.log(await verifyPassword('wrong password', record));
console.log(await verifyPassword('correct horse battery staple', record));