← 中文资源

简体中文版本

Node 内置 Scrypt 密码哈希

用 Node 内置 crypto 的 scrypt 做密码哈希:每用户随机盐、显式成本参数随密文存储、验证时恒时比较防时序侧信道。

阅读英文原文

示例说明

密码绝不能明文或单向 MD5 存储。scrypt 是内存受限型慢哈希,GPU 暴力破解成本远高于传统算法。安全实现的三要素在这里齐备:随机盐让相同密码产出不同密文、成本参数随密文持久化以便未来升级、timingSafeEqual 让验证耗时与结果无关,攻击者无法用响应时间推测前缀。

技术要点

  • randomBytes 生成 16 字节独立盐
  • 成本参数编码进存储格式便于平滑升级
  • timingSafeEqual 抵御时序侧信道

代码解读

hashPassword 先规范化 Unicode 防等价字符绕过,再派生 64 字节密钥,存储格式把算法名与全部参数冒号分隔序列化。verifyPassword 解析同一格式还原盐与参数重新派生,长度一致后用恒时比较。两次验证演示错误口令返回 false 而正确口令返回 true。

使用建议

N 值提升会线性增加登录耗时,调整前要压测登录接口容量。参数升级时可同时接受新旧格式并在下次登录时透明重哈希。登录失败统一延迟返回,避免用户枚举。

代码示例

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));