SM4 对称加密算法:从原理到性能调优
SM4 算法,圈内也叫 SMS4,是我国官方发布的第一个商用分组密码标准。说实话,我刚接触它的时候,第一反应是「这不就是类 AES 吗?」—— 后来踩了几个坑才发现,细节上差别还挺大。今天咱们就把它掰开揉碎,从数学原理一直聊到工程优化。
SM4 算法原理:Feistel 结构的中国实践
SM4 采用经典的 Feistel 结构,分组长度 128 位,密钥长度也是 128 位。它不像 AES 那样用 SPN 结构,而是把数据分成左右两半,每轮只处理一半,然后交换。这种结构有个好处:加密和解密可以共用同一套硬件逻辑,只是轮密钥的使用顺序反过来。
我个人习惯把 SM4 的核心流程概括为「32 轮迭代 + 反序变换」。每一轮做的事情其实不复杂:
- 轮函数 F:接收 32 位输入和 32 位轮密钥,输出 32 位
- 合成置换 T:非线性变换 τ 加上线性变换 L
- 非线性变换 τ:4 个并行的 S 盒,每个 8 位输入输出
- 线性变换 L:循环移位 + 异或,扩散性极强
核心公式:第 i 轮,左半部分 Li = Ri-1,右半部分 Ri = Li-1 ⊕ F(Ri-1, Ki)
32 轮跑完后,把最后输出的 (R32, L32) 反序拼接,就是密文。
你可能会问:「为什么是 32 轮?」—— 嗯,我记得当年看设计文档时,设计者给出的理由是:经过 32 轮迭代,差分攻击和线性攻击的复杂度都能达到 2128 以上,刚好卡在安全边界上。
密钥扩展:从 128 位主密钥到 32 个轮密钥
密钥扩展这块,SM4 和 AES 的思路完全不同。AES 用密钥编排算法生成轮密钥,而 SM4 直接复用加密算法本身的轮函数结构。
具体做法是这样的:
- 把 128 位主密钥分成 4 个 32 位字:MK0, MK1, MK2, MK3
- 用固定系统参数 FK 做初始异或:K0 = MK0 ⊕ FK0,依此类推
- 然后迭代 32 轮,每轮生成一个轮密钥 rki
避坑指南:我曾经在项目里直接用 SM4 加密算法去算密钥扩展,结果发现轮密钥生成时用的线性变换 L' 和加密轮函数里的 L 不一样。L' 是 L 的简化版,只做 13 位和 23 位的循环移位。这个细节很容易被忽略,但错了整个加解密都会乱掉。
密钥扩展的迭代公式:
rk[i] = K[i+4] = K[i] ⊕ T'(K[i+1] ⊕ K[i+2] ⊕ K[i+3] ⊕ CK[i])
其中 CK[i] 是固定常数,T' 是带简化线性变换的合成置换。32 轮跑完,你就得到了 rk[0] 到 rk[31] 共 32 个轮密钥。
SM4 的四种工作模式
算法本身只是块加密,怎么用还得看工作模式。我按实际项目中的使用频率来排个序:
| 模式 | 特点 | 典型场景 | 我的评价 |
|---|---|---|---|
| ECB | 每个分组独立加密,可并行 | 密钥分发、少量数据 | 别用在长消息上,模式特征太明显 |
| CBC | 前一个密文参与下一个加密 | 文件加密、数据库字段加密 | 最常用,但注意 IV 不能重复 |
| CTR | 计数器模式,可并行,可随机访问 | 网络流加密、磁盘加密 | 我个人的首选,性能好还灵活 |
| GCM | CTR + GMAC 认证,同时提供完整性 | 通信协议、TLS 替代方案 | 国密通信的首选,一步到位 |
重要提醒:ECB 模式下,相同的明文块会产生相同的密文块。我在一次渗透测试中亲眼看到,有人用 ECB 加密图片,结果密文里还能看出原图的轮廓。所以除非你只加密单个块,否则别用 ECB。
GCM 模式值得多说两句。它把 SM4 的 CTR 模式和 GHASH 认证结合起来,加密和认证可以并行处理。在国密通信协议里,我建议直接用 GCM,省得自己再拼一个 HMAC 上去,容易出漏洞。
SM4 的软件实现与性能优化
好了,理论说完了,咱们聊聊实战。SM4 的软件实现,说白了就是怎么让那 32 轮跑得更快。
基础实现:查表法
最直接的做法是把 S 盒做成 256 字节的查找表,每轮查 4 次 S 盒,再做循环移位和异或。这种实现简单可靠,但性能一般。在我的笔记本上,纯 C 实现大概能跑到 200-300 MB/s。
// SM4 单轮加密核心代码(简化版)
uint32_t sm4_round(uint32_t x, uint32_t rk) {
uint32_t t = x ^ rk;
uint8_t a0 = SBOX[(t >> 24) & 0xFF];
uint8_t a1 = SBOX[(t >> 16) & 0xFF];
uint8_t a2 = SBOX[(t >> 8) & 0xFF];
uint8_t a3 = SBOX[t & 0xFF];
uint32_t z = (a0 << 24) | (a1 << 16) | (a2 << 8) | a3;
// 线性变换 L
return z ^ ROTL(z, 2) ^ ROTL(z, 10) ^ ROTL(z, 18) ^ ROTL(z, 24);
}
查表法优化:T 盒合并
把 S 盒和线性变换合并成 4 个 T 盒,每个 T 盒 1KB(256 × 4 字节)。这样每轮只需要 4 次查表和 3 次异或,省掉了循环移位的计算。我实测过,性能能提升 40% 左右。
优化技巧:T 盒的构造公式是 Ti[a] = L(S(a) << (24 - 8i))。提前算好存成静态数组,运行时直接查。注意字节序,大端小端要统一。
指令集加速:AES-NI 的「曲线救国」
你可能会问:「SM4 能用 AES-NI 加速吗?」—— 答案是不能直接复用,但可以间接利用。现代 CPU 的 AES-NI 指令虽然是为 AES 设计的,但我们可以用它们来加速 SM4 的 S 盒查找和字节置换。
我记得在某个项目中,我们用 SSSE3 的 PSHUFB 指令实现了 SM4 的 S 盒查找,配合 AES-NI 的 AESENC 指令做线性变换,最终把吞吐量推到了 1.2 GB/s。具体做法是:
- 用 PSHUFB 做 16 字节并行的 S 盒查找
- 用 AESENC 的 ShiftRows 和 MixColumns 模拟 SM4 的线性变换
- 用 PCLMULQDQ 加速 GCM 模式下的 GHASH
性能对比(我的测试数据):
- 纯 C 查表法:280 MB/s
- T 盒合并优化:410 MB/s
- SSSE3 + AES-NI 加速:1.2 GB/s
- CTR 模式并行 + 多线程:3.5 GB/s(8 核)
CTR 模式的并行化
CTR 模式天然支持并行,因为每个分组的计数器是独立的。你可以把 128 位计数器拆成多个子计数器,每个线程处理一段。我一般用 OpenMP 或者直接手写线程池,把加密吞吐量推到单核的 4-6 倍。
注意:并行 CTR 时,计数器管理要小心。每个线程的起始计数器要算准,不能重叠也不能留空。我见过一个线上事故,就是因为计数器分配错了,导致两个线程加密了同一段数据,解密出来全是乱码。
一张图看懂 SM4 全流程
说了这么多,不如一张图来得直观。下面这张 SVG 图把 SM4 的加密流程、密钥扩展和工作模式串在了一起:
从图上你能看到,SM4 的整个流程其实很规整:密钥扩展走左边,加密走右边,最后通过工作模式适配不同的应用场景。我个人觉得,理解这张图比死记硬背公式重要得多。
写在最后
SM4 的软件实现,说白了就是在「查表」和「计算」之间找平衡。查表法简单但受限于缓存,指令集加速快但移植性差。我现在的做法是:在 x86 平台上用 AES-NI + SSSE3 的混合方案,在 ARM 上就用 NEON 指令集重写 S 盒查找。至于工作模式,除非有特殊要求,否则一律上 GCM —— 省心、安全、性能也不差。
嗯,SM4 这块就先聊到这儿。记住一点:算法是死的,优化是活的。多跑 benchmark,多试几种方案,你自然能找到最适合自己场景的那一套。