Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

从 DES 说起:分组加密的基本思想

| , 6 minutes reading.

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 轮的细节

每一轮:

  1. 将 64 位块分成左右两半(各 32 位)
  2. 右半部分经过 F 函数和子密钥处理
  3. F 函数的输出与左半部分 XOR
  4. 左右交换,进入下一轮
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 的问题

  1. 慢: 需要三次 DES 运算
  2. 块大小还是 64 位: 生日攻击问题依然存在
  3. 设计过时: 基于 1970 年代的技术

8. 从 DES 到 AES 的演进

特性DES3DESAES
年份197719982001
密钥长度56 位112/168 位128/192/256 位
块大小64 位64 位128 位
结构FeistelFeistelSPN
轮数164810/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. 本章小结

三点要记住:

  1. 分组加密将数据分成固定大小的块处理。 这解决了频率分析问题,但带来了「如何处理多个块」的新问题(这是下下篇的主题)。

  2. Feistel 结构是优雅的设计。 它让加密和解密共用同一个电路,只需要反向使用子密钥。这个设计影响了几十年的密码学发展。

  3. DES 失败是因为参数太小。 56 位密钥和 64 位块在 1977 年看起来够用,但技术进步使它们变得不安全。设计加密系统时要考虑未来几十年的安全性。

12. 下一步

我们理解了分组加密的基本思想和 Feistel 结构。但 AES 使用了不同的结构(SPN),而且看起来更复杂。

在下一篇文章中,我们将探讨:AES 是如何工作的——不靠数学也能懂的解释,以及为什么它成为了现代加密的标准。