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 直接复用加密算法本身的轮函数结构。

具体做法是这样的:

  1. 把 128 位主密钥分成 4 个 32 位字:MK0, MK1, MK2, MK3
  2. 用固定系统参数 FK 做初始异或:K0 = MK0 ⊕ FK0,依此类推
  3. 然后迭代 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 算法全流程框架 128 位主密钥 MK 密钥扩展算法 FK 系统参数 + CK 固定常数 32 个轮密钥 rk[0..31] 128 位明文 P ⊕ 32 轮 Feistel 迭代 反序变换 R||L 128 位密文 C 工作模式选择 ECB 模式 CBC 模式 CTR 模式 GCM 模式 关键说明 • 密钥扩展与加密共用轮函数结构,但线性变换 L' 不同 • 解密时轮密钥逆序使用:rk[31], rk[30], ..., rk[0] • GCM 模式 = CTR 加密 + GHASH 认证,推荐用于通信协议

从图上你能看到,SM4 的整个流程其实很规整:密钥扩展走左边,加密走右边,最后通过工作模式适配不同的应用场景。我个人觉得,理解这张图比死记硬背公式重要得多。

写在最后

SM4 的软件实现,说白了就是在「查表」和「计算」之间找平衡。查表法简单但受限于缓存,指令集加速快但移植性差。我现在的做法是:在 x86 平台上用 AES-NI + SSSE3 的混合方案,在 ARM 上就用 NEON 指令集重写 S 盒查找。至于工作模式,除非有特殊要求,否则一律上 GCM —— 省心、安全、性能也不差。

嗯,SM4 这块就先聊到这儿。记住一点:算法是死的,优化是活的。多跑 benchmark,多试几种方案,你自然能找到最适合自己场景的那一套。