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