title: “密码存储为何从MD5走向Argon2——一场持续六十年的攻防博弈” date: “2026-03-06T08:45:20+08:00” description: “从1960年代MIT CTSS系统的首次密码泄露,到1979年Morris和Thompson发明salt机制,再到1999年bcrypt、2009年scrypt、2015年Argon2的相继问世,系统梳理密码存储技术六十年的攻防演进。深入分析彩虹表攻击的原理与防御、内存硬函数的设计哲学、GPU/FPGA破解技术的威胁演变,以及OWASP与NIST最新的参数配置建议。” draft: false categories: [“密码学”, “信息安全”, “Web安全”] tags: [“密码存储”, “bcrypt”, “scrypt”, “Argon2”, “PBKDF2”, “彩虹表”, “内存硬函数”, “密码哈希”, “GPU破解”, “salt”]
1962年,MIT的CTSS(Compatible Time-Sharing System)发生了一件小事:博士候选人Allan Scherr为了获得更多CPU时间,找到了一种方法打印出系统中所有用户的密码文件。这是历史上首次有记录的密码泄露事件,虽然影响范围有限,却暴露了一个根本性问题——当密码以明文存储时,任何获得数据库访问权限的人都能直接读取所有用户的凭证。
六十多年后的今天,密码存储已经从"不能明文存储"进化到"不能用普通哈希函数",再到"必须使用内存硬函数"。这场攻防博弈的背后,是计算能力的指数级增长与密码学设计的持续对抗。
Salt的诞生:打破彩虹表的前奏
1979年,贝尔实验室的Robert Morris和Ken Thompson发表了题为《Password Security: A Case History》的论文。这篇仅6页的文献奠定了现代密码存储的基础。他们当时面临的问题是:Unix系统使用DES算法加密密码,但相同的密码总是产生相同的密文——攻击者可以预先计算一个字典,然后直接查表匹配。
Morris和Thompson的解决方案是一个被称为"salt"的12位随机数。在计算密码哈希时,salt被混入密码中,使得相同的密码产生不同的哈希值。这意味着攻击者需要为每一个可能的salt值(4096种)单独准备一套预计算表,将攻击成本提升了4096倍。
Salt的意义不仅在于技术实现,更在于它引入了一个核心思想:密码存储的安全性不能依赖于保密,而应该依赖于计算成本。Salt本身是公开存储的,攻击者知道它的值,但无法利用预计算的优势。
彩虹表:时间与空间的博弈
1980年,Martin Hellman发表了《A cryptanalytic time-memory trade-off》,提出了一种在暴力破解和预计算表之间折中的攻击方法。2003年,Philippe Oechslin将其改进为"彩虹表"(Rainbow Table),大幅提升了攻击效率。
彩虹表的核心思想是:不存储所有可能的"密码→哈希"映射,而是存储"哈希链"。通过定义一组约简函数(reduction function),将哈希值映射回可能的密码空间:
sequenceDiagram
participant P as 明文密码
participant H as 哈希函数
participant R as 约简函数
Note over P,R: 哈希链示例
P->>H: password123
H->>R: 482c811da5d5b4bc6d497ffa98491e38
R->>H: qwerty01
H->>R: 8522b37cff95f1a1...
R->>H: abc456
存储时只记录链的起点和终点。攻击时,从目标哈希出发,反复应用约简函数和哈希函数,检查是否命中某个终点。如果命中,从对应起点重新计算链条,找到原始密码。
彩虹表对无盐哈希极为有效。2012年LinkedIn泄露的650万密码哈希(使用无盐SHA-1)在几天内被大量破解。但如果每个密码都有128位的随机salt,攻击者就需要为每个用户单独生成彩虹表——这在计算上完全不可行。
Bcrypt:可调节的慢速哈希
1999年,Niels Provos和David Mazières在USENIX会议上发表了《A Future-Adaptable Password Scheme》。他们注意到一个关键问题:随着计算能力的提升,固定复杂度的哈希函数会变得越来越不安全。
Bcrypt的设计基于Blowfish密码算法的一个独特特性——昂贵的密钥设置(expensive key setup)。Blowfish的密钥初始化需要执行多轮加密,将密钥材料"搅拌"进内部状态。Provos和Mazières利用这一点,设计了一个"可调节慢度"的哈希函数:
bcrypt(cost, salt, password):
state = EksBlowfishSetup(password, salt, cost)
ciphertext = "OrpheanBeholderScryDoubt"
repeat 64 times:
ciphertext = Encrypt(state, ciphertext)
return cost + salt + ciphertext
其中cost参数决定了密钥设置的迭代次数:2^cost轮。cost=10意味着1024轮,cost=12意味着4096轮。当硬件变快时,只需提高cost值就能保持相同的安全边际。
Bcrypt还内置了128位的salt,自动处理了彩虹表防御。它的输出格式是自描述的:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
$2b$:算法标识符(bcrypt)12:cost参数(2^12 = 4096轮)R9h/cIPz0gi.URNNX3kh2O:base64编码的128位saltPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW:base64编码的哈希值
Bcrypt的局限性
Bcrypt并非完美。它有两个主要缺陷:
密码长度限制:Bcrypt最多处理72字节的密码。超过这个长度的部分会被截断。这在对Unicode编码的密码处理中可能产生意想不到的问题——一个18字符的字符串如果每个字符都需要4字节UTF-8编码,就刚好达到72字节上限。
固定的内存需求:Bcrypt只需要约4KB内存。这个数字在1999年是合理的,但今天的GPU可以轻松并行处理大量这样的计算。一台RTX 4090在cost=5基准测试下每秒可计算约18万次bcrypt——虽然仍然比SHA-256慢得多,但随着cost参数的提高,破解成本仍然可控。
Scrypt:内存硬函数的开端
2009年,Colin Percival发表了《Stronger Key Derivation via Sequential Memory-Hard Functions》。他的洞察是:如果攻击者能用专用硬件(GPU、FPGA、ASIC)加速计算,那么单纯的CPU时间复杂度就不再是可靠的安全指标。
Percival提出了"内存硬函数"(Memory-Hard Function)的概念:一种需要大量内存才能高效计算的函数。由于内存的带宽和延迟限制难以通过并行化绕过,内存硬函数能有效抵抗专用硬件攻击。
Scrypt的设计包含三个参数:
| 参数 | 作用 | 影响 |
|---|---|---|
| N | CPU/内存成本参数 | 内存使用量 = 128 × N × r 字节 |
| r | 块大小参数 | 控制每次内存访问的数据量 |
| p | 并行化参数 | 独立运行的实例数量 |
内存计算公式:内存 = 128 × N × r 字节
当N=16384(2^14)、r=8时,scrypt需要约16MB内存。当N=1048576(2^20)时,内存需求达到1GB。这意味着攻击者即使拥有大量GPU,每个GPU核心也只能同时处理有限的哈希计算——因为显存是有限的。
graph LR
subgraph Scrypt内存访问模式
A[填充内存] --> B[随机访问]
B --> C[顺序XOR]
C --> D[最终PBKDF2]
end
subgraph 攻击者困境
E[大量并行核心] --> F[但显存有限]
F --> G[无法同时处理]
end
从Scrypt到Argon2:密码哈希竞赛
2013年,密码学界发起了一场"密码哈希竞赛"(Password Hashing Competition,PHC)。2015年7月,Argon2从24个参赛算法中脱颖而出,成为最终获胜者。
Argon2有两个主要变体:
Argon2d:数据依赖的内存访问。访问模式取决于输入数据,提供最强的GPU/ASIC抵抗能力,但可能泄露一些信息给侧信道攻击者。
Argon2i:数据无关的内存访问。访问模式是预先确定的,提供最强的侧信道抵抗能力,但GPU抵抗能力略弱。
Argon2id:混合模式。前半部分使用数据无关访问,后半部分使用数据依赖访问。这是目前OWASP推荐的首选方案。
Argon2的参数设计更加灵活:
| 参数 | 推荐值(OWASP) | 作用 |
|---|---|---|
| 内存(m) | ≥19 MiB | 控制内存使用量 |
| 迭代次数(t) | ≥2 | 控制CPU时间 |
| 并行度(p) | 1 | 控制线程数 |
| 输出长度 | ≥32字节 | 哈希值长度 |
2022年,Argon2被正式标准化为RFC 9106。标准推荐的参数是:内存2048 MiB、迭代次数4、并行度8——这是针对高强度安全场景的配置。对于交互式登录场景,OWASP建议至少19 MiB内存、2次迭代、并行度1。
攻击者的武器:从CPU到GPU再到FPGA
理解密码哈希的演进,需要理解攻击者的能力增长。
GPU时代:并行化的威力
现代GPU拥有数千个计算核心。RTX 4090有16384个CUDA核心,能够以惊人的速度并行计算哈希值。以下是基于hashcat benchmark的实测数据:
| 算法 | RTX 4090 哈希速率 | 相对安全评估 |
|---|---|---|
| MD5 | 约1640亿/秒 | 极不安全 |
| SHA-256 | 约220亿/秒 | 不适合密码存储 |
| SHA-512 | 约75亿/秒 | 不适合密码存储 |
| bcrypt (cost=5) | 约18万/秒 | 可接受(需更高cost) |
这些数字解释了为什么MD5和SHA系列不适合密码存储——它们的速度太快了。一个8位小写字母密码(约2000亿种组合)在RTX 4090上使用MD5只需要约1秒就能穷举完毕。
值得注意的是,bcrypt的cost参数呈指数级增长:cost每增加1,计算时间翻倍。cost=10比cost=5慢32倍,cost=12比cost=5慢128倍。因此,使用cost=12时,RTX 4090每秒只能计算约1400次bcrypt——这才是实际部署中应该使用的安全参数。
FPGA与ASIC:专业化的威胁
2020年,安全研究人员展示了使用FPGA阵列破解bcrypt的能力。一个4U高度的FPGA集群可以在585瓦功耗下实现每秒210万次bcrypt计算——比GPU能效高45倍以上。
FPGA(现场可编程门阵列)的优势在于可以针对特定算法优化电路。与GPU不同,FPGA不需要处理通用计算的开销,每个逻辑门都可以专用于哈希计算。对于固定内存需求4KB的bcrypt,FPGA可以轻松集成大量并行实例。
这就是为什么内存硬函数如此重要:当每个哈希计算需要64MB甚至1GB内存时,专用硬件的效率优势就被内存带宽瓶颈抵消了。在显存或内存带宽饱和之前,增加更多计算核心并不能提升破解速度。
实践指南:如何选择和配置
场景一:新项目,无历史包袱
首选:Argon2id
# Python示例(使用argon2-cffi库)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2, # 迭代次数
memory_cost=19456, # 19 MiB (以KB为单位)
parallelism=1, # 并行度
hash_len=32, # 输出长度
salt_len=16 # salt长度
)
# 存储密码
hash = ph.hash("user_password")
# 验证密码
try:
ph.verify(hash, "user_password")
except:
# 验证失败
pass
场景二:需要兼容性或资源受限
备选:bcrypt
如果Argon2在目标环境中不可用,或者内存资源非常紧张,bcrypt仍然是安全的选择。建议使用cost=12或更高:
// Node.js示例(使用bcrypt库)
const bcrypt = require('bcrypt');
const saltRounds = 12; // cost = 12,即4096轮
// 存储密码
const hash = await bcrypt.hash(password, saltRounds);
// 验证密码
const match = await bcrypt.compare(password, hash);
场景三:遗留系统迁移
如果现有系统使用PBKDF2或其他较弱的算法,应该规划迁移到Argon2id。迁移策略:
- 在用户下次登录时,用新算法重新哈希密码
- 在数据库中存储算法标识符,支持多版本并存
- 逐步淘汰旧算法
# 迁移期间的密码记录格式示例
$argon2id$v=19$m=19456,t=2,p=1$... # 新用户
$pbkdf2-sha256$i=100000$... # 待迁移用户
常见错误与陷阱
错误一:使用快速哈希函数
# 错误示例:不要这样存储密码
import hashlib
hash = hashlib.sha256(password.encode()).hexdigest() # 危险!
SHA-256是为数据完整性设计的,其设计目标是"快速"。一个RTX 4090每秒可以计算约220亿次SHA-256——这意味着8位密码在毫秒级就能被穷举。
错误二:自己实现加密
永远不要自己编写密码哈希算法。使用经过严格审查的库:
| 语言 | 推荐库 |
|---|---|
| Python | argon2-cffi, passlib |
| Node.js | argon2, bcrypt |
| Java | Argon2-JVM, jBCrypt |
| Go | golang.org/x/crypto/argon2 |
| Rust | argon2 crate, bcrypt crate |
错误三:忽略时间常数比较
在验证密码时,必须使用时间常数比较函数,防止计时攻击:
# 错误示例
if stored_hash == computed_hash: # 可能泄露信息
pass
# 正确示例
import hmac
if hmac.compare_digest(stored_hash, computed_hash):
pass
从历史看未来
回顾六十年密码存储的演进,可以看到清晰的脉络:攻击者的能力在增长,防御者的手段也在进化。
| 年代 | 主流方案 | 攻击威胁 | 防御思路 |
|---|---|---|---|
| 1960s | 明文存储 | 数据库泄露 | 物理隔离 |
| 1970s | DES crypt | 预计算攻击 | Salt机制 |
| 1990s | MD5/SHA | 彩虹表 | 多轮迭代 |
| 2000s | bcrypt | GPU加速 | 可调参数 |
| 2010s | scrypt/Argon2 | FPGA/ASIC | 内存硬函数 |
未来会怎样?量子计算对对称密码的影响有限——Grover算法最多将安全强度减半,而密码哈希属于对称密码范畴。更大的威胁可能来自计算成本的持续下降——今天的"安全参数"在十年后可能就显得不足了。
这提醒我们:密码存储不是"一次性配置"的工作,而是需要持续评估和更新的安全实践。定期审视参数配置、关注密码学社区的最新建议、规划迁移路径——这些都是负责任的安全实践。
密码存储的安全不在于选择"最强"的算法,而在于理解威胁模型、选择合适的工具、正确地配置参数、并保持对技术演进的关注。这场攻防博弈还将继续,但至少现在,我们知道如何让攻击者的成本保持在可接受的范围内。
参考资料
- Morris, R., & Thompson, K. (1979). Password Security: A Case History. Bell Laboratories.
- Provos, N., & Mazières, D. (1999). A Future-Adaptable Password Scheme. USENIX Annual Technical Conference.
- Percival, C. (2009). Stronger Key Derivation via Sequential Memory-Hard Functions. BSDCan.
- Biryukov, A., Dinu, D., & Khovratovich, D. (2016). Argon2: the memory-hard function for password hashing. Password Hashing Competition.
- Oechslin, P. (2003). Making a Faster Cryptanalytic Time-Memory Trade-Off. CRYPTO 2003.
- RFC 9106: Argon2 Memory-Hard Function for Password Hashing (2022).
- OWASP Password Storage Cheat Sheet (2024).
- NIST Special Publication 800-63B: Digital Identity Guidelines (2024).
- Hellman, M. (1980). A Cryptanalytic Time-Memory Trade-Off. IEEE Transactions on Information Theory.
- Wiemer, F., & Zimmermann, R. (2016). High-speed implementation of bcrypt password search using special-purpose hardware. Ruhr-University Bochum.
- Chick3nman. (2022). Hashcat v6.2.6 benchmark on the Nvidia RTX 4090. GitHub Gist.