哈希 vs 加密:常见误区与正确用法
深入理解哈希与加密的本质区别(单向 vs 双向)、为什么密码必须哈希不能加密、bcrypt/argon2 加盐与慢哈希、HMAC 防篡改、SHA-256 vs SHA-3、彩虹表与 Salt、应用错配(MD5 存密码/AES 做指纹)。
"我们用 AES 加密密码存在数据库" "用 MD5 做文件完整性检查" "这个 API 用 SHA-256 签名,很安全"。这些说法看似懂密码学,其实满是错误。哈希和加密是两个完全不同的概念,混淆它们会导致严重的安全漏洞。本文将深入探讨这两个概念的本质区别、常见误区、为什么密码必须哈希而非加密、现代密码存储最佳实践(bcrypt/argon2)、如何用盐值和 HMAC 防御攻击,以及过时算法(MD5、SHA-1)为什么已不可用。
1. 哈希与加密的本质区别
这两个概念经常被混淆,但它们在安全属性上是完全对立的。让我们用一个简单的类比:
加密:可逆的密钥保护
加密像是一把安全门锁。你用钥匙(密钥)锁上,收件人用相同或配对的钥匙解锁。
- 明文 + 密钥 → 加密 → 密文
- 密文 + 密钥 → 解密 → 原始明文
- 例子:AES、RSA、ChaCha20
- 应用:通信保密、文件加密、数据库字段加密
关键特性: 可逆、确定性(相同输入+密钥 = 相同输出)、需要密钥管理。
哈希:不可逆的指纹
哈希像是一个指纹识别系统。输入信息被转换成一个固定长度的"指纹",无法从指纹还原出原始信息。
- 输入数据 → 哈希 → 哈希值(指纹)
- 无法从哈希值逆向得到原始数据
- 例子:SHA-256、SHA-3、MD5(已过时)
- 应用:密码存储、文件完整性检查、区块链、数字签名
关键特性: 不可逆、确定性、快速、碰撞概率极低。
对比表
| 特性 | 加密 | 哈希 |
|---|---|---|
| 可逆性 | 可逆(用密钥) | 不可逆 |
| 需要密钥 | 是 | 否 |
| 速度 | 中等(毫秒) | 快(纳秒)或慢(毫秒/密码) |
| 输出长度 | 可变(等于输入) | 固定(256bit、512bit 等) |
| 确定性 | 可选(可用 IV/盐) | 通常确定(除密钥相关) |
2. 单向函数:为什么哈希无法逆向
单向函数的数学性质
哈希函数是一个单向函数(One-way Function),具有以下性质:
- 正向容易 — 给定输入 X,计算 H(X) 很快(纳秒级)
- 逆向困难 — 给定哈希值 H,找到任何输入 X 使得 H(X) 等于给定值是不可行的
- 碰撞稀有 — 找到两个不同输入产生相同哈希(碰撞)极其困难
为什么不可逆?因为哈希涉及信息损失。从 512 位输入压缩到 256 位输出,本质上丢弃了一半的信息。即使尝试所有可能的输入,也无法确定哪个产生了特定的哈希。
实际例子:SHA-256
SHA-256 哈希函数如下:
输入:"password"
SHA-256 哈希:5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
输入:"password123"
SHA-256 哈希:ef797c8d8f992f5b5614653fc3740ce5cb20963d2c0dda6df10d4afab0aeff59c
即使你看到哈希值 `5e884898...`,也不可能通过逆向计算还原出 "password"。唯一的方法是尝试所有可能的输入,看是否能匹配这个哈希。对于强密码,这需要数十亿年。
为什么加密不适合密码存储
如果用加密存储密码:
密码存储(错误方法):
密钥 K = "secretkey123"
用户密码 P = "user_password"
加密后存储:密文 C = AES_Encrypt(P, K)
问题:如果数据库泄露,黑客获得 K 和 C,立即可解密得到 P。
相比哈希存储:
密码存储(正确方法):
用户密码 P = "user_password"
哈希后存储:H = bcrypt(P) = "$2b$12$..."
问题:黑客即使获得 H,也无法逆向得到 P。只能暴力尝试。
3. 密码存储:为什么必须哈希不能加密
从零开始理解密码验证流程
用户注册时,系统需要存储密码以供后续登录验证。但如何安全地存储是核心问题。
方案 1:明文存储(灾难)
最不安全。数据库一泄露,所有密码都暴露。现实中曾有大量网站这样做,结果造成巨大安全事故。
方案 2:用加密存储(错误)
注册:密文 = AES_Encrypt("my_password", serverKey)
登录:接收用户输入,加密后与数据库密文比较。
问题:serverKey 必须存在服务器上。如果服务器被攻破或有内部人员作恶,获得密钥即可解密所有密码。
方案 3:用快速哈希(不够好)
注册:存储 hash = SHA-256("my_password")
登录:哈希用户输入,与存储的哈希比较。
改进:只有知道原密码才能验证成功(因为哈希无法逆向)。
问题:SHA-256 太快(纳秒级)。黑客可用暴力破解尝试数十亿个密码,在短时间内找到匹配。常见密码(如 "123456"、"password")有预计算的哈希值(彩虹表),查表即可。
方案 4:用慢哈希 + 盐值(推荐)
注册:
salt = random(16 bytes)
hash = bcrypt("my_password", cost=12, salt=salt)
store: (hash, salt)
登录:
input_hash = bcrypt(user_input, cost=12, salt=stored_salt)
if input_hash == stored_hash: login success
优势:
- bcrypt 很慢(cost=12 约 100ms),降低暴力破解速度
- 随机盐值确保相同密码产生不同哈希,彩虹表失效
- 即使密码被猜中,验证速度也只是 100ms,用户几乎不感觉
4. 盐值与彩虹表攻击
彩虹表:预计算哈希表
彩虹表是攻击者预先计算的哈希表。它包含常见密码及其哈希值:
密码 | SHA-256 哈希
--- | ---
123456 | 8d969eef6ecad3c29a3a873fba5d659b32fdb7f7
password | 5e884898da28047151d0e56f8dc6292773...
qwerty | d0303db1c62fe41f69fa3ee06b4e73c0e3a...
… (数十亿条记录)
黑客从泄露的数据库中取出哈希,只需查表就能迅速找到对应密码。预计算表通常几个 GB,包含常见的数十亿个密码,查询极快。
盐值的防御原理
盐值(Salt)是在哈希前混入的随机数据。关键是每个用户使用不同的盐:
用户 A:
盐 = "a1b2c3d4..."
密码 = "password"
hash = SHA-256("password" + "a1b2c3d4...") = "xyz..."
用户 B:
盐 = "e5f6g7h8..."
密码 = "password"(相同)
hash = SHA-256("password" + "e5f6g7h8...") = "abc..."(完全不同)
即使两个用户密码相同,盐值不同,最终哈希也完全不同。彩虹表失效,因为表中没有考虑这么多种盐的组合。
盐值的最佳实践
- 长度 — 至少 16 字节(128 比特)
- 随机性 — 用密码学安全的随机生成器
- 唯一性 — 每个密码都应使用不同的盐
- 存储 — 盐可明文存储(与哈希一起),它的作用是增加彩虹表的规模
好消息:bcrypt 和 argon2 都自动处理盐值生成和存储。开发者只需调用函数,不用手动管理。
5. bcrypt 和 argon2:慢哈希的力量
为什么慢哈希更安全?
快速哈希(SHA-256)设计用来哈希大量数据,速度是目标。但对密码存储,快就成了弱点——黑客可快速尝试数十亿个猜测。
慢哈希故意设计得慢:每次计算需要 100ms~1s。这对真实用户几乎无感(只登录一次),但对黑客破坏性极强:尝试 1 亿个密码需要 3000 年。
bcrypt:业界标准(25 年)
bcrypt 由 Niels Provos 和 David Mazières 在 1999 年设计,基于 Blowfish 加密算法。至今仍是最广泛使用的密码哈希方案。
Hash 格式:$2b$12$R9h/cIPz0gi.URNNGHQ1ve0dgaAZeKqtxWzwvyWI5SE.tmfGvSxy.
字段含义:
$2b$ — bcrypt 算法标识
$12$ — 工作因子(cost)= 12(2^12 轮)
R9h/cIPz... — 盐值(22 字符)+ 哈希值
关键参数:Cost(工作因子)
- Cost = 10 → ~10ms
- Cost = 12 → ~100ms
- Cost = 14 → ~1s
- 每增加 1,时间翻倍
推荐: 新应用使用 Cost = 12。如果需要额外安全,随时可升至 14(已有哈希不需重算)。
argon2:新一代标准(2015)
argon2 在 2015 年赢得 Password Hashing Competition。相比 bcrypt,它更灵活、参数更多、抗 GPU 攻击更强。
参数:
- Time cost — 迭代次数(多次通过)
- Memory cost — 内存使用量(MB)
- Parallelism — 并行度
优势:
- 内存硬度(Memory-hard)— 不仅吃 CPU,也吃内存,更难被 GPU 加速
- 三种变体 — Argon2i(抗 GPU)、Argon2d(快速)、Argon2id(混合)
- 参数灵活 — 可独立调整时间、内存、并行度
推荐配置(2026):
Time cost = 2
Memory cost = 65536 KB (64 MB)
Parallelism = 4
预期耗时:~500ms
bcrypt vs argon2:选择建议
- bcrypt — 成熟、库支持好、足够安全。适合大多数应用。
- argon2 — 更新、参数丰富、抗硬件加速更强。适合超高安全需求(银行、军事)。
6. HMAC:消息认证与完整性校验
问题:纯哈希无法验证来源
假设服务器发送一条消息及其 SHA-256 哈希:
消息:"转账 1000 元给 Alice"
哈希:SHA-256(消息) = "abc123..."
问题:中间人可以同时修改消息和哈希。
攻击者修改为:"转账 1000 元给 Bob"
重新计算哈希:SHA-256(新消息) = "def456..."
接收者收到新的消息和新的哈希,验证通过(虽然内容改了)
HMAC 的解决方案
HMAC(Hash-based Message Authentication Code)使用密钥和哈希函数组合,确保来源真实且内容未被篡改。
secret_key = "server_secret_key_12345"
message = "转账 1000 元给 Alice"
HMAC = HMAC-SHA-256(secret_key, message)
= "xyz789..."
发送:(message, HMAC)
关键:攻击者没有 secret_key,无法计算正确的 HMAC。即使修改消息,新的 HMAC 也是错的。
HMAC 的应用场景
- API 签名 — AWS、Stripe 等 API 都用 HMAC 签名请求
- Cookie 防篡改 — web 框架用 HMAC 保护 session 数据
- 日志完整性 — 确保日志在传输和存储中未被修改
- 消息队列 — RabbitMQ、Kafka 等用 HMAC 验证消息真实性
HMAC vs 数字签名
两者都能验证来源和完整性,但不同:
- HMAC — 对称(双方共享密钥),适合内部系统通信
- 数字签名 — 非对称(私钥签名,公钥验证),适合公开验证
7. SHA-256 vs SHA-3:算法演进
SHA-256(SHA-2 家族)
SHA(Secure Hash Algorithm)由美国 NSA 设计,发表于 1993 年。SHA-256 是 SHA-2 系列中最常用的版本。
- 输出长度 — 256 比特(32 字节)
- 安全强度 — 128 比特(时间成本 2^128)
- 速度 — 快(现代 CPU 可达 GB/s)
- 使用场景 — 文件完整性检查、区块链、TLS、一般哈希
至今仍被广泛使用,安全性已被 30 年的实践验证。
SHA-3(最新标准)
2015 年,NIST 采纳 SHA-3 为新的标准哈希算法。基于 Keccak 算法,采用完全不同的数学结构(海绵函数)。
- 输出长度 — 256 比特(32 字节)或更长
- 安全强度 — 128+ 比特
- 速度 — 中等(比 SHA-256 略慢)
- 特点 — 对位翻转、边界情况更鲁棒
SHA-2 vs SHA-3 对比
| 特性 | SHA-256 | SHA-3-256 |
|---|---|---|
| 发布年份 | 2001 | 2015 |
| 结构 | Merkle-Damgård | 海绵函数 |
| 已知弱点 | 无重大弱点 | 无已知弱点 |
| 性能 | ~500 MB/s | ~100 MB/s |
| 推荐用途 | 现有系统继续 | 新项目优先 |
选择建议
- 现有项目 — SHA-256 完全足够,无需迁移
- 新项目 — 优先使用 SHA-3-256(更新、更稳健)
- 密码存储 — 使用 bcrypt 或 argon2,不直接用 SHA-256
8. 为什么 MD5 和 SHA-1 已死
MD5(1992)— 永远不要用
MD5 输出 128 比特,一度很受欢迎。但在 2004 年,王小云证明了 MD5 的碰撞(两个不同输入产生相同哈希)。
MD5 碰撞示例:
Input 1: d131dd02c5e6eec4693d61a8c169...(64 字节)
Input 2: d131dd02c5e6eec4693d61a8c169...(64 字节,中间不同)
MD5 Output: 79054025255fb1a26e4b0d753b789d82(两者相同)
风险:
- 不能用于文件完整性检查(攻击者可修改文件和哈希)
- 不能用于数字签名(可伪造签名)
- 不能用于密码存储(虽然密码本身一般不会碰撞,但多个 MD5 数据库可互通)
SHA-1(1995)— 基本退役
SHA-1 输出 160 比特,比 MD5 更强,长期被用于 HTTPS 证书和 Git。但在 2017 年,Google 和 CWI Amsterdam 联合发表了 SHA-1 的实际碰撞(SHAttered 项目)。
- 难度远低于理论,实际可在数周内计算
- 2019 年,主流浏览器完全停止接受 SHA-1 证书
- Git 社区正逐步迁移到 SHA-256
现在应该做什么
| 旧算法 | 替代品 | 迁移优先级 |
|---|---|---|
| MD5 | SHA-256 或 SHA-3 | 立即 |
| SHA-1(非签名) | SHA-256 或 SHA-3 | 高 |
| SHA-1(证书) | SHA-256 | 已完成(浏览器强制) |
常见问题
为什么密码必须哈希而不能加密?
如果用加密存密码,数据库一旦泄露,黑客可获得密钥或解密方式,立即解出所有密码。而哈希是单向的,即使获得哈希值也无法反向得到原密码。此外,即使两个用户密码相同,使用不同的盐值也会产生不同的哈希,防止彩虹表攻击。
什么是彩虹表?如何用盐值防御?
彩虹表是常见密码的预计算哈希表。黑客可直接查表匹配哈希。盐值是加密学中的随机数,与密码混合后再哈希,使每个密码的哈希都独一无二。即使两用户密码相同,盐值不同,哈希也不同,彩虹表失效。
bcrypt 和 argon2 为什么比 SHA-256 更好?
SHA-256 是快速哈希(纳秒级),容易被暴力破解。bcrypt 和 argon2 是慢哈希(毫秒级),内置盐值和工作因子,每次计算可调慢。这使攻击者需要更长时间破解,而真实用户只需等待一次。bcrypt 被广泛使用,argon2 更新且参数更灵活。
HMAC 是什么?为什么需要它?
HMAC 是消息认证码,用密钥和哈希函数组合,确保消息来源真实且未被篡改。与纯哈希不同,HMAC 需要密钥,攻击者无法自行计算正确的 HMAC。常用于 API 签名、Cookie 防篡改。
SHA-256 和 SHA-3 有什么区别?
SHA-256 是 SHA-2 系列,广泛使用,安全性已被验证 20+ 年。SHA-3 是更新的标准(2015),基于不同的数学结构,对边界情况更稳健,性能更好。两者都很安全,SHA-3 只有在需要额外保险时才必要,但新项目可优先 SHA-3。
为什么 MD5 和 SHA-1 不能再用?
MD5 和 SHA-1 都有已知的碰撞攻击。在 2004 年,研究者找到 MD5 碰撞方法;2017 年,Google 展示了 SHA-1 的实际碰撞。这意味着黑客可构造两个不同输入产生相同哈希,破坏完整性校验。不应用于密码、数字签名或文件校验。