列混合、轮密钥加与密钥扩展:AES的“搅动”核心
好了,上一章我们聊了字节代换和行移位,那只是AES热身。真正让AES变得“复杂”且难以破解的,是列混合、轮密钥加和背后的密钥扩展。我个人觉得,这三块才是AES的灵魂。特别是列混合,我第一次看公式差点被GF(2^8)吓退,但后来发现,其实没那么玄乎。
4.1 列混合(MixColumns)
列混合的作用是什么?说白了,就是把状态矩阵的每一列当成一个向量,和某个固定矩阵做乘法。注意,这个乘法不是普通乘法,是在有限域GF(2^8)上的运算,加法就是异或,乘法稍微复杂点。
数学公式
对于每一列 [a0, a1, a2, a3]^T,输出 [b0, b1, b2, b3]^T 等于:
b0 = (02)·a0 ⊕ (03)·a1 ⊕ (01)·a2 ⊕ (01)·a3
b1 = (01)·a0 ⊕ (02)·a1 ⊕ (03)·a2 ⊕ (01)·a3
b2 = (01)·a0 ⊕ (01)·a1 ⊕ (02)·a2 ⊕ (03)·a3
b3 = (03)·a0 ⊕ (01)·a1 ⊕ (01)·a2 ⊕ (02)·a3
这里的 (02)·a0 表示用0x02乘a0,乘法规则是:左移一位,如果结果超过255(即0xFF),再异或0x1B。为什么是0x1B?因为GF(2^8)的不可约多项式是 x^8 + x^4 + x^3 + x + 1,0x1B 就是它的二进制值。嗯,这里要注意,实际实现时通常用查表法,我当年自己写软件实现时也是用查表,速度能快不少。
我在项目里见过什么坑?
有一次做嵌入式安全模块,同事直接用了C语言的乘法——结果当然不对。AES的列混合不是整型乘法,必须用GF(2^8)的规则。我后来帮他把乘法全部换成256字节的预计算表,问题就解决了。
逆列混合
解密时用的是逆列混合,乘数矩阵不同:
b0 = (0E)·a0 ⊕ (0B)·a1 ⊕ (0D)·a2 ⊕ (09)·a3
b1 = (09)·a0 ⊕ (0E)·a1 ⊕ (0B)·a2 ⊕ (0D)·a3
b2 = (0D)·a0 ⊕ (09)·a1 ⊕ (0E)·a2 ⊕ (0B)·a3
b3 = (0B)·a0 ⊕ (0D)·a1 ⊕ (09)·a2 ⊕ (0E)·a3
这些乘数(0E、0B、0D、09)看起来比02、03复杂,但查表法仍然适用。我记得有本经典书《The Design of Rijndael》里专门讨论了这些常数的选择,都是为了扩散性最优。你想想看,如果列混合只是简单的异或,那AES的安全性就要大打折扣。
4.2 轮密钥加(AddRoundKey)
轮密钥加是所有轮操作中最简单的一步:将状态矩阵的每个字节与对应位置的轮密钥字节做异或。说白了,就是“加”了个密钥,但在二元域中,加就是异或。
为什么是异或?
因为异或运算在加法中可逆,自己就是自己的逆运算。解密时,同样的轮密钥加一遍,就能恢复原状。我在安全芯片项目中经常利用这个特性做校验:对明文加密一次,再解密一次,检查是否能还原。如果密钥扩展或列混合实现错了,这一步往往就能暴露问题。
轮密钥加在每一轮都会执行,包括第0轮(初始轮密钥加)和最后一轮。注意,AES-256有14轮,但最后一轮不做列混合,这一点我后面还会强调。
实现提示
轮密钥加可以用128位异或指令(如AES-NI中的PXOR)直接做,极快。但在软件实现中,记得确保字节顺序与网络字节序一致。我遇到过一个大端平台上的项目,轮密钥加始终对不上,后来发现是密钥扩展时字节序没处理好。
4.3 密钥扩展(Key Expansion)
密钥扩展是AES中最绕的部分,但也是设计的精华。它负责从原始密钥(128/192/256位)生成所有轮密钥(Round Keys)。对于AES-256,需要生成15个128位的轮密钥(第0轮到第14轮)。
核心流程
- 原始密钥按4字节一列分成Nk列(Nk=4/6/8 分别对应128/192/256位)。
- 每生成一个新列,都基于前一列和第(Nk)列前的列运算。
- 当索引 i 是 Nk 的倍数时,要执行 RotWord、SubWord 和 Rcon 加常数。
- 对于AES-256,当 i 是 4 的倍数时(Nk=8),还需要额外一次 SubWord。
RotWord:将一个4字节字循环左移1字节。(例如 [b0,b1,b2,b3] → [b1,b2,b3,b0])
SubWord:对字中每个字节应用S盒。
Rcon:轮常数,一个4字节值,只有最低字节非零。Rcon[i/Nk] = [02^(i/Nk-1), 0, 0, 0] (在GF上计算)。
一张SVG图说明密钥扩展结构
下面这张图展示了AES-256密钥扩展时,前几个轮密钥的生成关系。注意,AES-256的Nk=8,所以每8个字为一个迭代块,前4个字使用直接异或,后4个字中每4个字做一次SubWord。
4.4 AES-256的完整轮结构
现在我们把所有操作串起来。AES-256加密过程如下:
| 步骤 | 操作 | 备注 |
|---|---|---|
| 初始轮 | AddRoundKey(轮密钥0) | 密钥扩展后的第一个128位 |
| 第1~13轮 | SubBytes → ShiftRows → MixColumns → AddRoundKey | 每一轮用不同的轮密钥 |
| 第14轮(最后一轮) | SubBytes → ShiftRows → AddRoundKey | 无MixColumns |
为什么最后一轮不做列混合?这在设计上是为了保证加解密结构的对称性,让解密也能用同样的反向流程。我个人觉得这有点像“少了一味佐料”,但这就是标准,不可改的。我在做FPGA实现时,有人为了省资源把最后一轮的列混合去掉了,结果解密完全对不上——因为解密时最后一轮也不做逆列混合,两边对称才行。
4.5 实现中的注意事项
小心密钥扩展中的字节序:很多标准文档给出的是大端表示,但实际编程语言(如C)在内存中可能是小端。我建议在实现时先统一成一个端序,可以用网络字节序(大端)作为内部表示,与标准文档一致。我曾经因为端序问题,调试密钥扩展整整两天,最终发现是宏定义写反了。
性能优化建议:列混合和密钥扩展都可以通过预计算T表(T-tables)来加速。把SubBytes、ShiftRows和MixColumns合并成4张32位查询表,这样一轮只需要16次查表和16次异或。不过T表可能容易受缓存时限攻击,对于安全要求高的场景,要慎用。