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 成为了现代默认选择。
