從 DES 說起:分組加密的基本思想
1. 為什麼要關心這個問題?
你可能永遠不會在生產環境中使用 DES。它在 1999 年就被正式宣告不安全了。
那為什麼還要學它?
因為 DES 定義了分組加密的基本模式——分組、輪函式、金鑰排程——這些概念在 AES、Blowfish、甚至現代的 ChaCha20 中都能看到。理解 DES 的設計和失敗,能幫助你理解為什麼 AES 要那樣設計,以及為什麼某些配置是安全的而其他的不是。
更實際的是:你可能會在遺留系統中遇到 DES 或 3DES。知道它們為什麼不安全,比只知道「不要用」更有價值。
2. 定義
分組加密(Block Cipher) 是一種將固定長度的明文區塊轉換為相同長度密文區塊的加密演算法。
明文區塊(64 位元)+ 金鑰 → 密文區塊(64 位元)關鍵特徵:
- 固定區塊大小: DES 是 64 位元,AES 是 128 位元
- 確定性: 相同的區塊和金鑰總是產生相同的密文
- 可逆: 給定金鑰,密文可以解密回明文
與串流加密(Stream Cipher) 對比:
- 串流加密逐位元或逐位元組處理
- 分組加密一次處理一整個區塊
3. 為什麼要分組
問題:如何加密任意長度的資料?
最簡單的想法是為每個字元使用一個替換表(像凱薩密碼)。但這有致命的問題:
明文: ATTACK AT DAWN
密文: ZGGZXP ZG WZTK
問題:相同的字母 → 相同的密文
頻率分析可以破解解決方案 1:大區塊
如果我們把多個字元組成一個單位來處理呢?
區塊 1: "ATTA" → "K7X2"
區塊 2: "CK A" → "P9M1"
區塊 3: "T DA" → "Q3Z8"
區塊 4: "WN " → "L5Y4"現在相同的字母在不同位置會有不同的輸出,因為它們與不同的相鄰字元組合。
解決方案 2:複雜的轉換
光是分組還不夠。我們需要讓每個輸入位元影響多個輸出位元,使得統計分析變得困難。
這就是為什麼分組加密使用多輪複雜操作:
- 替換(Substitution):用一個值替換另一個值
- 置換(Permutation):重新排列位元的位置
- 混合(Mixing):結合不同的位元
4. DES 的結構:Feistel 網路
DES 使用一種叫做 Feistel 網路 的結構。這個設計非常優雅:加密和解密幾乎使用相同的電路。
基本流程
輸入(64 位元)
│
▼
┌───────────┐
│ 初始置換 │
└───────────┘
│
▼
┌─────────────────────────────────┐
│ 16 輪 Feistel │
│ │
│ L₀ ──────────┬────────► R₀ │
│ │ │
│ ▼ │
│ ┌───────┐ │
│ │ F │◄── K₁ │
│ └───────┘ │
│ │ │
│ ▼ │
│ L₁ = R₀ XOR │
│ R₁ = L₀ ⊕ F(R₀, K₁) │
│ │ │
│ ... │
│ (重複 16 次) │
└─────────────────────────────────┘
│
▼
┌───────────┐
│ 最終置換 │
└───────────┘
│
▼
輸出(64 位元)Feistel 輪的細節
每一輪:
- 將 64 位元區塊分成左右兩半(各 32 位元)
- 右半部分經過 F 函式和子金鑰處理
- F 函式的輸出與左半部分 XOR
- 左右交換,進入下一輪
def feistel_round(L, R, subkey):
new_L = R
new_R = L ^ F(R, subkey)
return new_L, new_R為什麼這個設計很聰明
解密只需要反向使用子金鑰:
# 加密:K1, K2, K3, ... K16
# 解密:K16, K15, K14, ... K1
def decrypt_round(L, R, subkey):
new_R = L
new_L = R ^ F(L, subkey) # 完全相同的 F 函式!
return new_L, new_R在硬體時代,這意味著同一個晶片可以做加密和解密,只需要改變金鑰順序。
5. F 函式:DES 的核心
F 函式是 DES 安全性的來源。它包含:
擴展置換(E)
32 位元 → 48 位元
原始位元被重複使用,建立冗餘
這使得輸入的小變化擴散到輸出的多個位置與子金鑰 XOR
48 位元資料 ⊕ 48 位元子金鑰S-盒(Substitution Box)
┌────────────────────────────────────────────┐
│ 8 個 S-盒,每個: │
│ - 輸入:6 位元 │
│ - 輸出:4 位元 │
│ - 非線性映射(這是安全性的關鍵!) │
└────────────────────────────────────────────┘
48 位元 → 8 × 4 = 32 位元S-盒是 DES 中唯一的非線性組件。沒有它,DES 就只是一堆 XOR 和置換,可以用線性代數破解。
置換(P)
32 位元重新排列位置
確保一個 S-盒的輸出會影響下一輪多個 S-盒的輸入6. 金鑰排程:從 56 位元到 16 個子金鑰
DES 使用 56 位元有效金鑰(64 位元中有 8 位元是奇偶校驗)產生 16 個 48 位元的子金鑰。
原始金鑰(56 位元)
│
▼
┌───────────┐
│ PC-1 置換 │ (選擇 56 位元)
└───────────┘
│
▼
分成 C₀ 和 D₀(各 28 位元)
│
▼
┌─────────────────────────────┐
│ 對於每一輪 i = 1 到 16: │
│ Cᵢ = 左移 Cᵢ₋₁ │
│ Dᵢ = 左移 Dᵢ₋₁ │
│ Kᵢ = PC-2(Cᵢ, Dᵢ) │
└─────────────────────────────┘
│
▼
16 個子金鑰 K₁, K₂, ... K₁₆7. DES 為什麼會失敗
致命傷:金鑰太短
56 位元金鑰 = 2⁵⁶ = 72,057,594,037,927,936 種可能
1977 年:這看起來很安全
1998 年:EFF 的 Deep Crack 用 25 萬美元的硬體在 56 小時內破解
1999 年:distributed.net + Deep Crack 在 22 小時內破解
2025 年:一台現代 GPU 幾分鐘內可以窮舉區塊大小也太小
64 位元區塊 = 2³² 對明文/密文後會發生碰撞
在 CBC 模式下,處理約 32GB 資料後就會開始洩漏資訊
這就是「生日攻擊」的問題3DES:權宜之計
為了延長 DES 的壽命,人們發明了 Triple DES:
3DES 加密:E(K3, D(K2, E(K1, plaintext)))
加密 → 解密 → 加密
使用三個不同的金鑰:168 位元有效金鑰
或兩個金鑰(K1 = K3):112 位元有效金鑰為什麼是「加密-解密-加密」而不是「加密-加密-加密」?
因為如果 K1 = K2 = K3,3DES 就變成普通的 DES,保持向後相容。
3DES 的問題
- 慢: 需要三次 DES 運算
- 區塊大小還是 64 位元: 生日攻擊問題依然存在
- 設計過時: 基於 1970 年代的技術
8. 從 DES 到 AES 的演進
| 特性 | DES | 3DES | AES |
|---|---|---|---|
| 年份 | 1977 | 1998 | 2001 |
| 金鑰長度 | 56 位元 | 112/168 位元 | 128/192/256 位元 |
| 區塊大小 | 64 位元 | 64 位元 | 128 位元 |
| 結構 | Feistel | Feistel | SPN |
| 輪數 | 16 | 48 | 10/12/14 |
| 狀態 | 已破解 | 已棄用 | 安全 |
AES 在 2001 年的競賽中勝出,取代了 DES 成為新標準。它使用不同的結構(SPN 而非 Feistel),我們將在下一篇文章中詳細介紹。
9. 程式碼範例:理解 Feistel 結構
"""
簡化的 Feistel 結構示範
注意:這不是真正的 DES,僅用於教學目的
"""
def simple_f_function(right_half, subkey):
"""簡化的 F 函式"""
# 真正的 DES 有複雜的 S-盒和置換
# 這裡只是示範基本概念
return (right_half ^ subkey) & 0xFFFFFFFF
def feistel_encrypt(plaintext, subkeys):
"""
Feistel 加密
plaintext: 64 位元整數
subkeys: 16 個子金鑰的列表
"""
# 分成左右兩半
L = (plaintext >> 32) & 0xFFFFFFFF
R = plaintext & 0xFFFFFFFF
# 16 輪
for i in range(16):
new_L = R
new_R = L ^ simple_f_function(R, subkeys[i])
L, R = new_L, new_R
# 最後交換並合併
return (R << 32) | L
def feistel_decrypt(ciphertext, subkeys):
"""
Feistel 解密
只需要反向使用子金鑰!
"""
return feistel_encrypt(ciphertext, subkeys[::-1])
# 示範
if __name__ == "__main__":
# 產生假的子金鑰(真正的 DES 有金鑰排程演算法)
import secrets
subkeys = [secrets.randbits(32) for _ in range(16)]
plaintext = 0x0123456789ABCDEF
ciphertext = feistel_encrypt(plaintext, subkeys)
decrypted = feistel_decrypt(ciphertext, subkeys)
print(f"明文: {plaintext:016X}")
print(f"密文: {ciphertext:016X}")
print(f"解密: {decrypted:016X}")
print(f"成功: {plaintext == decrypted}")10. 常見誤區
| 誤區 | 現實 |
|---|---|
| 「DES 只是金鑰短,演算法沒問題」 | 64 位元區塊大小也是問題,會導致生日攻擊 |
| 「3DES 有 168 位元金鑰所以很安全」 | 實際安全性約 112 位元(中間相遇攻擊),而且區塊大小問題依然存在 |
| 「Feistel 結構已經過時了」 | 許多現代加密仍使用 Feistel 變體(如 Blowfish、Twofish) |
| 「更多輪數 = 更安全」 | 輪數要和 F 函式的強度匹配,太多輪會浪費效能 |
11. 本章小結
三點要記住:
分組加密將資料分成固定大小的區塊處理。 這解決了頻率分析問題,但帶來了「如何處理多個區塊」的新問題(這是下下篇的主題)。
Feistel 結構是優雅的設計。 它讓加密和解密共用同一個電路,只需要反向使用子金鑰。這個設計影響了幾十年的密碼學發展。
DES 失敗是因為參數太小。 56 位元金鑰和 64 位元區塊在 1977 年看起來夠用,但技術進步使它們變得不安全。設計加密系統時要考慮未來幾十年的安全性。
12. 下一步
我們理解了分組加密的基本思想和 Feistel 結構。但 AES 使用了不同的結構(SPN),而且看起來更複雜。
在下一篇文章中,我們將探討:AES 是如何運作的——不靠數學也能懂的解釋,以及為什麼它成為了現代加密的標準。
