选模式这件事,看上去像是在一堆缩写里挑一个,实际上是在决定「加密到底保护了什么」。选错的后果不是性能差一点,而是加密做了等于没做。
先给结论:
新代码用 GCM。老系统迁不动用 CBC 加 HMAC。任何情况下不要用 ECB。
下面是理由。
模式对照
| 模式 | 需要 IV | 需要填充 | 可并行 | 防篡改 | 结论 |
|---|---|---|---|---|---|
| ECB | 否 | 是 | 是 | 否 | ❌ 泄漏明文结构 |
| CBC | 是(16 字节) | 是 | 仅解密 | 否 | ⚠️ 必须外加 MAC |
| CFB | 是 | 否 | 仅解密 | 否 | 已被 CTR 取代 |
| OFB | 是 | 否 | 否 | 否 | 已被 CTR 取代 |
| CTR | 是(nonce) | 否 | 是 | 否 | ⚠️ nonce 绝不能重用 |
| GCM | 是(12 字节 nonce) | 否 | 是 | ✅ 是 | ✅ 首选 |
注意「防篡改」那一列——只有 GCM 是勾。这一列就是选型的分水岭。
ECB:把明文的结构原样印在密文上
ECB 的做法是把明文切成 16 字节一组,每组独立加密。结果就是:相同的明文分组必然产生相同的密文分组。
最有名的演示是拿 ECB 加密一张位图,加密后企鹅的轮廓依然清晰可见。但别被「那是图片」误导,在业务数据上后果一样严重:
等值关联泄漏。 数据库里加密存储手机号,相同号码的密文也相同。攻击者不用解密就能:统计哪个号码出现得最多、把不同表里的同一个人关联起来、拿常见值做字典比对。
分组重排攻击。 密文分组彼此独立,可以剪切、复制、调换顺序。攻击者能从记录 A 剪一段密文贴到记录 B,拼出一条从未被加密过、但你的系统会正常解密的数据。转账金额、权限等级这类固定位置的字段尤其危险。
结构泄漏。 固定格式的数据里,哪些字段相同、哪些重复出现,都直接写在密文的模式里。
唯一可以接受的场景:明文总长不超过 16 字节,且每次的值都不重复(比如加密一个随机 token)。此时 ECB 和 CBC 没有实质差别。除此之外没有理由用它。
CBC:解决了图案问题,没解决篡改问题
CBC 让每个明文分组先和前一个密文分组异或再加密,第一个分组用 IV 顶上。这样相同的明文分组不再产生相同的密文,ECB 的图案问题解决了。
但 CBC 只提供保密性,它无法告诉你密文有没有被改过。这带来两类攻击:
比特翻转
翻转密文第 N 个分组的某一比特,解密后第 N+1 个分组的对应比特会跟着翻转。攻击者不需要密钥,只要猜到明文的结构,就能定向改动它。明文里有 "role":"user" 这种东西时,这不是理论问题。
padding oracle
只要系统在「填充错误」和「其它错误」上表现出任何差异——不同的错误码、不同的报错文案、甚至只是不同的响应时间——攻击者就能一个字节一个字节地把密文解出来,全程不需要密钥。
这是 Vaudenay 在 2002 年提出的攻击,二十多年来在 TLS、ASP.NET 和各类 Web 框架上反复出现,是最顽固的一类实现漏洞。
所以 CBC 必须配 MAC
顺序很重要:先加密,再对密文算 MAC(Encrypt-then-MAC)。验证 MAC 失败就直接拒绝,根本不要进入解密流程——这样 padding oracle 就没有可以观察的差异了。
反过来(先算 MAC 再加密,或者对明文算 MAC)仍然会漏出 oracle。另外加密密钥和 MAC 密钥要分开,不要复用同一把。
既然都要额外加一层,不如直接用 GCM。
CTR 和 GCM:nonce 是唯一的红线
CTR 把「nonce + 计数器」加密成密钥流,再和明文异或。好处很实在:不需要填充、加解密都能并行、能跳到任意位置解密。
代价是它对 nonce 重用零容忍:
同一把密钥 + 同一个 nonce
→ 完全相同的密钥流
→ 密文A ⊕ 密文B = 明文A ⊕ 明文B
密钥在这里被彻底绕过了。两段明文异或的结果,在很多情况下可以直接还原出原文。
GCM 下重用更严重一档:除了上面这条,还会泄漏 GHASH 的认证子密钥 H。拿到 H 之后,攻击者可以对任意密文伪造出合法的认证标签——完整性保护整个失效。这就是所谓的 forbidden attack。
怎么保证 nonce 不重复
- 每次用 12 字节的密码学安全随机数 —— GCM 的推荐做法,也是最不容易做错的
- 持久化递增计数器 —— 理论上更好,但要保证进程重启、多实例部署、数据库回滚都不会让它倒退,工程上比随机数难做对得多
- 同一把密钥不要接近 2³² 次加密 —— 用随机 nonce 时,超出这个量级碰撞概率就不可忽略了,该轮换密钥
最危险的写法是把 nonce 写死成常量。 这在老代码里出奇地常见,通常是因为「加密时要传个 IV,随便填一个先跑通」,然后就上线了。
GCM 的三样东西缺一不可
密文、nonce、认证标签(16 字节)——三样都要传给解密方。nonce 和 tag 都不需要保密,但都不能丢。对接时漏传 tag 是常见问题。
CFB 和 OFB:了解就行
它们当年的价值是「不需要填充」,密文和明文等长。但 CTR 同样不需要填充,而且加解密都能并行、支持随机访问,在多核 CPU 上快得多。
演进线很清楚:
CFB / OFB → CTR → GCM
(免填充) (可并行) (加完整性)
越往后越没有理由退回去。
DES 和 3DES:只用来读历史数据
DES 的密钥只有 56 位。1998 年就被专用硬件在几天内破解,今天用云上算力是小时级的事。
3DES 把有效密钥提到 112 位,抗暴力破解没问题,但分组长度只有 64 位。按生日界,同一把密钥加密约 32 GB 数据后就会出现分组碰撞,可据此恢复明文——这就是 2016 年的 Sweet32。NIST 在 SP 800-131A Rev.2 中已将 3DES 的加密用途列为 disallowed。
AES 的分组是 128 位,同样的碰撞门槛远在任何现实数据量之外。
本站工具保留 DES 和 3DES,是为了帮你把历史数据解出来再用 AES 重新加密,不是鼓励在新代码里用。
决策树
需要防篡改吗?(数据经过不可信链路 / 存在不可信的地方)
├─ 是(绝大多数场景)
│ ├─ 环境和对接方都支持 GCM? → ✅ GCM,nonce 每次随机 12 字节
│ └─ 不支持 → CBC + HMAC-SHA256(先加密再算 MAC,双密钥)
└─ 否(罕见,且你确定)
├─ 明文 ≤ 16 字节且不重复 → ECB 勉强可以
└─ 其它 → CBC,IV 每次随机
动手看看差别
AES 加解密支持全部六种模式,可以直接观察它们的行为差异:
- ECB 的图案泄漏:把
AAAAAAAAAAAAAAAA重复三遍加密,会看到密文里出现三段一模一样的分组 - CBC 的 IV 效应:同样的明文和密钥,换个 IV,密文完全不同
- GCM 的完整性:把密文改动一个字符再解密,会直接验证失败,而不是吐出一段乱码
工具在浏览器本地计算,密钥和明文都不会离开设备。AES 三种密钥长度均通过 FIPS-197 官方测试向量验证,并与 OpenSSL 逐字节交叉核对过。
如果两端结果对不上,模式只是四个变量之一,另外三个(密钥编码、IV 来源、填充方式)见同样的密钥和明文,为什么各家在线 AES 工具算出来不一样。