SM4 加解密流程:密钥扩展、轮密钥生成与加解密详解
SM4 的流程,说白了就是「密钥先自我繁殖,然后每一轮跟数据打架」。我最早看国标文档的时候,被那一堆 XOR 和移位搞得有点晕,后来手撸一遍才发现,套路其实很固定。
今天我们就把它拆成三块:密钥扩展(怎么从 128 位主密钥变出 32 个轮密钥)、轮密钥生成(32 个轮密钥具体怎么算出来)、加解密过程(数据怎么在 32 轮里翻滚)。
1. 密钥扩展算法
SM4 的主密钥是 128 位,拆成 4 个 32 位字:MK = (MK0, MK1, MK2, MK3)。
第一步,用 4 个系统参数 FK 做「初始化异或」:
K0 = MK0 ⊕ FK0
K1 = MK1 ⊕ FK1
K2 = MK2 ⊕ FK2
K3 = MK3 ⊕ FK3
FK 是固定常数,国标里直接给的,不用自己算。我记得第一次实现时手抖把 FK 写错了,结果加解密出来的数据全是乱码……排查了半天才发现常数抄错了一位。
第二步,迭代生成 32 个轮密钥 rk[i](i=0..31):
for i in range(32):
Ki+4 = Ki ⊕ T'(Ki+1 ⊕ Ki+2 ⊕ Ki+3 ⊕ CKi)
rk[i] = Ki+4
这里的 T' 是一个 合成置换,结构跟加密里的 F 函数很像,区别在于线性变换用的不是 L,而是 L'(一个更简单的移位异或组合)。CK 是 32 个固定轮常数,国标附录里直接列出来了。
2. 轮密钥生成
说白了,上一节讲的迭代就是轮密钥生成的全部。但我要强调几个容易踩坑的地方:
- 轮密钥 32 个,一个都不能少。 网上有些简化实现只存 16 个,加密结果能对上,但解密会失效。
- 轮密钥顺序加密是 rk[0..31],解密就是 rk[31..0]。 我当初为了省内存,把轮密钥直接存在一个数组里,加解密共用,结果忘了在解密时反向遍历——那一上午全浪费在找 bug 上了。
- T' 中的 S 盒跟加密中的 S 盒是同一个。 但线性变换 L' 跟加密的 L 不同:L' 只有 2 个移位异或,而 L 有 3 个。别抄错。
| 项目 | 密钥扩展 T' | 加密轮函数 F |
|---|---|---|
| S 盒 | 同一个 8×8 非线性表 | 同一个 |
| 线性变换 | L'() = B ⊕ (B≪13) ⊕ (B≪23) | L() = B ⊕ (B≪2) ⊕ (B≪10) ⊕ (B≪18) ⊕ (B≪24) |
| 输入 | 3 个 Ki 异或 + 1 个 CK | 前一轮的 Xi 与轮密钥异或 |
| 输出用途 | 作为下一轮的 Ki+4 及轮密钥 | 与 X0…X3 组合进下一轮 |
3. 加解密过程
加密过程可以理解为「把明文喂进一个 32 轮循环,每轮跟一个轮密钥干一架」。具体的:
3.1 初始状态
128 位明文拆成 4 个 32 位字:X0, X1, X2, X3。
3.2 32 轮迭代(i 从 0 到 31)
每一轮计算新的 Xi+4:
Xi+4 = Xi ⊕ F(Xi+1, Xi+2, Xi+3, rk[i])
其中 F 函数内部:
- 先把 Xi+1、Xi+2、Xi+3 和 rk[i] 异或在一起:
B = Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕ rk[i] - 然后过一个 S 盒替换(把 32 位分成 4 个字节,每个字节查 S 盒,得到 4 个新字节,拼回 32 位):
B' = (S盒[B>>24], S盒[(B>>16)&0xFF], S盒[(B>>8)&0xFF], S盒[B&0xFF]) - 再做线性变换 L(32 位循环移位异或):
L(B') = B' ⊕ (B'≪2) ⊕ (B'≪10) ⊕ (B'≪18) ⊕ (B'≪24) - 最后跟 Xi 异或,得到 Xi+4。
3.3 反序变换
32 轮跑完后,我们得到 X32, X33, X34, X35。注意这 4 个字不能直接作为密文输出——要做一个反序:
(Y0, Y1, Y2, Y3) = (X35, X34, X33, X32)
为什么?简单说,这是为了保持加解密结构完全对称。你把加密的最后一步反过来,就是解密的第一步。
4. 解密过程——真的就是反着来
解密时,把密文当作 Y 输入,先反序得到 X35..X32,然后对轮密钥逆序执行相同的 32 轮迭代:
Xi = Xi+4 ⊕ F(Xi+1, Xi+2, Xi+3, rk[31-i])
最后得到 X0..X3,就是明文。
5. 避坑汇总
- S 盒索引:SM4 的 S 盒是 8 位输入、8 位输出,查表时注意位宽。有些实现把 32 位拆成高低 4 位用 16×16 表,那是 AES 的做法,SM4 不通用。
- 循环移位方向:L 和 L' 的移位都是循环左移(≪),不是逻辑左移。如果你的语言里 << 是算术左移,要手动补位。
- 轮常数 CK:用手打的话很容易少写一个 0,建议从国标原文复制,或者用公式自己生成(CK[i] = (4i+1)*7 mod 256 的 4 字节组合),但我建议直接查表省心。
嗯,SM4 的加解密流程其实不复杂,就是步骤多。我当年第一次用 C 写的时候,画了整整一页纸的寄存器分配图才没跑飞。你多跟我一样手撸一遍,保准下次面试能倒背如流。