Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

AES 是如何工作的(不靠数学也能懂)

| , 6 minutes reading.

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)
- 常数时间实现
- 不要自己实现 AES

10. 代码示例: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. 本章小结

三点要记住:

  1. AES 使用 SPN 结构。 每轮的四个步骤(SubBytes、ShiftRows、MixColumns、AddRoundKey)各有其目的:非线性、扩散、更多扩散、引入密钥。

  2. 128 位块解决了 DES 的生日攻击问题。 你可以用同一个密钥安全加密 EB 级的数据,而不用担心碰撞。

  3. AES 本身是安全的,但使用方式很重要。 选择正确的工作模式(GCM、CBC+HMAC)比选择 AES-128 还是 AES-256 更重要。

13. 下一步

我们理解了 AES 对单个块的加密。但现实中的数据很少刚好是 16 字节。我们如何加密任意长度的数据?

在下一篇文章中,我们将探讨:AES 的工作模式——为什么 ECB 是灾难,CBC 需要注意什么,以及为什么 GCM 成为了现代默认选择。