示例说明
密码绝不能明文或单向 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));