Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

AES 的工作模式:安全与灾难只差一个选项

| , 9 minutes reading.

1. 为什么要关心这个问题?

这是一张用 AES 加密的企鹅图片:

原图                    ECB 加密               CBC 加密
┌──────────────┐       ┌──────────────┐       ┌──────────────┐
│   🐧🐧🐧     │       │   🐧🐧🐧     │       │ ▓▒░█▓▒░█▓▒  │
│   🐧🐧🐧     │  →    │   🐧🐧🐧     │       │ ░█▓▒░█▓▒░█  │
│   🐧🐧🐧     │       │   🐧🐧🐧     │       │ █▓▒░█▓▒░█▓  │
└──────────────┘       └──────────────┘       └──────────────┘
                       企鹅轮廓可见!          完全随机

ECB 模式加密后,你仍然能看出这是一只企鹅。为什么?因为相同的明文块产生相同的密文块。图片中相同颜色的区域会有相同的密文,轮廓就这样泄漏了。

AES 算法完全相同,但模式的选择决定了安全性。

2. 定义

工作模式(Mode of Operation) 定义了如何使用块加密算法来加密超过一个块的数据。

AES 只能加密 16 字节的块。如果你的数据更长(几乎总是如此),你需要一种方法来:

  1. 把数据分成块
  2. 决定块之间如何关联
  3. 处理最后一个不完整的块(填充)

不同的工作模式以不同的方式解决这些问题,具有不同的安全特性。

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 的问题

  1. IV 必须随机且不可预测
# 错误
iv = b"0" * 16  # 固定 IV
iv = str(counter).zfill(16).encode()  # 可预测的 IV

# 正确
iv = os.urandom(16)  # 随机 IV
  1. 填充 Oracle 攻击
如果服务器在解密时泄漏「填充是否正确」:
攻击者可以逐字节恢复明文
这就是 POODLE 和 Lucky 13 攻击的原理
  1. 不提供完整性
攻击者可以修改密文
解密会产生垃圾,但你可能不知道
必须另外加 HMAC 来验证完整性
  1. 无法并行加密
每个块依赖前一个块
加密必须串行进行
(解密可以并行)

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 是现代选择

  1. 内置完整性检查
解密时自动验证认证标签
如果数据被篡改,解密会失败
不需要单独的 HMAC
  1. 附加数据认证(AAD)
可以认证不需要加密的数据(如头部)
AAD 不加密但包含在认证标签计算中
篡改 AAD 会导致认证失败
  1. 高性能
CTR 模式可以并行
GHASH 可以硬件加速
现代 CPU 有 AES-NI 和 PCLMULQDQ 指令

GCM 的注意事项

  1. Nonce 必须唯一
和 CTR 一样,重复 nonce 是灾难性的
常见做法:
- 计数器(如果你能保证不重复)
- 随机 96 比特(碰撞概率极低但不是零)
  1. 标签长度
推荐:128 比特(16 字节)
可接受:96 比特用于某些应用
较短的标签 = 较弱的认证
  1. 数据量限制
单个密钥 + nonce 组合:
最多加密 2³⁹ - 256 比特(约 64GB)
超过这个限制需要换密钥或 nonce

7. 模式比较表

特性ECBCBCCTRGCM
机密性⚠️
完整性
并行加密
并行解密
需要填充
需要 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 plaintext

9. 常见错误

错误 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. 本章小结

三点要记住:

  1. 永远不要使用 ECB。 它会泄漏明文的模式。如果你在代码中看到 ECB,那就是一个 bug。

  2. 优先使用 GCM。 它提供加密和认证,是现代的默认选择。如果必须用 CBC,一定要加 HMAC(Encrypt-then-MAC)。

  3. IV/Nonce 必须唯一。 CBC 的 IV 需要不可预测。CTR 和 GCM 的 nonce 只需要唯一,但重复使用会导致完全破解。

11. 下一步

我们已经理解了 AES 的工作模式。但在真实系统中,对称加密如何被使用?

在下一篇文章中,我们将探讨:对称加密在真实系统中的用法——HTTPS 中的对称加密阶段、文件加密、数据库加密的最佳实践。