SM4 对称加密算法(上):从背景到核心流程

聊到国密算法,SM4 是绕不开的一块基石。我最早接触它是在一个政务系统的数据加密项目里,客户要求所有存储的敏感字段必须用国密加密。当时第一反应是「用 AES 不也挺好?」——后来才明白,SM4 的设计其实有很多巧思,值得细细拆解。

SM4 算法背景与设计思想

SM4 的前身叫 SMS4,2006 年首次公开,2012 年正式成为国家标准(GB/T 32907-2016)。它是分组对称加密算法,分组长度 128 位,密钥长度也是 128 位。说白了,就是你把数据切成 16 字节一块,用 16 字节的密钥去加密。

你可能会问:为什么不用 AES?嗯,这里有个现实问题。AES 是美国标准,在一些合规场景里(比如政务、金融、军工),国密是硬性要求。我遇到过一家银行,因为跨境业务需要同时支持 AES 和 SM4,结果发现 SM4 的轮函数结构在硬件实现上反而更省资源——这是后话。

SM4 的设计思想可以概括为三点:

  • Feistel 结构:和 DES 类似,但轮数更多(32 轮),安全性更高。
  • 面向硬件优化:S 盒和线性变换都考虑了硬件实现的效率。
  • 密钥扩展与加解密共用:解密时只需要把轮密钥逆序使用,不需要额外计算。

核心要点:SM4 不是 AES 的简单模仿。它的 S 盒、线性变换、密钥扩展都有独立的设计逻辑,理解这些才能用好它。

SM4 加解密流程详解

SM4 的加解密流程可以拆成三块:密钥扩展、轮函数、反序变换。我们一个一个来看。

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

主密钥 MK 是 128 位,拆成 4 个 32 位字:(MK0, MK1, MK2, MK3)。然后通过一个固定的系统参数 FK 和 CK 来生成轮密钥。

具体步骤:

  1. 计算 K0 = MK0 ⊕ FK0, K1 = MK1 ⊕ FK1, K2 = MK2 ⊕ FK2, K3 = MK3 ⊕ FK3
  2. 对于 i = 0 到 31:
    rki = Ki+4 = Ki ⊕ T'(Ki+1 ⊕ Ki+2 ⊕ Ki+3 ⊕ CKi)

这里的 T' 是一个非线性变换,和加密轮函数里的 T 很像,但线性部分不同。我刚开始看的时候觉得这有点绕,后来写代码调试才发现,密钥扩展其实可以提前算好存起来,加解密时直接查表就行。

实战建议:在嵌入式设备上,密钥扩展的计算量可以提前到初始化阶段完成,避免每次加解密都重复计算。我在一个 IoT 项目里就是这么做的,性能提升了约 30%。

2. 轮函数:32 轮迭代的核心

每一轮的结构很简单:输入是 4 个 32 位字 (X0, X1, X2, X3),输出是 (X1, X2, X3, X4),其中 X4 = X0 ⊕ T(X1 ⊕ X2 ⊕ X3 ⊕ rki)。

这个 T 变换包含两步:

  • S 盒替换:把 32 位拆成 4 个字节,每个字节通过 S 盒映射成新字节。
  • 线性变换 L:对 32 位结果做循环移位和异或操作。

L 变换的公式是:L(B) = B ⊕ (B <<< 2) ⊕ (B <<< 10) ⊕ (B <<< 18) ⊕ (B <<< 24)。

说白了,就是通过移位和异或把 S 盒的输出扩散到整个 32 位空间。这个设计比 AES 的 MixColumns 更简单,硬件实现时只需要几个移位器和异或门。

注意:SM4 的轮函数里没有字节代换后的行移位和列混合,而是直接用线性变换 L 完成扩散。这个差异在软件实现时要注意,别和 AES 搞混了。

3. 反序变换:最后一步的巧妙设计

32 轮迭代结束后,会得到 (Y0, Y1, Y2, Y3)。但直接输出的话,解密时会有问题。所以 SM4 加了一个反序变换:输出 (Y3, Y2, Y1, Y0)。

为什么这么做?因为 SM4 是 Feistel 结构,解密时只需要把轮密钥逆序使用,加解密流程完全一样。反序变换保证了加解密的一致性。我第一次实现时忘了这一步,结果解密出来的数据全是乱码——嗯,这个坑我踩过。

SM4 的 S 盒设计与线性变换

S 盒是 SM4 唯一的非线性部件,它的设计直接决定了算法的安全性。SM4 的 S 盒是一个 8×8 的置换表,输入 8 位,输出 8 位。具体数值可以在标准文档里查到,我这里不贴完整表了,但有几个特点值得注意:

  • 代数结构:基于有限域 GF(2^8) 上的仿射变换和乘法逆元构造。
  • 差分均匀性:最大差分概率为 2^{-6},能抵抗差分密码分析。
  • 线性逼近:最大线性偏差为 2^{-4},能抵抗线性密码分析。

线性变换 L 的设计也有讲究。它的分支数为 5,意味着任意 1 个字节的变化至少会扩散到 5 个字节。这个指标比 AES 的 MixColumns(分支数 5)一样,但实现更简单。

一句话总结:SM4 的 S 盒负责「混淆」,线性变换负责「扩散」,两者配合实现了香农提出的密码设计两大原则。

SM4 整体流程框架图

下面这张图展示了 SM4 加密的完整流程,从密钥扩展到 32 轮迭代,再到反序输出。你可以对照着看,每一步的位置和作用一目了然。

SM4 加密流程框架图 128 位主密钥 MK 密钥扩展 (FK + CK) 32 个轮密钥 rk[i] 128 位明文 X 拆分为 4 个 32 位字 32 轮迭代 (i=0..31) 每轮内部: S 盒替换 → 线性变换 L X4 = X0 ⊕ T(...) 反序变换 (Y3,Y2,Y1,Y0) 128 位密文 Y 关键说明 1. 密钥扩展和 32 轮迭代是 SM4 的核心计算部分 2. 每轮使用一个不同的轮密钥 rk[i],由密钥扩展生成 3. 反序变换保证加解密流程一致(解密时轮密钥逆序使用) 4. 解密流程与加密完全相同,只需将轮密钥逆序输入

实战中的几个注意点

讲完理论,说几个我在项目中踩过的坑:

  • 轮密钥不要搞错顺序:加密时用 rk0 到 rk31,解密时用 rk31 到 rk0。我见过有人解密时忘了逆序,结果 debug 了一整天。
  • S 盒查表要小心:SM4 的 S 盒是 256 字节的表,用 C 语言实现时记得用 const 数组,别放在栈上。
  • 线性变换的移位位数:2、10、18、24,这四个数字是固定的,别记错了。我曾经把 18 写成 16,结果加解密对不上。

小技巧:如果你在 ARM Cortex-M 系列芯片上实现 SM4,可以利用它的 barrel shifter 硬件特性,把线性变换的移位和异或合并成一条指令,能省不少周期。