一句话概括
像 bcrypt 和 Argon2 这样的密码哈希算法,会将用户密码转换成一长串不可逆的字符串。它们的设计初衷就是让计算过程变得缓慢且昂贵,这样即使攻击者偷走了你的整个用户数据库,也很难破解出原始密码。
它解决了什么问题
在数字世界的黑暗时代,开发者们直接用明文存储用户密码。如果黑客黑进了数据库,他们就能拿到一个包含所有用户名和对应密码的文件。直接 Game over。你可以想象那场面有多惨烈。
第一代进步是“哈希”。你不再存储 P@ssword123,而是存储它的哈希值——一个由 MD5 或 SHA-1 等算法生成的固定长度字符串。当用户登录时,你把他输入的密码哈希一下,看看是否与存储的哈希值匹配。这好了一点,但它有一个致命缺陷:彩虹表(rainbow tables)。因为哈希函数对相同的输入总是产生相同的输出(比如,password 用 MD5 哈希永远是 5f4dcc3b5aa765d61d8327deb882cf99),攻击者就预先计算了数十亿个常用密码的哈希值,并把它们存在一个巨大的查询表里。一个被盗的哈希值可以在毫秒内被反查出原始密码。
下一次进化是“加盐(salting)”。在哈希密码之前,你先给它加上一个唯一的、随机的字符串,称为“盐(salt)”。所以你哈希的不再是 P@ssword123,而是 P@ssword123 + aBcDeFg12345。这个盐会和哈希值一起存储在数据库里。这招彻底废掉了彩虹表,因为现在每个用户的哈希值都是独一无二的,哪怕他们都用了“password”这个密码。攻击者必须为每一个用户的唯一盐值生成一个新的彩虹表,这在计算上是不可行的。
但是,一个新的反派登场了:纯粹的计算能力。现代的 GPU 和定制硬件(ASIC)每秒可以针对一个被盗的哈希值尝试数十亿个加盐密码组合。像 SHA-256 这样的哈希算法在设计上就是快的,这对于验证文件完整性来说很棒,但对于保护密码来说简直是灾难。
这就是 bcrypt 和 Argon2 为解决这个问题而生的原因。它们是密码哈希函数(Password Hashing Functions, PHF),一类特殊的哈希算法,被故意设计得极其缓慢且消耗资源。它们使得哪怕只验证一个密码的成本都非常高昂,从而让攻击者在暴力破解被盗数据库时陷入经济和时间上的噩梦。
底层工作原理
现代密码哈希的魔法不仅仅在于算法本身,更在于让这个算法执行可配置的“工作量”。这通常被称为“密钥拉伸(key stretching)”。
核心思想:自适应成本
Bcrypt 和 Argon2 都有可调节的“成本”或“工作”因子。这是它们的超能力。你可以设置一个数字,来决定哈希过程要多慢、多消耗资源。核心思想是,将这个成本调整到在你服务器上哈希一个密码需要一个合理的时间(比如 100-500 毫秒)——慢到足以让攻击者抓狂,但又快到不至于让你的用户烦躁。
随着摩尔定律让计算机变得越来越快,你只需每隔几年增加这个成本因子,就能维持同等级别的安全性。当攻击者的硬件变强时,你的哈希“训练强度”也随之增加。
Bcrypt:久经考验的老将
Bcrypt 于 1999 年推出,基于 Blowfish 加密算法。它对抗暴力破解的主要防御手段是,它不仅计算密集,而且内存密集。
该算法会重复处理密码和盐,但其内部状态的一个关键部分以一种需要频繁访问一块 RAM 的方式存储。虽然 CPU 很擅长这个,但攻击者的首选武器——GPU——更喜欢简单、可并行的计算,且内存访问要少。通过强迫计算过程不断地访问内存,bcrypt 给那些基于 GPU 的破解设备狠狠地使了个绊子。
一个典型的 bcrypt 哈希字符串看起来像这样:
$2b$12$J9yLsf9dTOZiCEpT1zO97uF2Yp3uG/yG4Q6V.C1rCg8u6d5qO5GqS
我们来分解一下:
| 部分 | 示例 | 含义 |
|---|---|---|
| 算法 | $2b |
标识 bcrypt 算法和版本。 |
| 成本因子 | 12 |
工作因子。迭代次数是 212。这个值加 1,时间就翻倍。 |
| 盐 & 哈希值 | J9yLsf...d5qO5GqS |
前 22 个字符是 Base64 编码的盐。剩下的部分是实际的密码哈希摘要。 |
关键在于,验证密码所需的所有信息——算法、成本和盐——都方便地存储在这一个字符串里。
Argon2:新科冠军
Argon2 是 2015 年密码哈希竞赛(Password Hashing Competition, PHC)的获胜者,这场竞赛历时数年,旨在寻找一个新的、标准化的密码哈希算法。它的设计目标是比 bcrypt 更能抵抗现代硬件攻击。
它提供了三个不同的杠杆来调整其成本:
- 内存成本 (m): 算法将使用的内存量(单位 KiB)。这是它对抗硬件加速的主要防御手段,因为可以将其调整为需要比单个 GPU 核心可用内存更多的内存。
- 时间成本 (t): 算法对内存进行操作的迭代次数或“遍数”。这会增加总计算时间。
- 并行度 (p): 可以使用的线程数或“通道”数。这使得算法可以利用多核 CPU 进行哈希,但这也意味着攻击者可以使用相同的并行度。这是一个需要权衡的参数。
推荐的变体是 Argon2id,它是另外两个版本的混合体。它对旁道攻击(side-channel attacks,攻击者通过分析功耗等系统影响来进行攻击)和 GPU 破解攻击都提供了均衡的抵抗力。
一个 Argon2id 哈希字符串同样是自包含的:
$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTIT0T7cKLfgflAIk124
| 部分 | 示例 | 含义 |
|---|---|---|
| 算法 | $argon2id |
标识 Argon2id 算法。 |
| 版本 | v=19 |
算法版本号。 |
| 参数 | m=65536,t=3,p=4 |
内存、时间和并行度成本。 |
| 盐 | c29tZXNhbHQ |
Base64 编码的随机盐。(这里解码出来是 "somesalt")。 |
| 哈希值 | Rdescudv...flAIk124 |
Base64 编码的密码哈希摘要。 |
验证过程
这才是最美妙的部分。你永远不会去解密一个哈希值。验证是一条单行道。
- 用户尝试用
P@ssword123登录。 - 你的应用从数据库中检索出该用户的完整哈希字符串(例如,上面表格中的那个 Argon2id 字符串)。
- 你的代码解析该字符串,提取出算法(
argon2id)、版本(19)、成本因子(m=65536, t=3, p=4)和盐(c29tZXNhbHQ)。 - 然后,它用用户刚刚输入的密码(
P@ssword123)和完全相同的参数来哈希一遍。 - 最后,它将新生成的哈希值与从数据库中取出的字符串的哈希部分进行比较。如果它们完全相同,密码正确。否则,拒绝访问。
真实世界的故事
2012 年 LinkedIn 泄露事件:关于速度的教训
2012 年,LinkedIn 遭受了一次大规模泄露,超过 650 万用户密码的哈希值被泄露到一个俄罗斯黑客论坛上。问题出在哪?这些哈希是未加盐的 SHA-1。在数小时内,安全爱好者和黑帽黑客们展开了一场破解竞赛。成功破解的密码列表每分钟都在增长。因为这些哈希是“快速”且未加盐的,一个巨大的彩虹表就足以搞定一切。声誉损失是巨大的。
教训: 一个快速、未加盐的哈希几乎不比明文好多少。LinkedIn 泄露事件是给整个行业敲响的一记警钟,推动了无数公司最终采用像 bcrypt 这样缓慢、加盐的哈希方案。
为自己争取时间的电商网站
一家名为 "CircuitCraze" 的初创公司的精明开发者坚持在新客户数据库中使用 Argon2id,并将参数调整到每次登录大约耗时 300 毫秒。一年后,一个配置错误的云存储桶暴露了一个数据库备份。包含所有 Argon2id 哈希的 users 表被攻击者下载了。团队立即强制所有用户重置密码。攻击者面对着要花费数千美元的云计算资源,仅仅为了从一个已经失效的数据集中破解少数几个密码的前景,干脆放弃并转向了更容易得手的目标。
教训: 强大的密码哈希是你在泄露发生后的主要防线。它不能阻止泄露,但它通过为你争取宝贵的反应时间,将一场灾难性的数据泄露变成一个可控的事件。
成本因子的“蠕变”
一个团队在 2015 年推出了一个新的 Web 应用,明智地选择了 bcrypt,成本因子为 10。当时效果很好。快进到今天。摩尔定律发挥了它的作用,成本因子为 10 现在被认为是弱的。一次安全审计将其标记为高优先级风险。团队现在面临一个两难的境地:如何在不强制所有用户重置密码的情况下,为数百万用户升级安全性?优雅的解决方案是:当用户下一次登录时,系统使用旧的 cost-10 哈希验证他们的密码。如果正确,系统立即用新的 cost-13 重新哈希相同的密码,并替换掉数据库中的旧哈希。
教训: 密码安全不是“一次设置,终身无忧”的。成本因子是一个动态的设置,必须定期审查和提高,以跟上攻击者能力的发展。
常见错误和陷阱
- 使用快速哈希。 千万别。MD5, SHA-1, SHA-256, SHA-512……它们是用于校验和的,不是用于密码的。它们的设计初衷就是快,而这恰恰是你最不想要的。
- 自己造加密轮子。 永远不要尝试发明你自己的哈希方案,比如
sha512(password + salt)。使用一个标准的、经过同行评审的、经过实战考验的 bcrypt 或 Argon2 库,它们能把事情做对。 - 使用静态或重用的盐。 盐必须是密码学安全的随机值,并且为你存储的每个密码都是唯一的。为每个哈希存储一个唯一的盐是正确且标准的做法。
- 成本设置得太低。 在 2024 年,一个低于 12 的 bcrypt 成本因子或过弱的 Argon2 参数会使整个方案的目的落空。你只是在给攻击者放水。把它调到你能容忍的最慢速度。
- 成本设置得太高。 如果哈希需要 10 秒,你的登录表单会超时并且感觉像是坏了。更糟的是,你把你的服务器暴露在拒绝服务(DoS)攻击之下,攻击者只需不断向登录表单发请求就能让你的 CPU 瘫痪。
- 只存储哈希摘要。 bcrypt 和 Argon2 生成的字符串包含算法、成本和盐是有原因的。你必须存储那个完整的字符串(例如,在一个
VARCHAR(255)列中),以便将来能够验证密码。
为什么它值得你关注
如果你的工作涉及到构建任何带有登录表单的东西,你就需要理解这个。
- 后端 & 全栈开发者: 这是你的直接责任。搞砸了这一点,你就会把你的每一个用户都置于风险之中。
- 安全工程师 & DevOps: 你们是制定策略、执行审计和管理基础设施的人。你需要知道什么是“好”的标准,以及如何测试它。
- 前端开发者: 理解一个安全的登录可能需要几百毫秒,这解释了为什么你需要提供用户反馈(比如一个加载中的 spinner),以防止他们狂点“登录”按钮。
简而言之,如果“用户”、“账户”或“密码”这些词出现在你项目需求的任何地方,那么密码哈希就是你绝对不能忽视的一个基本概念。
深入了解
- OWASP Password Storage Cheat Sheet — 为开发者准备的、必不可少的权威指南。
- Argon2 Specification — 描述 Argon2 工作原理的正式论文。
- How To Safely Store A Password by Coda Hale — 一篇经典的 2010 年博文,为一代开发者解释了“为什么”要用 bcrypt。
- Bcrypt on Wikipedia — 对该算法历史和设计的扎实概述。
- Argon2 on Wikipedia — 关于密码哈希竞赛和 Argon2 设计的背景知识。
- Password Hashing Competition — 为 Argon2 加冕的竞赛的官方网站。