第2章 SM4加密与解密流程:基本轮函数、非线性变换

SM4的加解密流程,说白了就是一套「轮函数反复套圈」的过程。我刚接触这个算法时,总觉得32轮迭代很吓人,但拆开看,每一轮干的活其实就三件事:加轮密钥、非线性变换、线性变换。你吃透一轮,后面31轮全是重复。

2.1 整体加解密框架

SM4采用128位分组、128位密钥,32轮迭代。加密和解密共用同一个轮函数,唯一区别是轮密钥的使用顺序——解密时把加密的轮密钥倒过来用。为什么能这样?因为轮函数是可逆的,而且逆变换刚好是它自己换了个顺序。

⚠️ 我曾经踩过的坑:第一次实现SM4解密时,我直接照搬加密的轮密钥顺序,结果解出来全是乱码。后来才意识到,解密必须把轮密钥数组反转(rk[31], rk[30], ..., rk[0]),而且线性变换部分也要取逆。记住了:加密和解密不是同一个轮函数直接被调用,只是结构对称。

2.2 轮函数结构

先看加密轮函数。设第i轮的输入为 \((X_i, X_{i+1}, X_{i+2}, X_{i+3})\),轮密钥为 \(rk_i\),输出为 \(X_{i+4}\)。我习惯把它写成下面这样:

X_{i+4} = X_i ^ L( τ( X_{i+1} ^ X_{i+2} ^ X_{i+3} ^ rk_i ) )

看不懂公式没关系,咱们分步拆解:

  1. 轮密钥加:把后三个字与轮密钥异或:\(A = X_{i+1} \oplus X_{i+2} \oplus X_{i+3} \oplus rk_i\)
  2. 非线性变换 τ:把32位的A拆成4个字节,分别查S盒,再合并成32位字
  3. 线性变换 L:把τ的输出做循环移位异或(类似扩散)
  4. 结果异或:L的输出与 \(X_i\) 异或,得到 \(X_{i+4}\)

每一轮就这四步,你想想看,是不是很简单?

SM4加密轮函数内部结构 输入 (X0~X3) 轮密钥加 (X1⊕X2⊕X3⊕rk) 非线性变换 τ (4个Sbox查表) 线性变换 L (循环移位异或) 异或 Xi → 输出 Xi+4 进入下一轮 / 输出密文 轮密钥 rk 由密钥扩展生成 每轮使用不同的 rk 解密时倒序使用 解密时线性变换改用 L_inv 其余步骤完全相同

2.3 核心秘籍:非线性变换 τ

τ就是把32位分成4个字节,每个字节丢进S盒,得到4个新字节再拼回32位。我个人喜欢用查表法实现——预计算好256个字节的S盒表,每轮查4次。SM4的S盒是固定写死的,网上能查到16×16的矩阵。这里我贴出关键部分的伪代码:

unsigned int tau(unsigned int A) {
    unsigned char b0 = (A >> 24) & 0xFF;
    unsigned char b1 = (A >> 16) & 0xFF;
    unsigned char b2 = (A >> 8)  & 0xFF;
    unsigned char b3 =  A        & 0xFF;
    return (Sbox[b0] << 24) | (Sbox[b1] << 16) |
           (Sbox[b2] << 8)  |  Sbox[b3];
}
💡 工程小贴士:S盒的查表操作是典型的「数据依赖查表」。你在做性能测试时,如果CPU的缓存命中率不高,这个查表会成为瓶颈。我记得有个项目里,我们把S盒从4次查表合并成一次大表查32位,但安全性分析上有点争议,慎用。

2.4 线性变换 L 与解密逆变换

线性变换L负责扩散。加密时它定义为:

L(B) = B ^ (B<<<2) ^ (B<<<10) ^ (B<<<18) ^ (B<<<24)

其中 <<< 表示循环左移。这个变换保证了「雪崩效应」——输入改1bit,输出会变很多bit。解密时用的不是L,而是它的逆变换L_inv:

L_inv(B) = B ^ (B<<<2) ^ (B<<<4) ^ (B<<<8) ^ (B<<<12) ^
           (B<<<14) ^ (B<<<16) ^ (B<<<18) ^ (B<<<22) ^ (B<<<24) ^ (B<<<30)

注意看,L_inv比L多了好几个异或项。你想想看,这其实是数学上的可逆性保证,实际实现时如果偷懒直接用L去解密,输出会彻底乱掉。我遇到过有人把L_inv写错,调试了整整两天。

加密/解密轮函数对比
环节 加密 解密
轮密钥顺序 rk[0], rk[1], ..., rk[31] rk[31], rk[30], ..., rk[0]
非线性变换 τ 完全相同 完全相同
线性变换 L (循环左移异或) L_inv (更多循环异或)
输出生成 Xi ^ L(τ(...)) Xi ^ L_inv(τ(...))

2.5 解密流程的对称性

SM4解密其实就是把加密的轮函数「倒过来走」。你看上面表格,轮密钥逆序、线性变换取逆,其他一概不变。所以你在写性能测试程序时,可以复用同一套轮函数代码,通过一个「加密/解密」标志来切换L还是L_inv。我个人习惯在轮函数参数里传一个函数指针,指向L或L_inv,这样32轮迭代的循环体不需要if判断,性能更高。

🔑 核心结论:SM4的32轮迭代中,每一轮都在做同一套「异或 → 查表 → 移位异或」。解密只是旋转了密钥方向、换了线性变换的系数。理解轮函数,你就抓住了SM4的七寸。

2.6 性能审计关注点

做并发数性能审计时,轮函数的几个瓶颈点需要格外留意:

  • S盒查表粒度:每轮4次查表,32轮就是128次查表。如果共享S盒表有缓存竞争,建议每个线程预分配自己的S盒副本(只读,无锁)。
  • 循环移位的SIMD化:L变换中的循环左移在x86上可以用_mm_rol_epi32加速,能省不少指令。
  • 轮密钥预计算:所有32轮轮密钥可以一次性提前算好,加解密时直接读取数组。我见过有人每轮都重新算密钥,吞吐量直接掉一半。
⚠️ 特别注意:有些ARM平台对循环移位的指令支持不如x86,如果目标平台是ARM,建议用C语言直接写移位和OR,不要过度依赖intrinsic,否则跨平台可能有问题。

好了,这一章的核心内容就是这些。理解轮函数的结构,后面讲并发性能测试时,你才能知道哪些地方值得优化。记住:轮函数是SM4的灵魂,查表和移位是性能的左右手。