AES 是如何運作的(不靠數學也能懂)
1. 為什麼要關心這個問題?
每當你:
- 訪問 HTTPS 網站
- 使用 WhatsApp 或 Signal 發訊息
- 將檔案存到加密硬碟
- 用 SSH 連接伺服器
你都在使用 AES。
但大多數開發者對 AES 的了解停留在「呼叫函式庫」的層面。這篇文章的目標是讓你理解 AES 內部在做什麼——不需要數學證明,只需要直觀理解。
理解 AES 的運作方式能幫助你:
- 選擇正確的工作模式(ECB vs CBC vs GCM)
- 理解為什麼某些配置是危險的
- 在除錯時知道問題可能出在哪裡
2. 定義
AES(Advanced Encryption Standard) 是一種對稱分組加密演算法,在 2001 年被 NIST 選為取代 DES 的新標準。
技術規格:
- 區塊大小: 固定 128 位元(16 位元組)
- 金鑰長度: 128、192 或 256 位元
- 輪數: 10、12 或 14 輪(取決於金鑰長度)
- 結構: SPN(替換-置換網路)
AES 的原名是 Rijndael(發音接近「Rain-doll」),由比利時密碼學家 Vincent Rijmen 和 Joan Daemen 設計。
3. SPN vs Feistel:結構的差異
Feistel(DES 使用)
每輪只處理一半資料:
L' = R
R' = L ⊕ F(R, K)
優點:加密解密共用相同電路
缺點:擴散較慢,需要更多輪數SPN(AES 使用)
每輪處理全部資料:
State' = MixColumns(ShiftRows(SubBytes(State))) ⊕ RoundKey
優點:擴散快,更少輪數達到相同安全性
缺點:加密和解密需要不同的操作(逆操作)4. AES 的狀態矩陣
AES 把 16 位元組的輸入組織成一個 4×4 的位元組矩陣:
輸入:00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
狀態矩陣:
┌────┬────┬────┬────┐
│ 00 │ 04 │ 08 │ 0C │
├────┼────┼────┼────┤
│ 01 │ 05 │ 09 │ 0D │
├────┼────┼────┼────┤
│ 02 │ 06 │ 0A │ 0E │
├────┼────┼────┼────┤
│ 03 │ 07 │ 0B │ 0F │
└────┴────┴────┴────┘
注意:是按列填充,不是按行!所有的加密操作都在這個矩陣上進行。
5. AES 輪函式的四個步驟
每一輪(除了最後一輪)都執行這四個操作:
步驟 1:SubBytes(位元組替換)
每個位元組通過一個叫做 S-Box 的查找表進行替換。
┌─────────────────────────────────────────────┐
│ 輸入位元組 → 查表 → 輸出位元組 │
│ │
│ 例如:0x53 → S-Box[0x53] → 0xED │
└─────────────────────────────────────────────┘為什麼需要這步?
這是 AES 中唯一的非線性操作。沒有它,AES 就只是一堆線性操作(XOR、移位、乘法),可以用線性代數直接求解。
S-Box 的設計基於有限域的乘法逆元,具有良好的密碼學特性:
- 沒有不動點(沒有 x 使得 S(x) = x)
- 沒有反不動點(沒有 x 使得 S(x) = x ⊕ 0xFF)
- 高度非線性
步驟 2:ShiftRows(行移位)
矩陣的每一行向左循環移位不同的位置:
第 0 行:不移位
第 1 行:左移 1 位
第 2 行:左移 2 位
第 3 行:左移 3 位
之前: 之後:
┌────┬────┬────┬────┐ ┌────┬────┬────┬────┐
│ 00 │ 04 │ 08 │ 0C │ │ 00 │ 04 │ 08 │ 0C │
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ 01 │ 05 │ 09 │ 0D │ → │ 05 │ 09 │ 0D │ 01 │
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ 02 │ 06 │ 0A │ 0E │ │ 0A │ 0E │ 02 │ 06 │
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ 03 │ 07 │ 0B │ 0F │ │ 0F │ 03 │ 07 │ 0B │
└────┴────┴────┴────┘ └────┴────┴────┴────┘為什麼需要這步?
確保每一列的位元組在下一輪會被分散到不同的列。這提供了擴散——一個輸入位元的變化會影響整個輸出。
步驟 3:MixColumns(列混合)
每一列被視為一個多項式,與一個固定的多項式相乘(在 GF(2⁸) 有限域中):
┌────┐ ┌────────────────┐ ┌────┐
│ a₀ │ │ 02 03 01 01 │ │ b₀ │
│ a₁ │ × │ 01 02 03 01 │ = │ b₁ │
│ a₂ │ │ 01 01 02 03 │ │ b₂ │
│ a₃ │ │ 03 01 01 02 │ │ b₃ │
└────┘ └────────────────┘ └────┘為什麼需要這步?
這是擴散的另一個來源。它確保一列中的每個位元組都會影響該列的所有位元組。結合 ShiftRows,幾輪之後輸入的每個位元都會影響輸出的每個位元。
步驟 4:AddRoundKey(輪金鑰加)
狀態矩陣與該輪的子金鑰進行 XOR:
State' = State ⊕ RoundKey為什麼需要這步?
這是金鑰材料被引入的地方。沒有這步,加密就與金鑰無關——任何人都可以「解密」。
6. 完整的 AES 流程
明文(16 位元組)
│
▼
AddRoundKey(初始輪金鑰)
│
▼
┌─────────────────────────┐
│ 重複 N-1 輪: │
│ SubBytes │
│ ShiftRows │
│ MixColumns │
│ AddRoundKey │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 最後一輪(無 MixColumns)│
│ SubBytes │
│ ShiftRows │
│ AddRoundKey │
└─────────────────────────┘
│
▼
密文(16 位元組)
N = 10(AES-128)、12(AES-192)、14(AES-256)為什麼最後一輪沒有 MixColumns?這是為了讓加密和解密更對稱——解密時第一輪也沒有 MixColumns。
7. 金鑰擴展
AES 需要為每一輪產生一個子金鑰。這通過金鑰擴展演算法完成:
原始金鑰(128/192/256 位元)
│
▼
┌─────────────────────────────────────────┐
│ 金鑰擴展演算法: │
│ - 使用 S-Box │
│ - 使用輪常數(Rcon) │
│ - 每輪金鑰依賴前一輪金鑰 │
└─────────────────────────────────────────┘
│
▼
11/13/15 個輪金鑰(每個 128 位元)金鑰擴展確保:
- 原始金鑰的任何位元變化都會影響多個輪金鑰
- 無法從一個輪金鑰推導出其他輪金鑰(不知道原始金鑰的情況下)
8. 為什麼是 128 位元區塊?
安全考量
64 位元區塊(DES):2³² 個區塊後發生碰撞(約 32GB)
128 位元區塊(AES):2⁶⁴ 個區塊後發生碰撞(約 256 EB)128 位元區塊讓你可以安全處理海量資料而不必擔心生日攻擊。
效能考量
現代 CPU 的暫存器:64 位元或更大
128 位元 = 2 × 64 位元操作
256 位元區塊會更慢且收益遞減128 位元是安全性和效能的甜蜜點。
9. AES 的安全性
目前狀態
AES-128:安全
AES-192:安全
AES-256:安全
最佳已知攻擊:
- AES-128 的複雜度從 2¹²⁸ 降到約 2¹²⁶·¹
- 這在實務上仍然不可行
- 沒有實際的破解方法相關金鑰攻擊
如果攻擊者能用多個相關金鑰加密:
AES-256 可能比 AES-128 更脆弱
但在實際應用中:
- 金鑰應該是隨機的
- 不存在「相關金鑰」
- AES-256 仍然安全側通道攻擊
AES 本身是安全的,但實作可能洩漏資訊:
- 時序攻擊:不同操作用時不同
- 快取攻擊:S-Box 查找的快取行為
- 電力分析:消耗的電力與資料相關
防禦:
- 使用硬體加速(AES-NI)
- 常數時間實作
- 不要自己實作 AES10. 程式碼範例:AES 的基本使用
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import os
def aes_encrypt_block(plaintext: bytes, key: bytes) -> bytes:
"""
使用 AES-ECB 加密單個區塊(僅用於理解,不要在生產中使用 ECB!)
"""
if len(plaintext) != 16:
raise ValueError("AES 區塊必須是 16 位元組")
if len(key) not in (16, 24, 32):
raise ValueError("AES 金鑰必須是 16、24 或 32 位元組")
cipher = Cipher(algorithms.AES(key), modes.ECB())
encryptor = cipher.encryptor()
return encryptor.update(plaintext) + encryptor.finalize()
def aes_decrypt_block(ciphertext: bytes, key: bytes) -> bytes:
"""
使用 AES-ECB 解密單個區塊
"""
cipher = Cipher(algorithms.AES(key), modes.ECB())
decryptor = cipher.decryptor()
return decryptor.update(ciphertext) + decryptor.finalize()
# 示範
if __name__ == "__main__":
# 產生隨機金鑰
key = os.urandom(32) # AES-256
# 明文必須是 16 位元組
plaintext = b"Hello, AES-256!!"
# 加密和解密
ciphertext = aes_encrypt_block(plaintext, key)
decrypted = aes_decrypt_block(ciphertext, key)
print(f"明文: {plaintext}")
print(f"密文: {ciphertext.hex()}")
print(f"解密: {decrypted}")11. 常見誤區
| 誤區 | 現實 |
|---|---|
| 「AES-256 比 AES-128 安全兩倍」 | AES-256 有 2^256 種金鑰,是 AES-128 的 2^128 倍,但兩者在實務上都是不可破解的 |
| 「AES 加密是安全的」 | AES 區塊加密是安全的,但工作模式的選擇同樣重要(ECB 是不安全的) |
| 「更長的金鑰一定更好」 | 對於量子電腦,AES-256 確實更好。但對於經典電腦,AES-128 已經足夠 |
| 「AES 解密比加密慢」 | 使用硬體加速時,兩者速度相同 |
12. 本章小結
三點要記住:
AES 使用 SPN 結構。 每輪的四個步驟(SubBytes、ShiftRows、MixColumns、AddRoundKey)各有其目的:非線性、擴散、更多擴散、引入金鑰。
128 位元區塊解決了 DES 的生日攻擊問題。 你可以用同一個金鑰安全加密 EB 級的資料,而不用擔心碰撞。
AES 本身是安全的,但使用方式很重要。 選擇正確的工作模式(GCM、CBC+HMAC)比選擇 AES-128 還是 AES-256 更重要。
13. 下一步
我們理解了 AES 對單個區塊的加密。但現實中的資料很少剛好是 16 位元組。我們如何加密任意長度的資料?
在下一篇文章中,我們將探討:AES 的工作模式——為什麼 ECB 是災難,CBC 需要注意什麼,以及為什麼 GCM 成為了現代預設選擇。
