大多数对哈希的解释停在"它能把数据变成一串短字符"。那是"是什么",不是"为什么"。如果你想弄明白长度扩展攻击为什么会存在、为什么 SHA-3 看着和 SHA-2 完全不像、以及 HMAC 为什么是那个构造,你就得往机器里面看。这篇文章往下钻一层。
压缩函数是发动机
每一个密码学哈希都是由一个更小的部件搭起来的,叫压缩函数。它接收两个固定大小的输入——当前的链接值(chaining value)和你消息的下一个分组——返回一个同样大小的新链接值。哈希就是把你的消息一个分组一个分组地喂进去:
CV0 = 初始常量
CV1 = compress(CV0, block1)
CV2 = compress(CV1, block2)
...
digest = CVn
如果压缩函数表现得像一个随机映射,那么整个哈希就继承了它的单向性和雪崩特性。设计一个哈希,几乎等同于设计这一个函数。
Merkle-Damgård 与长度扩展陷阱
SHA-1、SHA-256 和 SHA-512 全都使用 Merkle–Damgård 构造。你的消息先被填充到分组大小的整数倍,然后像上面那样流过压缩函数,最后的链接值就是摘要。
这种设计有个已知弱点。摘要本质上就是处理完最后一个分组时的内部状态。如果我知道 H(m) 和 m 的长度,我就能从这个精确的状态出发、继续往后算——也就是在不知道 m 的情况下算出 H(m || 填充 || extra)。这就是长度扩展攻击。
它没法让我恢复出 m,但它能让我伪造出扩展消息的、看起来合法的摘要。这就是为什么 hash(密钥 || 消息) 是个会出事的消息认证写法,也是为什么 HMAC 会存在(下面讲)。
海绵结构:SHA-3 的回答
SHA-3(Keccak)干脆扔掉了 Merkle–Damgård,改用海绵构造。想象一个固定大小的状态:你先把消息"吸收"进去,一个分组一个分组;然后再从里面把摘要"挤"出来。因为输出是从一个从不等于中途内部状态的状态里挤出来的,长度扩展就不适用了。
海绵结构还带来一个好处:你想要多少位就能挤多少位,这对 SHAKE128/SHAKE256 这种输出长度由你决定的变体特别方便。它并不是在简单意义上"比 SHA-2 更好"——两者都是安全的——但它的结构完全不同,这对整个生态是好事。万一某一族被削弱,我们不至于一无所有。
雪崩效应,落到具体
雪崩要求:翻转一个输入位,大约翻转一半的输出位。这不是魔法,它来自压缩函数如何经过许多轮非线性操作把位混在一起。直觉是这样的:每一轮都把每个输入位再扩散远一点,足够的轮数之后,每个输出位都依赖于每个输入位。一个"改一个输入只翻转几个输出位"的哈希,会明显区别于随机,因而通不过设计。
你不用懂数学也能感受它:哈希 "cat" 和 "cats",然后对比两段摘要。它们几乎没有任何相同的字符,尽管输入只差一个字母。
生日悖论与 2^(n/2) 边界
找一个碰撞有多难?朴素地想,你大概会觉得 n 位摘要需要试 2^n 次哈希。其实不需要,因为生日悖论。
同一个道理——一个 23 人的房间里大概率有人同一天生日——也适用于哈希:只要有大约 sqrt(2^n) = 2^(n/2) 个随机输入,你就相当有可能撞出两个相同的。所以一个 256 位的哈希给你的碰撞抵抗力大约是 128 位,而不是 256 位。像 MD5 这种 128 位的哈希,碰撞抵抗力只有约 64 位——而 64 位的工作量对认真攻击者来说已经够得着了,这也是 MD5 在安全用途上完蛋的另一个原因。这也解释了为什么 SHA-1 的 160 位(约 80 位碰撞抵抗力)在 SHAttered 把它变成实际可行之前,就早被认为只是"勉强"。
非密码学哈希有另一份工作
不是每个哈希都要对抗攻击者。CRC32 和 xxHash 是为速度和"抓意外变化 / 把东西分桶"而设计的。它们故意不对蓄意伪造者保持抗碰撞——而这没问题,因为本来也不该有人去攻击它们。用 CRC32 检测网络包里一个翻转的位是完美的;用 CRC32 校验一个软件下载就不行了。
| 家族 | 例子 | 对抗对象 | 用在 |
|---|---|---|---|
| 非密码学 | CRC32、xxHash、FNV | 仅意外错误 | 校验和、哈希表 |
| 密码学 | SHA-256、SHA-3、BLAKE3 | 蓄意伪造者 | 签名、完整性、密码 |
为什么 HMAC 是那个样子
要用共享密钥给消息做认证,你不能简单地 hash(密钥 || 消息)——那会中我们前面讲的长度扩展攻击。HMAC 用"哈希两次"的构造解决了这个问题:
HMAC(K, m) = H( (K ⊕ opad) ‖ H( (K ⊕ ipad) ‖ m ) )
拆开看:内层哈希先把密钥(与一个固定的 ipad 做异或)混进消息;外层哈希再把密钥(与另一个不同的 opad 做异或)混进内层的结果。这两个 pad 加两遍哈希,意味着只看到输出的攻击者无法利用长度扩展去伪造消息,因为无论内层还是外层都得先知道密钥才能算。双重哈希也能抵抗某些单独出现在底层压缩函数上的弱点。
如果你要在服务之间给请求做认证,直接上 HMAC-SHA256 和一个成熟的库——别用原始哈希自己拼,哪怕那句一行代码看着很诱人。
想不装任何东西就试验这些算法,哈希生成工具 在浏览器里就算出 SHA-256、SHA-512 和 BLAKE3,而把敏感内容粘进浏览器本地处理,本来也是最安全的做法。