security-audit
密码哈希算法全景:从算法百科到工程选型
系统梳理 MD5、SHA、PBKDF2、bcrypt、scrypt、Argon2、yescrypt 与历史密码格式,并给出参数、选型和迁移方法。
先给结论:新建的通用密码数据库,优先使用成熟框架或密码库提供的 Argon2id。环境不支持 Argon2id 时,通常退到内存困难的 scrypt。如果系统受已验证密码模块或合规边界约束,PBKDF2-HMAC 仍然有现实价值。已有 bcrypt 数据库可以继续验证,但必须弄清 72 字节输入边界,并设计渐进迁移。MD5、SHA-1、SHA-256、SHA-3、BLAKE2 和 BLAKE3 都不能直接拿来存密码。
这件事容易混乱,是因为大家会把很多不同的东西都叫“密码哈希”。有些算法是追求吞吐量的摘要函数,有些是旧操作系统里的存储格式,还有一些才是专门把每次猜测变贵的密码哈希或 KDF。名字里都有 hash,不代表它们给离线攻击者制造的成本相同。
先把几个概念分开
| 类型 | 能否还原 | 主要用途 | 是否适合存密码 |
|---|---|---|---|
| 加密 | 持有密钥时可以 | 保护需要恢复的数据 | 通常不适合;解密密钥泄漏会暴露全部密码 |
| 通用密码学哈希 | 不可逆 | 完整性、签名、内容寻址 | 不适合;它被设计得很快 |
| MAC / HMAC | 不可逆、带密钥 | 真实性和完整性 | 不能单独作为密码存储方案 |
| 密码哈希 / KDF | 不可逆 | 让每次密码猜测消耗更多资源 | 配置和实现正确时适合 |
密码验证系统通常保存一个派生值,而不是能恢复的密码。登录时,系统读取盐和参数,用同一方案处理用户刚提交的密码,再调用密码库的验证接口比较结果。
“不可逆”不等于“破解不了”。攻击者拿到数据库后,可以离线猜一个候选密码,跑同样的公开算法,再对比输出。密码哈希解决的是攻击成本:让每次猜测都足够慢、足够占内存,避免数据库一泄漏,常见密码就立刻变成明文。
算法之外还要保存什么
只记一个算法名远远不够。
- **盐(salt):**每个密码单独生成的随机值。它防止一张预计算表同时攻击很多账号,也让相同密码产生不同结果。盐不需要保密,通常和哈希放在一起。
- **工作因子(work factor):**可调的时间或迭代成本。硬件变快后,可以提高它。
- **内存成本:**一次计算需要占用的工作内存。内存困难算法会提高 GPU、ASIC 大规模并行猜测的成本。
- **并行度:**算法允许同时推进多少计算链。不同算法里的含义不同,不能简单理解为越大越安全。
- **Pepper:**与密码数据库分离保存的可选秘密,最好放在 KMS、HSM 或其他独立保护边界后。它能降低“只偷到数据库”的风险,但丢失或轮换都比盐复杂。NIST 当前指南建议条件允许时再增加一次使用验证端秘密的带密钥运算 [5]。
- **版本和参数:**记录必须说明自己是怎么生成的,否则未来无法可靠地渐进升级。
盐解决的是预计算和跨账号复用,不会把弱密码变强,也不该被当成秘密。Pepper 解决的是另一个问题:如果攻击者只拿到数据库,没有拿到独立秘密,还不能完成验证计算。
算法全景
通用高速哈希:算法没坏,但用途不对
MD5、SHA-1、SHA-2、SHA-3、BLAKE2 和 BLAKE3 都追求高效处理数据。SHA-2、SHA-3 是标准化的通用哈希族,BLAKE2、BLAKE3 的设计也明确强调高效计算 [10] [11] [12] [13]。文件校验、数字签名、Merkle tree、内容寻址和协议构造都需要这种速度;离线猜密码时,这个优点正好变成缺点。
| 算法族 | 现在的用途 | 密码存储结论 |
|---|---|---|
| MD5 | 旧校验和;碰撞安全性已破坏 | 无论是否加盐都不要使用 |
| SHA-1 | 旧系统兼容;碰撞安全性已破坏 | 不要直接使用 |
| SHA-2 / SHA-3 | 现代通用哈希 | 原语本身可用,但单独存密码太快 |
| BLAKE2 / BLAKE3 | 高性能通用哈希 | 快是设计目标,不是密码哈希 |
密码场景最核心的问题不是碰撞。攻击者通常会拿一个高概率密码去找某条记录的原像。即使通用哈希仍然抗碰撞,只要它足够快,就能让攻击者快速试大量候选值。给 SHA-256 加随机盐可以解决彩虹表复用,却不能解决速度。自己把 SHA-256 循环几万次,也只是做了一个编码、参数、升级语义和硬件抵抗力都未经审查的私有 KDF。
历史密码格式
安全审计和迁移时经常会遇到这些格式,但不应该再给它们写入新密码。
| 方案 | 识别重点 | 工程状态 |
|---|---|---|
传统 DES crypt | 密码和盐都很短,成本停留在早期 Unix 时代 | 淘汰;登录时迁移或要求重置 |
| LM hash | 忽略大小写并分段处理,长度和结构弱点严重 | 已淘汰,风险很高 |
| NT hash / NTLM 密码哈希 | 对 UTF-16LE 密码做无盐 MD4 | 仅用于 Windows 兼容;能迁则迁 |
MD5-crypt($1$) | 加盐并迭代 MD5 的 Unix 格式 | 淘汰 |
SHA-crypt($5$、$6$) | 可调轮数的 SHA-256/SHA-512 Unix 格式 | Unix 兼容;只消耗 CPU,不是内存困难算法 |
| phpass portable hash | 旧 PHP 应用里的迭代 MD5 格式 | 只作为迁移输入,不再新选 |
libxcrypt 仍把提高轮数后的 SHA-256/512-crypt 视为可接受的 Unix 新哈希,而 OWASP 的应用选型顺序优先 Argon2id 和 scrypt [14]。两者讨论的环境不同,不能据此把 SHA-crypt 和现代内存困难 KDF 当成同一级别。LM 和 NT hash 也应被看作 Windows 兼容凭据,而不是新应用的设计模板 [15] [16]。Openwall 对 phpass 的说明同样明确:portable MD5 fallback 只维持旧系统,新 PHP 项目应使用原生 password API [19]。
PBKDF2
PBKDF2 会重复执行一个伪随机函数,实际最常见的是 HMAC。它在 PKCS #5 中有标准定义,平台密码学 API 普遍支持 [4]。它的优势是互操作性好、实现成熟,也容易进入已验证密码模块。缺点是主要消耗 CPU、占用内存很少,GPU 等并行硬件更容易优化攻击。
只有真实的平台或合规边界要求时,才因为这个理由选择 PBKDF2,不能把“NIST”三个字当成万能安全标签。FIPS 要求指向的是部署中的密码模块、approved mode、配置和系统边界,而不是源码里写了某个算法名 [17]。OWASP 当前在 FIPS-140 场景下给出的基线是 PBKDF2-HMAC-SHA-256 迭代 600,000 次 [1]。这只是需要实测的起点,不是永久通用常数。NIST SP 800-132 发布于 2010 年且正在修订,也不应该被当成 2026 年固定迭代数表 [6]。
PBKDF1 只为兼容保留,PKCS #5 已明确不建议新系统使用 [4]。
bcrypt
bcrypt 在 1999 年发表,利用 Blowfish 昂贵的 key schedule,并加入对数形式的成本参数和 128-bit 盐,让 Unix 密码哈希能随硬件升级而变慢 [7]。
它二十多年的部署经验很有价值,但今天有两个明显限制:
- bcrypt 使用的内存很少,不具备 Argon2、scrypt 意义上的内存困难特性。
- 大多数兼容格式只使用密码输入的前 72 字节。字节不等于字符,UTF-8 密码可能远不到 72 个可见字符就越界;不同库可能静默截断,也可能直接拒绝。
不能让两个只在第 72 字节后不同的密码悄悄变成同一个凭据。维护 bcrypt 系统时,需要测试具体库和版本,明确字符编码和超长输入行为,并始终使用库提供的 verify API。OWASP 把 bcrypt 放在旧系统场景,要求必须保留时 work factor 至少为 10 [1]。
“先 SHA-512 再 bcrypt”也不是随手就能加的修复。它会引入 domain separation、二进制编码、NUL、截断和 password shucking 等新问题。只有把它当成一个有版本、有测试、有迁移元数据的完整协议,才可能安全使用。
scrypt
scrypt 同时消耗计算和大量内存,常见参数是 N、r、p,分别控制 CPU/内存成本、block size 和并行度。RFC 7914 给出了算法定义和测试向量 [3]。
当 Argon2id 不可用时,scrypt 仍是合理后备方案。真正容易出错的是照抄参数。OWASP 当前列出的第一组基线为 N=2^17, r=8, p=1,大约占用 128 MiB;其他组合可以用更多并行计算换更低内存 [1]。服务必须在登录峰值并发下测试,而不只是单请求跑一次 benchmark。
Argon2d、Argon2i 与 Argon2id
Argon2 是 Password Hashing Competition 的获胜方案,之后由 RFC 9106 规范化。它暴露内存大小 m、pass 次数 t 和并行度 p;编码格式还能携带 variant、版本、盐和参数 [2]。
- Argon2d 使用数据相关的内存访问,对 time-memory trade-off 抵抗力强,但可能泄露访存侧信道,因此不适合普通密码数据库的默认场景。
- Argon2i 使用数据无关访问,侧信道风险较低,但为了抵抗部分权衡攻击需要更多 pass。
- Argon2id 先使用 Argon2i 风格,再切换到 Argon2d 风格,是 RFC 9106 和 OWASP 推荐的平衡方案 [1] [2]。
两份权威资料给出不同参数并不矛盾。RFC 9106 的两组统一建议是 2 GiB、t=1、p=4,或者在内存受限环境使用 64 MiB、t=3、p=4;两者都使用 128-bit 盐和 256-bit 输出。OWASP 面向应用服务器给了更低的实用下限,其中第一组是 19 MiB、t=2、p=1。它们假设的资源预算不同。工程上应该先守住适用指南的下限,再尽量使用可承受的最大内存,并在真实服务上测量。
yescrypt 与 Balloon Hashing
yescrypt 构建在 scrypt 之上,加入了面向大规模破解成本的扩展模式。它并不是只存在于论文:Openwall 列出的多个 Linux 发行版已经把它作为系统密码默认格式 [8]。如果操作系统和 libxcrypt 管理 /etc/shadow,yescrypt 是严肃的平台选择;跨语言 Web 服务通常仍优先采用指导和库支持更广的 Argon2id。
Balloon Hashing 是数据无关访存的内存困难方案,论文给出了形式化分析,也讨论了顺序攻击模型的边界 [9]。不过它的主流认证框架支持明显少于 Argon2id、scrypt、PBKDF2 和 bcrypt,作者还明确标注公开原型不能安全用于生产 [18]。它值得了解,不适合作为常规默认方案。
Catena、Lyra2、Makwa、POMELO 等 Password Hashing Competition 方案也有研究价值。但“论文里值得研究”和“生产环境有成熟库、格式、运维工具”是两件不同的事。
工程上怎么选
| 场景 | 默认方向 | 原因 |
|---|---|---|
| 新应用、普通服务器环境 | Argon2id | 内存困难、现行指南明确、格式可自描述 |
| 不支持 Argon2,但有成熟内存困难 KDF | scrypt | 已标准化且实现广泛 |
| 受验证模块或 FIPS 边界约束 | approved module 中的 PBKDF2-HMAC | 符合常见验证边界,但必须核对具体模块和 policy |
| 已稳定运行的 bcrypt 数据库 | 保持验证、实测后提高成本,并向 Argon2id 迁移 | 避免一次性切换,同时解决旧限制 |
| 由现代 Linux 发行版管理的 Unix 登录 | 跟随系统支持的 yescrypt / Argon2 policy | 保持 PAM、crypt、恢复工具和系统格式一致 |
| MD5、SHA、LM/NTLM-only、DES-crypt 等弱记录 | 优先迁移或要求重置 | 离线猜测成本过低,或格式本身有严重限制 |
算法选型同时也是容量设计。一次验证占 64 MiB 的登录接口,如果允许攻击者并发发起几千个请求,仍可能耗尽内存。限流、队列、超时、账号滥用控制和容量测试都属于密码哈希设计。不能因为在线 DoS 风险就把 KDF 成本降到不安全水平。
参数要在真正执行验证的系统上调
权威参数适合做下限和测试起点,不能代替测量。
- 定义用户能接受、服务也能持续承受的单次验证延迟。OWASP 建议一次哈希大致不要超过一秒,很多在线服务会主动控制得更低。
- 在接近生产的 CPU、内存、容器 cgroup 或 serverless 限额下测试。
- 测登录峰值并发,不只看单请求中位数。
- 测错误密码和限流路径,确保无效请求不会无限制造昂贵计算。
- 保存算法、variant、版本、参数、盐和输出;库支持时优先使用 PHC string 等自描述格式。
- 定期重新 benchmark,策略变化后在成功登录时 rehash。
盐应该由密码库或密码学安全随机数生成器产生,不能用用户名或时间戳拼出来。最终比较也不要用普通字符串相等;框架有恒定时间 verify API 时直接使用它。
输入规则同样要明确。除非产品有公开且稳定的 normalization 规则,否则应保留用户实际输入,不能悄悄 trim 空格、改变大小写或做会让两个不同输入等价的 Unicode 归一化。NIST 只允许有限且清晰的输入容错,并要求支持长密码 [5]。
不停机迁移
旧哈希通常不能直接“转换”为 Argon2id,因为数据库里没有原始密码。最干净的迁移时机是成功登录:此时应用短暂拿到了用户提交的密码。
从记录中读取算法和参数
用旧方案验证
如果验证成功,而且策略判断需要升级:
用当前方案重新哈希刚提交的密码
原子替换旧记录
丢弃输入密码
完整迁移可以这样做:
- 验证器先支持所有仍在数据库里的旧格式。
- 指定唯一的当前算法和参数 policy。
- 旧格式验证成功后,立刻生成新记录并替换。
- 到期后让长期不登录的账号走安全重置流程。
- 持续统计各格式数量,归零后再删除旧验证代码。
把旧哈希再喂给新 KDF 的“双层哈希”有时能在没有明文时临时增加成本,但不会增加原密码熵,也修不好截断、碰撞或旧格式限制。它还会产生自定义格式,只能作为带版本、带退出条件的过渡层。
Pepper 轮换也有同类约束。旧记录依赖旧 pepper 时,直接删除旧密钥会让全部登录失败。应该在受控迁移期保留版本化密钥,或者要求重置,不能假设改写数据库就能重建已经丢失的秘密。
评审清单
- 使用的是专门的密码哈希,而不只是“密码学哈希”吗?
- 每个密码都有独立随机盐吗?
- 算法、版本、参数、盐和输出能被无歧义识别吗?
- 是否在接近生产的硬件和峰值并发下测过时间与内存?
- bcrypt 字节限制、输入编码、Unicode 处理和最大请求长度是否明确?
- Pepper 是否与数据库分离,并有轮换和恢复方案?
- 验证是否使用维护中的库接口和恒定时间比较?
- 应用能否识别旧参数,并在成功登录后 rehash?
- 限流和容量控制是否能避免 KDF 变成 DoS 工具?
- 合规结论是否针对真实部署的验证模块和运行模式,而不只是算法名?
真正耐用的规则不是“选一个哈希永远不变”,而是选择经过审查的密码哈希,保存完整参数,在真实系统里测量,并始终保留迁移路径。Argon2id 是今天常见的起点;带版本的记录和 rehash 流程,才是系统能应对明天变化的关键。
参考文献
[1] OWASP Foundation. “Password Storage Cheat Sheet.” https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
[2] A. Biryukov et al. “Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications.” RFC 9106. https://www.rfc-editor.org/rfc/rfc9106.html
[3] C. Percival and S. Josefsson. “The scrypt Password-Based Key Derivation Function.” RFC 7914. https://www.rfc-editor.org/rfc/rfc7914.html
[4] K. Moriarty et al. “PKCS #5: Password-Based Cryptography Specification Version 2.1.” RFC 8018. https://www.rfc-editor.org/rfc/rfc8018.html
[5] National Institute of Standards and Technology. “SP 800-63B: Authentication and Authenticator Management.” https://pages.nist.gov/800-63-4/sp800-63b.html
[6] National Institute of Standards and Technology. “SP 800-132: Recommendation for Password-Based Key Derivation, Part 1.” https://csrc.nist.gov/pubs/sp/800/132/final
[7] Niels Provos and David Mazières. “A Future-Adaptable Password Scheme.” USENIX ATC 1999. https://www.usenix.org/conference/1999-usenix-annual-technical-conference/future-adaptable-password-scheme
[8] Openwall. “yescrypt — scalable KDF and password hashing scheme.” https://www.openwall.com/yescrypt/
[9] Dan Boneh, Henry Corrigan-Gibbs, and Stuart Schechter. “Balloon Hashing: A Memory-Hard Function Providing Provable Protection Against Sequential Attacks.” https://eprint.iacr.org/2016/027
[10] National Institute of Standards and Technology. “Secure Hash Standard (SHS).” FIPS 180-4. https://csrc.nist.gov/pubs/fips/180-4/upd1/final
[11] National Institute of Standards and Technology. “SHA-3 Standard.” FIPS 202. https://csrc.nist.gov/pubs/fips/202/final
[12] M-J. Saarinen and J-P. Aumasson. “The BLAKE2 Cryptographic Hash and Message Authentication Code.” RFC 7693. https://www.rfc-editor.org/rfc/rfc7693.html
[13] BLAKE3 Team. “BLAKE3 specification and implementations.” https://github.com/BLAKE3-team/BLAKE3
[14] libxcrypt Project. “crypt(5): password hashing methods and prefixes.” https://man.archlinux.org/man/crypt.5.en
[15] Microsoft. “Prevent Windows from storing an LM hash of your password.” https://learn.microsoft.com/en-us/troubleshoot/windows-server/windows-security/prevent-windows-store-lm-hash-password
[16] Microsoft Open Specifications. “MS-NLMP: NTLM v1 Authentication.” https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/464551a8-9fc4-428e-b3d3-bc5bfb2e73a5
[17] NIST Cryptographic Module Validation Program. “FIPS 140-3 standards and approved security functions.” https://csrc.nist.gov/Projects/cryptographic-module-validation-program/fips-140-3-standards
[18] Stanford Applied Cryptography Group. “Balloon Hashing project and prototype.” https://crypto.stanford.edu/balloon/
[19] Openwall. “Portable PHP password hashing framework.” https://www.openwall.com/phpass/