AES 的工作模式:安全与灾难只差一个选项
1. 为什么要关心这个问题?
这是一张用 AES 加密的企鹅图片:
原图 ECB 加密 CBC 加密
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 🐧🐧🐧 │ │ 🐧🐧🐧 │ │ ▓▒░█▓▒░█▓▒ │
│ 🐧🐧🐧 │ → │ 🐧🐧🐧 │ │ ░█▓▒░█▓▒░█ │
│ 🐧🐧🐧 │ │ 🐧🐧🐧 │ │ █▓▒░█▓▒░█▓ │
└──────────────┘ └──────────────┘ └──────────────┘
企鹅轮廓可见! 完全随机ECB 模式加密后,你仍然能看出这是一只企鹅。为什么?因为相同的明文块产生相同的密文块。图片中相同颜色的区域会有相同的密文,轮廓就这样泄漏了。
AES 算法完全相同,但模式的选择决定了安全性。
2. 定义
工作模式(Mode of Operation) 定义了如何使用块加密算法来加密超过一个块的数据。
AES 只能加密 16 字节的块。如果你的数据更长(几乎总是如此),你需要一种方法来:
- 把数据分成块
- 决定块之间如何关联
- 处理最后一个不完整的块(填充)
不同的工作模式以不同的方式解决这些问题,具有不同的安全特性。
3. ECB:永远不要使用
工作原理
明文: P₁ P₂ P₃ P₄
│ │ │ │
▼ ▼ ▼ ▼
┌──┐┌──┐┌──┐┌──┐
Key→ │E ││E ││E ││E │
└──┘└──┘└──┘└──┘
│ │ │ │
▼ ▼ ▼ ▼
密文: C₁ C₂ C₃ C₄
每个块独立加密为什么不安全
如果 P₁ = P₃,那么 C₁ = C₃
攻击者可以:
- 检测重复模式
- 重新排列块
- 在不解密的情况下替换块著名的 ECB 企鹅
这个例子如此著名,以至于「ECB 企鹅」成了一个密码学梗。任何有重复模式的图片在 ECB 加密后都会泄漏结构信息。
何时可以接受 ECB
几乎从不。唯一例外:
- 加密单个块(16 字节)的随机数据
- 密钥包装算法(有特殊设计)
即使这些情况,也有更好的选择。
4. CBC:经典但需要注意
工作原理
明文: P₁ P₂ P₃ P₄
│ │ │ │
IV ──►⊕ │ │ │
│ │ │ │
▼ │ │ │
┌──┐│ │ │
Key→ │E │▼ │ │
└──┘│ │ │
│ ⊕ │ │
│ │ │ │
▼ ▼ │ │
C₁ ─────┬──┐│ │
│E │▼ │
└──┘│ │
│ ⊕ │
│ │ │
▼ ▼ │
C₂ ───────┬──┐ │
│E │ ▼
└──┘ │
│ ⊕
│ │
▼ ▼
C₃ ─────────┬──┐
│E │
└──┘
│
▼
C₄
每个块的加密依赖于前一个密文块IV 的关键作用
相同的明文 + 相同的密钥:
IV = "random1234567890" → 密文 A
IV = "different7654321" → 密文 B(完全不同!)
IV 确保相同的明文不会产生相同的密文CBC 的优点
- 相同明文产生不同密文(如果 IV 不同)
- 密文中的一比特错误只影响两个明文块
- 被充分研究,广泛使用
CBC 的问题
- IV 必须随机且不可预测
# 错误
iv = b"0" * 16 # 固定 IV
iv = str(counter).zfill(16).encode() # 可预测的 IV
# 正确
iv = os.urandom(16) # 随机 IV- 填充 Oracle 攻击
如果服务器在解密时泄漏「填充是否正确」:
攻击者可以逐字节恢复明文
这就是 POODLE 和 Lucky 13 攻击的原理- 不提供完整性
攻击者可以修改密文
解密会产生垃圾,但你可能不知道
必须另外加 HMAC 来验证完整性- 无法并行加密
每个块依赖前一个块
加密必须串行进行
(解密可以并行)5. CTR:把块加密变成流加密
工作原理
Nonce || Counter: N|0 N|1 N|2 N|3
│ │ │ │
▼ ▼ ▼ ▼
┌──┐ ┌──┐ ┌──┐ ┌──┐
Key ──────────► │E │ │E │ │E │ │E │
└──┘ └──┘ └──┘ └──┘
│ │ │ │
Keystream: K₁ K₂ K₃ K₄
│ │ │ │
明文: P₁ ⊕ P₂ ⊕ P₃ ⊕ P₄
│ │ │ │
▼ ▼ ▼ ▼
密文: C₁ C₂ C₃ C₄CTR 的优点
- 可以并行加密和解密
- 不需要填充
- 加密和解密使用相同操作
- 可以随机访问(计算第 N 个块不需要前面的块)
CTR 的问题
Nonce 绝对不能重复使用!
如果用相同的 key + nonce 加密两个消息:
C₁ = P₁ ⊕ K
C₂ = P₂ ⊕ K
C₁ ⊕ C₂ = P₁ ⊕ P₂
攻击者得到两个明文的 XOR
如果知道其中一个明文,就能得到另一个6. GCM:现代默认选择
什么是 GCM
GCM(Galois/Counter Mode) 是一种认证加密模式,同时提供:
- 机密性(加密)
- 完整性(认证)
- 附加数据认证(AAD)
┌───────────────────────────────────────────────────────────┐
│ AES-GCM = AES-CTR 加密 + GHASH 认证 │
└───────────────────────────────────────────────────────────┘工作原理
┌─────────────────────────────────────┐
│ AES-CTR 加密 │
│ Nonce → Counter → AES → Keystream │
│ ⊕ │
│ Plaintext │
│ ↓ │
│ Ciphertext │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ GHASH 认证 │
│ AAD + Ciphertext + Lengths │
│ ↓ │
│ Authentication Tag │
└─────────────────────────────────────┘为什么 GCM 是现代选择
- 内置完整性检查
解密时自动验证认证标签
如果数据被篡改,解密会失败
不需要单独的 HMAC- 附加数据认证(AAD)
可以认证不需要加密的数据(如头部)
AAD 不加密但包含在认证标签计算中
篡改 AAD 会导致认证失败- 高性能
CTR 模式可以并行
GHASH 可以硬件加速
现代 CPU 有 AES-NI 和 PCLMULQDQ 指令GCM 的注意事项
- Nonce 必须唯一
和 CTR 一样,重复 nonce 是灾难性的
常见做法:
- 计数器(如果你能保证不重复)
- 随机 96 比特(碰撞概率极低但不是零)- 标签长度
推荐:128 比特(16 字节)
可接受:96 比特用于某些应用
较短的标签 = 较弱的认证- 数据量限制
单个密钥 + nonce 组合:
最多加密 2³⁹ - 256 比特(约 64GB)
超过这个限制需要换密钥或 nonce7. 模式比较表
| 特性 | ECB | CBC | CTR | GCM |
|---|---|---|---|---|
| 机密性 | ⚠️ | ✅ | ✅ | ✅ |
| 完整性 | ❌ | ❌ | ❌ | ✅ |
| 并行加密 | ✅ | ❌ | ✅ | ✅ |
| 并行解密 | ✅ | ✅ | ✅ | ✅ |
| 需要填充 | ✅ | ✅ | ❌ | ❌ |
| 需要 IV/Nonce | ❌ | ✅ | ✅ | ✅ |
| IV/Nonce 重用后果 | N/A | 模式泄漏 | 完全破解 | 完全破解 |
| 推荐使用 | ❌ | ⚠️ | ⚠️ | ✅ |
8. 实用代码示例
AES-GCM(推荐)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
def encrypt_aes_gcm(plaintext: bytes, key: bytes, aad: bytes = b"") -> tuple:
"""
使用 AES-GCM 加密
返回 (nonce, ciphertext_with_tag)
"""
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96 比特 nonce
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
return nonce, ciphertext
def decrypt_aes_gcm(nonce: bytes, ciphertext: bytes, key: bytes, aad: bytes = b"") -> bytes:
"""
使用 AES-GCM 解密
如果认证失败会抛出异常
"""
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, aad)
# 使用示例
key = AESGCM.generate_key(bit_length=256)
message = b"Secret message"
header = b"public header" # AAD
nonce, ciphertext = encrypt_aes_gcm(message, key, header)
plaintext = decrypt_aes_gcm(nonce, ciphertext, key, header)
print(f"原文: {message}")
print(f"解密: {plaintext}")AES-CBC + HMAC(传统但仍可接受)
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import hashes, hmac, padding
import os
def encrypt_aes_cbc_hmac(plaintext: bytes, enc_key: bytes, mac_key: bytes) -> tuple:
"""
AES-CBC 加密 + HMAC 认证
Encrypt-then-MAC 模式
"""
# 填充
padder = padding.PKCS7(128).padder()
padded = padder.update(plaintext) + padder.finalize()
# 加密
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(enc_key), modes.CBC(iv))
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded) + encryptor.finalize()
# 计算 MAC(包含 IV)
h = hmac.HMAC(mac_key, hashes.SHA256())
h.update(iv + ciphertext)
tag = h.finalize()
return iv, ciphertext, tag
def decrypt_aes_cbc_hmac(iv: bytes, ciphertext: bytes, tag: bytes,
enc_key: bytes, mac_key: bytes) -> bytes:
"""
验证 HMAC 然后解密
"""
# 先验证 MAC
h = hmac.HMAC(mac_key, hashes.SHA256())
h.update(iv + ciphertext)
h.verify(tag) # 如果失败会抛出异常
# 解密
cipher = Cipher(algorithms.AES(enc_key), modes.CBC(iv))
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
# 移除填充
unpadder = padding.PKCS7(128).unpadder()
plaintext = unpadder.update(padded) + unpadder.finalize()
return plaintext9. 常见错误
错误 1:使用 ECB 模式
# 错误
cipher = Cipher(algorithms.AES(key), modes.ECB())
# 正确
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce))错误 2:重复使用 IV/Nonce
# 错误
nonce = b"fixed_nonce_123" # 固定 nonce
for message in messages:
encrypt(message, key, nonce)
# 正确
for message in messages:
nonce = os.urandom(12) # 每次生成新的
encrypt(message, key, nonce)错误 3:CBC 没有认证
# 错误
ciphertext = aes_cbc_encrypt(plaintext, key, iv)
# 没有 MAC,攻击者可以篡改
# 正确
ciphertext = aes_cbc_encrypt(plaintext, key, iv)
tag = hmac(mac_key, iv + ciphertext)
# 解密前先验证 tag错误 4:MAC-then-Encrypt vs Encrypt-then-MAC
# 错误(MAC-then-Encrypt)
tag = hmac(plaintext)
ciphertext = encrypt(plaintext + tag)
# 无法在解密前验证完整性
# 正确(Encrypt-then-MAC)
ciphertext = encrypt(plaintext)
tag = hmac(ciphertext)
# 可以在解密前验证完整性10. 本章小结
三点要记住:
永远不要使用 ECB。 它会泄漏明文的模式。如果你在代码中看到 ECB,那就是一个 bug。
优先使用 GCM。 它提供加密和认证,是现代的默认选择。如果必须用 CBC,一定要加 HMAC(Encrypt-then-MAC)。
IV/Nonce 必须唯一。 CBC 的 IV 需要不可预测。CTR 和 GCM 的 nonce 只需要唯一,但重复使用会导致完全破解。
11. 下一步
我们已经理解了 AES 的工作模式。但在真实系统中,对称加密如何被使用?
在下一篇文章中,我们将探讨:对称加密在真实系统中的用法——HTTPS 中的对称加密阶段、文件加密、数据库加密的最佳实践。
