bcrypt vs Argon2: 2026年密码哈希
选择正确的密码哈希算法并为生产调整其参数。
密码哈希只有一个任务:让已经偷走你数据库的攻击者为每一次猜测付出高昂代价。通用哈希在设计上就无法完成这个任务——一块消费级GPU每秒能计算数百亿次SHA-256,用SHA-256哈希的8位密码几分钟就会沦陷。bcrypt和Argon2的存在就是为了改变这个经济方程,两者之间的选择主要在于理解各自让什么变得昂贵。
威胁模型: 离线破解
数据泄露后,攻击者掌握哈希值,在本地以硬件速度运行猜测。加盐(两种算法都自动处理,并把盐嵌入输出字符串)可以防止彩虹表并强制逐用户破解,但对猜测速率毫无作用。猜测速率由两个杠杆决定:
- 时间成本 — 一次哈希需要多少计算
- 内存成本 — 一次哈希需要多少RAM
内存是更有意思的杠杆。GPU有数千个核心,但每核心的高速内存有限;一个每次哈希需要64MB的算法,会把GPU并行度从数千个并发猜测削减到寥寥几个。
bcrypt: 1999年的设计,依然可靠
bcrypt(Provos和Mazières)建立在刻意昂贵的Blowfish密钥调度之上。成本参数是对数的——cost 12意味着4,096次迭代,每加1工作量翻倍:
import bcrypt from 'bcrypt';const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(password, hash);
它4KB的内部状态比SHA系列更抗GPU,但远低于现代内存困难函数的要求。两个必须了解的锋利边缘:
- 72字节截断。 bcrypt静默忽略超过72字节的输入,因此仅在72字节之后不同的两个字符串会哈希出相同结果。如果用预哈希来绕过,请先把摘要编码为base64或hex——原始SHA-256输出可能包含NUL字节,而某些bcrypt实现会在第一个NUL处截断,把大量密码坍缩为同一个。
- 变体动物园。
$2a$、$2b$、$2y$前缀是历史实现bug的遗迹。现代库输出$2b$;不要混用解释不一致的库。
Argon2: 内存困难的冠军
Argon2赢得了2015年密码哈希竞赛,是OWASP推荐的默认选择。请使用 Argon2id 变体——它结合了Argon2i对侧信道攻击的防御和Argon2d更强的GPU抗性:
import argon2 from 'argon2';const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 单位KiB,即64 MiB
timeCost: 3,
parallelism: 1
});
const ok = await argon2.verify(hash, password);
三个参数相互独立:memoryCost设定每次哈希的RAM,timeCost设定遍历该内存的次数,parallelism设定通道数。OWASP当前的最低标准是19 MiB / t=2 / p=1;64 MiB / t=3 / p=1是服务器级的宽裕配置。输出是自描述的PHC字符串——$argon2id$v=19$m=65536,t=3,p=1$...——参数随哈希一起存储,提高配置后旧哈希的验证依然有效。
调参方法论
参数依赖硬件,所以要用实测调优而不是照抄数字:
- 在生产级硬件上做基准测试,交互式登录以每次哈希200-500毫秒为目标。
- 在并发下测试。50个并发登录、每个64 MiB就是3.2GB内存——内存困难哈希若不加限制就是自找的DoS向量。给哈希操作加信号量或专用队列。
- 每年重新评估。今天的bcrypt cost 12在硬件追上来后应该变成13。
无需重置密码的迁移
升级哈希方案从来不需要强制重置:
1. 立即包裹现有哈希: 存储 argon2(legacy_hash) 并标记该行已包裹,马上消灭静态存储中的弱哈希。
2. 登录时重哈希: 用户认证时你短暂持有明文——先用旧方案验证,然后存储全新的Argon2id哈希。
3. 大多数库提供 needsRehash 检查,将已存哈希的参数与当前策略比较;在每次成功登录时调用它,升级就会自动完成。
常见错误
- 根本就在用快速哈希(MD5、SHA系列,即使加盐)处理密码
- 用非常量时间的字符串比较自行实现验证
- 把pepper和哈希存在同一个数据库(应放在KMS或环境密钥中,在哈希前以HMAC方式作用于密码)
- 在繁忙的Web服务器上把parallelism调高,饿死请求线程
- 混淆密码哈希与会话安全——如果会话令牌可被猜测,再完美的Argon2配置也无济于事
结论
新系统: 使用OWASP参数或更高的Argon2id。已有cost 12以上的bcrypt: 无需紧急迁移,但接入登录时重哈希,让系统自我升级。受FIPS约束的环境: 600,000次以上迭代的PBKDF2-HMAC-SHA256是合规的后备方案。任何情况下都不要自己造轮子。
scrypt的位置
scrypt(2009)在Argon2出现之前就开创了内存困难性,至今仍是可靠的选择,尤其是在库支持成熟的地方——Node内置的 crypto.scrypt、Go的x/crypto。它的参数更难推理:N、r、p以不明显的方式相互作用,常见推荐值N=2^17、r=8、p=1每次哈希约消耗128MB。但调校得当的scrypt并不比Argon2id弱多少。如果你的平台免费提供scrypt而Argon2需要引入原生依赖,选择scrypt是站得住脚的工程决策。
密码质量仍然比哈希更重要:用 sdk.is/password-generator 的密码生成器创建强随机密码。