本节摘要:分组密码只处理固定长度的块,把任意长度的消息串成块序列的方案叫工作模式。模式选错,再强的算法也会当场泄露内容:ECB 模式下密文会原样复刻明文的块级结构,著名的"密文企鹅"就拜它所赐。本节对比 ECB、CBC、CTR 与 GCM 四种代表模式的结构与适用场景,并用一个实验证明"随机数重用"为什么是灾难级事故。
1990 年代,密码学圈流传出一张著名图片:一张企鹅照片被 ECB 模式加密后,密文图像里依然能清楚看出企鹅轮廓。这不是算法被攻破,是模式背叛了算法。ECB(电子密码本)模式把消息切成块,每块独立加密:同一把密钥下,相同的明文块必然产生相同的密文块。分组密码内部那 16 轮 Feistel 或 SPN 搅碎了"块内"的统计结构,但"块与块之间"的关系被 ECB 原封不动地保留——图像大面积相同的区域、数据库里重复的记录、协议包里固定位置的字段,全部在密文里原样显形。
ECB 的失败揭示了一个原则性要求:语义安全——同一明文每次加密的结果应当不同,否则敌手只凭"这两处一样"就能获得信息。实现语义安全的标准办法是引入随机化的初始向量(IV)并把块串起来,这正好催生了 CBC 与 CTR 两种主流模式。

| 维度 | ECB | CBC | CTR | GCM |
|---|---|---|---|---|
| 块间结构 | 无链接,相同块同密文 | 密文链 + IV | 计数器造密钥流 | CTR 加密 + GHASH 认证 |
| 语义安全 | 无 | 有(IV 随机) | 有(nonce 唯一) | 有(nonce 唯一) |
| 并行加密 | 可 | 不可(串行链) | 可 | 可 |
| 随机访问解密 | 可 | 需先解前一块 | 可(跳到任意计数器) | 可 |
| 附带完整性 | 无 | 无(需另加 HMAC) | 无(需另加认证) | 有,输出认证标签 |
| 现状 | 禁用于新设计 | 遗留系统与 TLS 旧版 | 作为 GCM 底层广泛使用 | TLS 1.3 主力(AEAD) |
CBC 的链条式设计比 ECB 高明,但有串行化的代价:块与块必须按序处理,多核 CPU 无用武之地。CTR 把分组密码当成"密钥流发生器"——用计数器逐块生成伪随机流再与明文异或——既恢复了并行性,又允许从任意字节处开始解密(视频拖动进度条的底气)。而它对 nonce 唯一性的要求是绝对命令:计数器重用一次,上面的减法实验就当场兑现,两份密文的异或直接等于两份明文的异或。
GCM 在 CTR 外面又包了一层 GHASH 认证,把"加密 + 完整性"合成一次调用,输出一个认证标签——这类组合叫 AEAD(带关联数据的认证加密)。TLS 从 1.2 起力推 AES-GCM,到 1.3 干脆只保留 AEAD 套件(AES-GCM 与 ChaCha20-Poly1305),把 3.5 节要讲的"加密与认证次序之争"直接用协议格式终结。
用 Python 复现随机数重用实验,亲眼看密钥流消掉:
import hashlib def ctr_keystream(key: bytes, nonce: bytes, n: int) -> bytes: """以 nonce+计数器为输入造密钥流(演示用,非标准 GCM)""" stream, ctr = b"", 0 while len(stream) < n: blk = hashlib.sha256(key + nonce + ctr.to_bytes(8, "big")).digest() stream += blk ctr += 1 return stream[:n] key, nonce = b"demo-key", b"0001" p1, p2 = b"MOVE FUNDS TO ACCOUNT A", b"MOVE FUNDS TO ACCOUNT B" c1 = bytes(a ^ b for a, b in zip(p1, ctr_keystream(key, nonce, len(p1)))) c2 = bytes(a ^ b for a, b in zip(p2, ctr_keystream(key, nonce, len(p2)))) # nonce 复用! diff = bytes(a ^ b for a, b in zip(c1, c2)) print("两个密文的异或 =", diff) # 密钥流成对消掉 print("两个明文的异或 =", bytes(a ^ b for a, b in zip(p1, p2))) print("两者相等,且只有 A/B 两字节不同——差异暴露无遗")
⚠️ 常见坑:随机数(IV/nonce)不当使用是实践中最高频的对称密码事故——重复的 GCM nonce 不仅泄露内容,还会泄露认证密钥使伪造成为可能。规则只有一条:同一密钥下,nonce 绝不重复;不确定时就换密钥。
问一:遗留系统还在用 CBC,要立刻换吗?若 CBC 之外有独立认证(先加密后 MAC),风险可控,按升级路线图走;若靠 MAC 后加密或干脆无认证,应提高优先级——填充预言机不会等你。
问二:磁盘加密为什么用 XTS 而不是 GCM?盘块的原地改写、随机访问与"不认证内容"的语义和 XTS 的设计完全匹配;完整性由文件系统与日志层另行负责,加密层只管机密性。
问三:CBC 的 IV 与 CTR 的 nonce 生成规则一样吗?不一样。CBC 的 IV 要不可预测(防首块被针对性选择),CTR 的 nonce 在同一密钥下绝不重复即可(可预测无妨,重复即灾难)。两者常被混用配置,这是模式事故里最隐蔽的一类。
块模式还牵出一个不起眼却事故频出的角落:消息长度不是块长的整数倍怎么办。CBC 一类模式依赖显式填充(如 PKCS 系列:缺几字节就填几个"缺几"的数值),解密端必须先剥离填充再验内容,剥离逻辑就是填充预言机攻击的主战场;CTR 与流密码没有填充,但也因此要额外携带长度与认证标签,否则截断攻击无法被发现。工程建议一句话:填充满理由交给标准库,边界长度交给认证标签,永远不要手写这两段逻辑。
模式是把分组密码"装进"真实消息的胶水。下一节看另一条平行演化的路线——天生逐位处理的流密码,以及 RC4 从不可一世到全网禁用的兴衰。