4. AES 算法原理:从 SPN 到四个核心操作

说实话,AES 是我在项目中接触最多的对称加密算法。最早看它的设计文档时,我第一反应是:“这结构怎么这么规整?”后来才明白,这种规整恰恰是它安全高效的根基。AES 的核心是 SPN 结构(Substitution-Permutation Network),说白了就是重复做“替换—置乱—混合—加密钥”这四个操作,每一轮都让数据扩散得更充分。我个人习惯把把这四个操作记成:替、移、混、加,好记又好用。

4.1 SPN 结构:AES 的骨架

AES 不是 Feistel 网络,而是 SPN。Feistel 网络每轮只处理一半数据,SPN 则一次处理整个状态矩阵。这样做的好处是——扩散更快。你在第一轮字节代换里改一个字节,到第二轮列混合之后,整个矩阵的 16 个字节都会受影响。我曾经在性能调优时测试过,AES-128 做 10 轮,每轮 4 个操作,流水线设计得当的话,单轮延迟可以控制到 4 个时钟周期左右。

AES 加密单轮 SPN 结构流程 明文状态矩阵 AddRoundKey (初始) 轮密钥 K0 SubBytes (S-box) 非线替换 ShiftRows 行循环移位 MixColumns 列内线性变换 AddRoundKey (轮) 轮密钥 Kr (r=1..10/12/14) 最后一轮省略 MixColumns;密钥扩展独立于主流程,每轮生成不同轮密钥。 AES-128 共 10 轮,AES-192 共 12 轮,AES-256 共 14 轮。

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]];
}
避坑: 我曾经遇到过有人把 S-box 顺序背错了,结果解密时一直报错。千万记住:加密用 S-box,解密用逆 S-box (InvSubBytes),这两个表不同。

4.3 行移位(ShiftRows)—— 让列之间的数据“串门”

行移位很简单:把状态矩阵的每一行向左循环移位。第 0 行不移,第 1 行移 1 个字节,第 2 行移 2 个,第 3 行移 3 个。你想想看,原本在同一列里的字节,经过 ShiftRows 后分散到了不同列,这样后面的 MixColumns 才能把列间相关性打散。这部分没有太多陷阱,唯一要注意的是解密时要做反向移位(InvShiftRows)。

行移位偏移量
行号加密偏移解密偏移
000
113
222
331

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,但值得。

个人经验: 纯软件实现时,千万别在循环里逐位算乘法,一定要查表。GF(2⁸) 上的乘 2 其实就是左移一位再异或 0x1B(如果移位后溢出)。我见过一个新手写的版本,一个 MixColumns 花了上千个周期,吓得我连夜帮他重写了。

4.5 轮密钥加(AddRoundKey)—— 简单但关键

轮密钥加就是把轮密钥与状态矩阵按字节异或。非常简单,但它是整个 SPN 结构中唯一引入密钥信息的步骤。如果没有它,AES 就变成了一个纯确定性的置换,加密结果只取决于数据,密钥完全不起作用。你想想,还有比这更可怕的漏洞吗?

轮密钥来自密钥扩展(Key Expansion),它从原始密钥生成每一轮所需的 128 位子密钥。AES-128 需要 11 个子密钥(K0 到 K10),每轮用 1 个,初始再加 1 个。密钥扩展过程同样涉及 S-box 查表和轮常量 Rcon。这部分我放到后续章节详细讲,这里先记住:轮密钥加是所有回合的“调味料”,没有它,前面的替换和置乱就只是排列组合的游戏。

4.6 小结与对比

AES 四个操作的作用对比
操作类型扩散范围是否可逆计算代价
SubBytes非线性单字节是(逆 S-box)低(查表)
ShiftRows线性排列行间是(反向移位)极低
MixColumns线性扩散整列是(逆矩阵)中
AddRoundKey密钥混合全局自逆(异或)低

你可以把 AES 的每一轮想象成“揉面”:先给每个字节加调料(SubBytes),再换个位置(ShiftRows),然后用力揉一整列(MixColumns),最后撒一把新盐(AddRoundKey)。这样重复 10 遍以上,原始数据和密钥的关系就彻底“揉匀”了,任何一点微小改变都会影响到最终输出的一半左右比特——这叫做 雪崩效应。AES 的设计完美地实现了这一点。