4. AES 算法原理:从 SPN 到四个核心操作
说实话,AES 是我在项目中接触最多的对称加密算法。最早看它的设计文档时,我第一反应是:“这结构怎么这么规整?”后来才明白,这种规整恰恰是它安全高效的根基。AES 的核心是 SPN 结构(Substitution-Permutation Network),说白了就是重复做“替换—置乱—混合—加密钥”这四个操作,每一轮都让数据扩散得更充分。我个人习惯把把这四个操作记成:替、移、混、加,好记又好用。
4.1 SPN 结构:AES 的骨架
AES 不是 Feistel 网络,而是 SPN。Feistel 网络每轮只处理一半数据,SPN 则一次处理整个状态矩阵。这样做的好处是——扩散更快。你在第一轮字节代换里改一个字节,到第二轮列混合之后,整个矩阵的 16 个字节都会受影响。我曾经在性能调优时测试过,AES-128 做 10 轮,每轮 4 个操作,流水线设计得当的话,单轮延迟可以控制到 4 个时钟周期左右。
4.2 字节代换(SubBytes)—— 唯一非线性环节
字节代换就是查 S-box。AES 的 S-box 是一个 16×16 的查找表,输入一个字节,输出另一个字节。这个表是精心构造过的,能抵抗线性密码分析和差分密码分析。我记得有次同事问我:“为什么不直接用随机置换?”答案很简单——代数性质。AES 的 S-box 是仿射变换和有限域 GF(2⁸) 上乘法逆的复合,保证了非线性同时避免了不动点(S-box(0) ≠ 0)。
实现上,我建议用查表法,256 个字节预计算好。下面是一个常见的 C 实现片段:
// 预计算的 S-box (0x00 - 0xFF)
static const uint8_t sbox[256] = {
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
// ... 余下 240 个值
};
void sub_bytes(uint8_t state[4][4]) {
for (int c = 0; c < 4; c++)
for (int r = 0; r < 4; r++)
state[r][c] = sbox[state[r][c]];
}
4.3 行移位(ShiftRows)—— 让列之间的数据“串门”
行移位很简单:把状态矩阵的每一行向左循环移位。第 0 行不移,第 1 行移 1 个字节,第 2 行移 2 个,第 3 行移 3 个。你想想看,原本在同一列里的字节,经过 ShiftRows 后分散到了不同列,这样后面的 MixColumns 才能把列间相关性打散。这部分没有太多陷阱,唯一要注意的是解密时要做反向移位(InvShiftRows)。
| 行号 | 加密偏移 | 解密偏移 |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 1 | 3 |
| 2 | 2 | 2 |
| 3 | 3 | 1 |
4.4 列混合(MixColumns)—— 扩散的主力
MixColumns 是 AES 中计算量最大的步骤,也是体现扩散能力的关键。它对状态矩阵的每一列做线性变换,相当于在 GF(2⁸) 上乘一个固定的多项式矩阵:
// 列混合核心运算(加密时)
state'[0][c] = 2 * state[0][c] ^ 3 * state[1][c] ^ state[2][c] ^ state[3][c];
state'[1][c] = state[0][c] ^ 2 * state[1][c] ^ 3 * state[2][c] ^ state[3][c];
state'[2][c] = state[0][c] ^ state[1][c] ^ 2 * state[2][c] ^ 3 * state[3][c];
state'[3][c] = 3 * state[0][c] ^ state[1][c] ^ state[2][c] ^ 2 * state[3][c];
这里的乘法是 GF(2⁸) 上的乘法,不是普通整数乘法。实现时一般用查表代替(比如 xtime 函数)。我在嵌入式设备上优化 AES 时,曾把 MixColumns 改成了查四个 256 字节的表,从 60 个周期降到了 12 个周期——代价是多占 1KB ROM,但值得。
4.5 轮密钥加(AddRoundKey)—— 简单但关键
轮密钥加就是把轮密钥与状态矩阵按字节异或。非常简单,但它是整个 SPN 结构中唯一引入密钥信息的步骤。如果没有它,AES 就变成了一个纯确定性的置换,加密结果只取决于数据,密钥完全不起作用。你想想,还有比这更可怕的漏洞吗?
轮密钥来自密钥扩展(Key Expansion),它从原始密钥生成每一轮所需的 128 位子密钥。AES-128 需要 11 个子密钥(K0 到 K10),每轮用 1 个,初始再加 1 个。密钥扩展过程同样涉及 S-box 查表和轮常量 Rcon。这部分我放到后续章节详细讲,这里先记住:轮密钥加是所有回合的“调味料”,没有它,前面的替换和置乱就只是排列组合的游戏。
4.6 小结与对比
| 操作 | 类型 | 扩散范围 | 是否可逆 | 计算代价 |
|---|---|---|---|---|
| SubBytes | 非线性 | 单字节 | 是(逆 S-box) | 低(查表) |
| ShiftRows | 线性排列 | 行间 | 是(反向移位) | 极低 |
| MixColumns | 线性扩散 | 整列 | 是(逆矩阵) | 中 |
| AddRoundKey | 密钥混合 | 全局 | 自逆(异或) | 低 |
你可以把 AES 的每一轮想象成“揉面”:先给每个字节加调料(SubBytes),再换个位置(ShiftRows),然后用力揉一整列(MixColumns),最后撒一把新盐(AddRoundKey)。这样重复 10 遍以上,原始数据和密钥的关系就彻底“揉匀”了,任何一点微小改变都会影响到最终输出的一半左右比特——这叫做 雪崩效应。AES 的设计完美地实现了这一点。