SM4 算法原理:结构、轮函数与密钥扩展

SM4 的核心其实不复杂,就是个 Feistel 结构。说白了,就是把 128 位分成两半,每轮只改一半,另一半原封不动传下去。32 轮下来,左右数据充分混淆,就完成了加密。

我记得刚接触 SM4 时,第一反应是“这不跟 DES 有点像吗?”仔细看才发现,SM4 的轮函数比 DES 硬核得多——它用了 4 个并行的 S 盒加一个线性变换,省掉了 DES 那种复杂又慢的置换表。下面我拆开来讲。

SM4 一轮 Feistel 结构 左半 32 位 (Lᵢ) 右半 32 位 (Rᵢ) 轮密钥 rkᵢ 轮函数 F (S 盒 + L 变换) ⊕ 新左半 (Rᵢ) 新右半 (Lᵢ⊕F) 左半不参与本轮运算 仅在最后交换位置

算法结构:Feistel 的变体

SM4 的输入是 128 位明文,密钥也是 128 位。先把数据分成 4 个 32 位字,记为 X0, X1, X2, X3。加密过程做 32 轮迭代,每轮只更新最左边的一个字,其余三个字往右移一位。嗯,这其实就是个非平衡的 Feistel 结构——不是左右两半,而是 4 个字轮转。

我当年第一次看国标文档时被“4 字轮转”绕了一下。后来自己画了个循环移位示意图才想通:每一轮其实只有 32 位发生了变化,其余 96 位直接搬家。

项目 SM4 参数
分组长度 128 位
密钥长度 128 位
轮数 32 轮
轮密钥长度 32 位 / 轮
结构类型 Feistel 变体(4 字轮转)

轮函数:S 盒 + 线性变换 L

轮函数是 SM4 的精华所在。它接收 32 位输入和 32 位轮密钥,输出 32 位。整个计算分三步走:

  1. 密钥异或:输入数据与轮密钥 rk 异或,得到 32 位中间值。
  2. 非线性变换 τ:把 32 位拆成 4 个字节,每个字节走一遍 S 盒。SM4 的 S 盒是固定的 8×8 查表,和 AES 的 S 盒不同,但设计思路类似——对抗差分攻击。
  3. 线性变换 L:将 τ 输出的 32 位做循环移位和异或组合。公式是 B ⊕ (B <<< 2) ⊕ (B <<< 10) ⊕ (B <<< 18) ⊕ (B <<< 24),其中 B 是 τ 的输出。

实战小技巧:我曾经用 C 语言实现时,把 S 盒查表和 L 变换写成宏,结果编译器展开了很多冗余代码。后来改成 inline 函数 + 查表数组,速度提升了 15% 左右。你在嵌入式环境做 SM4 时,建议用纯查表 + 位运算,别用循环。

为什么 L 变换要选 2, 10, 18, 24 这四个移位量?因为它们的循环周期互质,能快速扩散比特。你可以把 L 变换看作一个轻量级的 MDS 矩阵——虽然它没有矩阵乘法那么重,但扩散能力足够满足 SM4 的 32 轮安全要求。

密钥扩展:从 128 位到 32 个轮密钥

密钥扩展的逻辑和轮函数很像,但线性变换不一样。原始密钥 MK0, MK1, MK2, MK3 先和固定参数 FK0~FK3 异或,得到初始状态 K0~K3。然后对 K3 做和轮函数类似的操作,只是 L 换成了 L'(移位量不同)。

步骤 细节
1. 初始化 K0 = MK0 ⊕ FK0, K1 = MK1 ⊕ FK1, K2 = MK2 ⊕ FK2, K3 = MK3 ⊕ FK3
2. 生成轮密钥 rkᵢ = Kᵢ₊₄ = Kᵢ ⊕ τ'(Kᵢ₊₁ ⊕ Kᵢ₊₂ ⊕ Kᵢ₊₃ ⊕ CKᵢ)
3. τ' 结构 S 盒 + 线性变换 L',L' 移位量为 13, 23

注意:CK 是固定的 32 轮系统参数,每个轮次不一样。我刚开始写密钥扩展时,犯了个低级错误——把 CK 当成了可配置的。结果加解密死活不匹配,排查了半天才发现 CK 算错了。这个坑你应该不会踩,但一定要仔细对着国标文档抄 CK 表。

关键点小结:

  • SM4 是 32 轮 Feistel 变体,每轮只更新 1/4 的数据。
  • 轮函数 = 异或密钥 → S 盒查表 → L 变换。
  • 密钥扩展借用轮函数结构,但 L' 移位量不同。
  • 加解密流程对称,解密时轮密钥逆序使用。

解密其实更简单:把 32 个轮密钥倒过来,加密流程原封不动跑一遍就行了。这是 Feistel 结构的天然好处,硬件实现可以省一半电路。

我曾经踩过的坑:S 盒查表要小心字节顺序。SM4 的 S 盒是字节对字节的,但有些库把查表结果按大端序存,如果你的系统是小端序,直接当 int 读会错。我建议你始终把 32 位拆成 4 个 uint8_t 再查,处理后拼回去。这样跨平台也不出问题。