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位salt
  • PST9/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。迁移策略:

  1. 在用户下次登录时,用新算法重新哈希密码
  2. 在数据库中存储算法标识符,支持多版本并存
  3. 逐步淘汰旧算法
# 迁移期间的密码记录格式示例
$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算法最多将安全强度减半,而密码哈希属于对称密码范畴。更大的威胁可能来自计算成本的持续下降——今天的"安全参数"在十年后可能就显得不足了。

这提醒我们:密码存储不是"一次性配置"的工作,而是需要持续评估和更新的安全实践。定期审视参数配置、关注密码学社区的最新建议、规划迁移路径——这些都是负责任的安全实践。

密码存储的安全不在于选择"最强"的算法,而在于理解威胁模型、选择合适的工具、正确地配置参数、并保持对技术演进的关注。这场攻防博弈还将继续,但至少现在,我们知道如何让攻击者的成本保持在可接受的范围内。


参考资料

  1. Morris, R., & Thompson, K. (1979). Password Security: A Case History. Bell Laboratories.
  2. Provos, N., & Mazières, D. (1999). A Future-Adaptable Password Scheme. USENIX Annual Technical Conference.
  3. Percival, C. (2009). Stronger Key Derivation via Sequential Memory-Hard Functions. BSDCan.
  4. Biryukov, A., Dinu, D., & Khovratovich, D. (2016). Argon2: the memory-hard function for password hashing. Password Hashing Competition.
  5. Oechslin, P. (2003). Making a Faster Cryptanalytic Time-Memory Trade-Off. CRYPTO 2003.
  6. RFC 9106: Argon2 Memory-Hard Function for Password Hashing (2022).
  7. OWASP Password Storage Cheat Sheet (2024).
  8. NIST Special Publication 800-63B: Digital Identity Guidelines (2024).
  9. Hellman, M. (1980). A Cryptanalytic Time-Memory Trade-Off. IEEE Transactions on Information Theory.
  10. Wiemer, F., & Zimmermann, R. (2016). High-speed implementation of bcrypt password search using special-purpose hardware. Ruhr-University Bochum.
  11. Chick3nman. (2022). Hashcat v6.2.6 benchmark on the Nvidia RTX 4090. GitHub Gist.