同一个 AES,选错模式就等于裸奔。这一章有密码学史上最著名的一张翻车图。
AES 只能加密 16 字节。可你要加密的东西——一张图片、一段 JSON、一个视频——几乎总是更长。怎么把它切成很多块、逐块加密,再拼回去?这个"怎么衔接"的规则,就叫分组模式(mode of operation)。选对了,安全;选错了,再强的 AES 也保护不了你。
最直觉的办法:把数据切成 16 字节一块,每块各自独立地用同一把密钥加密,再首尾拼起来。这叫 ECB(电子密码本)模式。
问题一句话就能说清:相同的明文块,永远产生相同的密文块。这不就是我们第 3 章反复处刑的那个幽灵吗——确定性映射保留了统计结构。凯撒密码里"相同字母→相同密文"被频率分析击穿;ECB 里"相同数据块→相同密文块",会让明文里的重复模式原封不动地透过密文显形。
光说不够。下面的 Demo 现场画一只企鹅(大片纯色区域),然后分别用 ECB 思路和 CBC/CTR 思路去"加密"它的像素块。ECB 下,大片相同颜色的区域被加密成大片相同的密文——于是企鹅的轮廓完好地泄露了出来;而链式模式下,一切都变成了均匀噪声。
ECB 几乎是唯一一个"名字很正经、实际几乎永远是错的"模式。任何时候你在代码里看到 AES/ECB/...,都应该当成一个 bug。它不仅泄露图像轮廓,也泄露数据库里"哪些记录字段相同"、Cookie 里"哪些用户权限相同"等结构信息。可惜很多语言/库的默认模式恰好是 ECB(比如 Java 的 Cipher.getInstance("AES") 不指定模式时就是 ECB!),这是无数真实漏洞的源头。
ECB 的病根是"确定性"。治法是给加密注入随机性,让同样的明文每次加密都得到不同的密文。做这件事的关键零件叫 IV(初始化向量)——一个每次加密都重新随机生成的值。
CBC 的思路是"链式":加密每一块之前,先把它和上一块的密文 XOR;第一块则和 IV XOR。
这样每一块都"记得"它前面所有块,重复的明文块不再产生重复的密文块,ECB 的病根被拔除。只要 IV 每次随机,同一段明文两次加密的结果就完全不同。
CTR 更漂亮:它不直接加密明文,而是加密一串"nonce+计数器",用输出当密钥流,再去 XOR 明文——换句话说,CTR 把分组密码变成了流密码(第 6 章的思想在这里回归)。它天然支持并行、随机访问,是现代 AEAD(第 9 章 GCM)的基础。
| 模式 | 相同明文→相同密文? | 需要 IV/nonce? | 能并行? | 提供完整性? |
|---|---|---|---|---|
| ECB | 是(致命) | 否 | 是 | 否 |
| CBC | 否 | 是(随机 IV) | 加密否 | 否 |
| CTR | 否 | 是(nonce) | 是 | 否 |
① CBC 的 IV 必须不可预测(随机):如果攻击者能预测下一个 IV,就能发起 BEAST 那类攻击。② CTR/GCM 的 nonce 必须唯一、绝不重复——这又回到了第 5、6 章的两次一密。IV/nonce 通常和密文一起明文传输(它不是秘密),但它的"新鲜度"是安全的一部分。把 IV 写死成全零,是仅次于用 ECB 的第二大经典错误。
看上面的表格最后一列:CBC 和 CTR 都不提供完整性。它们只保证 Eve 读不懂,却挡不住 Mallory 篡改。
CTR 模式尤其危险:它本质是流密码,攻击者翻转密文的某一位,解密后明文对应位就精确翻转,且解密方毫无察觉。想象一段加密的转账报文 amount=0100,攻击者不需要密钥,只要知道字段位置,就能翻几个比特把它改成 amount=9100——这就是第 1 章开篇警告的"加密 ≠ 完整性"的具体形态。
CBC 也好不到哪去,它对特定篡改敏感,还催生了臭名昭著的 padding oracle 攻击:仅仅因为服务器会对"填充格式错误"和"其它错误"返回不同的响应,攻击者就能像第 1 章说的"预言机"一样,一次问出一个字节,最终解出整段密文——完全不需要密钥。
Padding oracle 不是理论。2010 年的 POODLE、针对 ASP.NET 的攻击、以及无数自研"加密"接口,都栽在同一件事上:只加密,不认证。教训极其明确——加密和完整性必须捆绑在一起做,而且要用经过验证的方式做。这正是下一章 AEAD 存在的全部理由。
于是我们来到了对称密码的终点站:一个既保机密、又保完整、还防重放的"全家桶"——AEAD。这也是你在 Android 上唯一应该选择的对称加密方式。下一章,我们把它讲透,并亲手篡改一段密文看它当场报警。