从 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 是如何工作的——不靠数学也能懂的解释,以及为什么它成为了现代加密的标准。
