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 位的子密钥,分别用在每一轮里。
扩展流程大致是这样的:
- 把 128 位原始密钥分成 4 个 32 位的字:MK0, MK1, MK2, MK3。
- 用固定的系统参数 FK(4 个 32 位常量)对这 4 个字做首次异或变换,得到 (K0, K1, K2, K3)。
- 然后通过迭代公式计算后面的 31 个子密钥。公式是:
其中 CK 是另一个固定参数,共 32 个。Ki = Ki-4 ⊕ L'(S盒(Ki-3 ⊕ Ki-2 ⊕ Ki-1 ⊕ CKi-4))
这看起来有点绕,但我举个例子就清楚了。假设我们拿到的密钥是全 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 结构最大的优点。我记得有个同事刚入门时以为解密需要单独写一套逻辑,其实完全不需要。
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%。这在高频密钥轮转的场景勉强够用。
实现时要注意以下三点:
- S 盒查表:建议直接用查找表,不要用位运算逐个算,后者速度会慢几倍。
- L 变换的循环移位:C 语言里用宏或内联函数实现 rotl,不要搞成函数调用,能省不少开销。
- 密钥扩展提前计算:把 32 个子密钥提前算好存起来,以后每块数据加密时直接调用,避免重复计算。
如果你在嵌入式环境里跑 SM4,可以用查表 + 循环展开的思路,把 32 轮写成手动的流水线。我在某款国产芯片上这么做过,性能提升了将近 40%。
好了,关于 SM4 的原理归纳就到这里。记住了:Feistel 结构、32 轮、S 盒 + L 变换、密钥扩展对称。这些核心概念理解了,后面的应用层开发就水到渠成了。