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 中的對稱加密階段、檔案加密、資料庫加密的最佳實踐。