4. SM4 分组密码算法原理归纳总结

SM4 是我们国密系列里的对称分组密码。说白了,它就是一把密钥同时完成加密和解密。在 Secrets Manager 里,我们会用它来加密存储的密钥数据本身。我个人习惯把 SM4 叫做“轮函数的美学”,因为它结构清晰,轮数固定,实现起来也相当规整。

4.1 算法总览

SM4 的分组长度是 128 位,密钥长度也是 128 位。大家一听“128 位”可能会觉得和 AES 有点像,没错,但它内部结构完全不同。它采用了一种叫做“非线性迭代”的 Feistel 结构。你想想看,32 轮迭代,足够把数据彻底打乱。

这里我先给大家一个直观对比表:

参数项 SM4 规格 备注(我的经验)
分组长度 128 bits (16 字节) 每次处理一块数据
密钥长度 128 bits (16 字节) 密钥扩展后生成 32 轮子密钥
轮数 32 我在项目中试过减少轮数来测试性能,但安全性会断崖式下跌,千万别这么做
结构 Feistel 网络 加密和解密过程的轮子密钥使用顺序相反
基本运算 异或 (XOR) + 非线性变换 硬件实现效率极高

4.2 核心部件:S 盒与线性变换

SM4 的非线性靠的是一个 8×8 的 S 盒(替换盒)。每一个输入字节都会映射到另一个字节。我曾经因为 S 盒实现搞错了一个偏移量,导致某次测试向量死活对不上,折腾了半天才发现问题。嗯,这里要特别记住:S 盒是固定的,而且它有严格的安全性设计。

然后,线性变换部分分为两个级别:

  • L 变换:用在加密/解密的轮函数里。它其实是做循环左移和异或的组合,把 32 位的输入变成新的 32 位。
  • L' 变换:用在密钥扩展里。结构和 L 类似,但移位参数不同。

我个人经验是,理解 L 变换可以用一个很直观的方法:你在纸上画 4 个字节,然后看哪几个字节会被异或到一起。写代码时也建议拆成几步,别直接搞一长串位运算,不然 Debug 时特别痛苦。

4.3 密钥扩展:从 128 位到 32 个子密钥

这个环节我建议大家重点关注。SM4 的密钥扩展过程会生成 32 个 32 位的子密钥,分别用在每一轮里。

扩展流程大致是这样的:

  1. 把 128 位原始密钥分成 4 个 32 位的字:MK0, MK1, MK2, MK3。
  2. 用固定的系统参数 FK(4 个 32 位常量)对这 4 个字做首次异或变换,得到 (K0, K1, K2, K3)。
  3. 然后通过迭代公式计算后面的 31 个子密钥。公式是:
    Ki = Ki-4 ⊕ L'(S盒(Ki-3 ⊕ Ki-2 ⊕ Ki-1 ⊕ CKi-4))
    其中 CK 是另一个固定参数,共 32 个。

这看起来有点绕,但我举个例子就清楚了。假设我们拿到的密钥是全 0x00,那么:

  • MK0=MK1=MK2=MK3=0x00000000
  • K0 = 0x00000000 ⊕ FK0
  • 经过 S 盒和 L' 后,子密钥会迅速产生大量 1 的比特,不会出现全 0 的子密钥。

这有效防止了弱密钥情况。我曾经见过有人直接用全 0 做密钥测试,然后说“怎么好像加密没效果?”其实是子密钥生成后变了样。

4.4 加密与解密过程

加密过程可以概括为:32 轮 Feistel 迭代 + 反序变换。

每一轮都做这么几件事:

  • 把当前状态分成左半边 (XLi) 和右半边 (XRi)
  • 对右半边做轮函数 F:先做 S 盒替换,再做 L 变换,最后和子密钥异或
  • 结果与左半边异或,然后左右交换进入下一轮

用代码片段来表达核心轮函数:

// 假设已经是 32 位无符号整数操作
uint32_t sm4_round_func(uint32_t input, uint32_t rk) {
    uint32_t temp = input ^ rk;
    uint8_t b[4];
    // S 盒替换
    b[0] = Sbox[(temp >> 24) & 0xFF];
    b[1] = Sbox[(temp >> 16) & 0xFF];
    b[2] = Sbox[(temp >> 8) & 0xFF];
    b[3] = Sbox[temp & 0xFF];
    // 合并成 32 位
    uint32_t t = (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3];
    // 线性变换 L
    return t ^ rotl(t, 2) ^ rotl(t, 10) ^ rotl(t, 18) ^ rotl(t, 24);
}

解密时,只需要把子密钥顺序反过来就行。也就是用 rk31, rk30, ... , rk0 去做相同的轮函数。这个对称性是 Feistel 结构最大的优点。我记得有个同事刚入门时以为解密需要单独写一套逻辑,其实完全不需要。

SM4 加密与密钥扩展整体流程 加密数据流 128位明文 第1轮 (X0,X1) 第2~31轮 第32轮 (X32,X33) 反序变换 128位密文 密钥扩展 128位密钥 FK系统参数 K0~K3 初态 S盒+L'迭代 rk0~rk31 32个子密钥 逐轮注入 输入/输出 中间轮状态 密钥处理 最终轮/子密钥 交互关系

4.5 工作模式

在 Secrets Manager 里,我们并不会只用 SM4 裸加密。一般会搭配工作模式使用。我个人最常用的模式有:

  • ECB(电子密码本模式):简单直接,但相同明文会导致相同密文。所以我不太推荐用来加密连续的密钥数据。
  • CBC(密码分组链接模式):常用模式。需要一个 IV(初始向量),每个密文块依赖前一块。我曾经在项目里忘了给 IV 做随机化,结果加解密一致性都没问题了,但安全性直接崩盘。这是个坑,大家要避开。
  • CTR(计数器模式):支持并行加解密,且不需要填充。在云场景里,这个模式对性能很友好。我在 Secrets Manager 的实现里,对于大块密钥数据的加密,倾向选用 CTR。

避坑指南:我曾经在某个版本里,把 ECB 当成了默认模式,结果密钥备份文件里的连续密钥块出现了明显的规律性。后来改成了 CBC 加随机 IV,才彻底解决。所以,哪怕只是临时存储,也务必用 CBC 或 CTR。

4.6 性能与实现要点

SM4 的轮数虽然比 AES 多(32 vs 10/12/14),但每轮的计算量相对较小。我的实测经验是:在相同的软件实现下,SM4 的吞吐量大概能达到 AES-128 的 70%~80%。这在高频密钥轮转的场景勉强够用。

实现时要注意以下三点:

  1. S 盒查表:建议直接用查找表,不要用位运算逐个算,后者速度会慢几倍。
  2. L 变换的循环移位:C 语言里用宏或内联函数实现 rotl,不要搞成函数调用,能省不少开销。
  3. 密钥扩展提前计算:把 32 个子密钥提前算好存起来,以后每块数据加密时直接调用,避免重复计算。

如果你在嵌入式环境里跑 SM4,可以用查表 + 循环展开的思路,把 32 轮写成手动的流水线。我在某款国产芯片上这么做过,性能提升了将近 40%。

好了,关于 SM4 的原理归纳就到这里。记住了:Feistel 结构、32 轮、S 盒 + L 变换、密钥扩展对称。这些核心概念理解了,后面的应用层开发就水到渠成了。