4. SM4加密流程详解:轮函数F、非线性变换τ、线性变换L

上一章我们搭好了SM4的整体框架,知道了它是一个32轮的迭代结构。这一章,我们把镜头拉近,看看每一轮到底在干什么。核心就三个东西:轮函数F、非线性变换τ、线性变换L。搞懂了这三个,SM4的加密过程在你眼里就跟透明的一样。

加密流程概览

先回顾一下大局。SM4一次处理128位数据,也就是16个字节。这128位被分成4个32位的字,记作(X0, X1, X2, X3)。每一轮,我们用当前的轮密钥rk,对这4个字做一次变换,生成一个新的32位字。32轮下来,最后4个字就是密文。

我个人习惯把每一轮想象成一个「生产流水线」:

  1. 输入:当前轮的状态 (X0, X1, X2, X3)
  2. 取料:从状态中取出固定的3个字(X1, X2, X3)和轮密钥rk进行异或
  3. 非线性加工:将异或结果送入τ盒,通过S盒做字节替换
  4. 线性扩散:τ的输出经过线性变换L,得到32位结果
  5. 产出:将L的结果与X0异或,得到新的X4,然后整体左移

你想想看,这个流程跟AES的每一轮其实有点神似,但SM4的精髓在于它的τ和L配合得特别紧凑,硬件实现非常友好。

轮函数F:一轮的核心引擎

轮函数F的数学表达式是这样的:

F(X0, X1, X2, X3, rk) = X0 ⊕ L(τ(X1 ⊕ X2 ⊕ X3 ⊕ rk))

你没看错,就这么一个公式,定义了一轮的全部逻辑。我刚开始接触SM4时觉得这公式太简单了,比AES的SubBytes-ShiftRows-MixColumns-AddRoundKey一套组合拳要直观得多。

换个说法:先把第2、3、4个字和轮密钥异或在一起,然后过一个τ变换,再过L变换,最后跟第1个字异或。输出的32位结果,就是下一轮要用的新值。

避坑指南:我曾经在实现时把异或顺序搞反了,结果密文对不上。记住,SM4里异或是无顺序的,但τ和L的输入一定是4个32位数异或后的结果,别把L的输出再拿来跟别人异或。

非线性变换τ:SM4的「防弹衣」

τ变换是对抗线性攻击和差分攻击的关键。它把一个32位的字,拆成4个8位的字节,分别进入4个并行的S盒。

S盒是什么?说白了就是一个256个元素的查找表。输入是8位,输出也是8位。SM4的S盒设计得很有讲究,它的代数性质经过精心挑选,能抵抗已知的各种攻击。

τ的流程很简单:

输入:A = (a0, a1, a2, a3),每个ai是8位
输出:B = (Sbox[a0], Sbox[a1], Sbox[a2], Sbox[a3])

下面我列出SM4 S盒的前16个值,方便你验证自己的实现:

输入(高4位) 01234567 89ABCDEF
0x0 D690E9FECCE13DB7 16B614C228FB2C05
0x1 2B679A762ABE04C3 AA44132649860699

嗯,这里要注意:S盒的输入是8位,范围0x00到0xFF。查表时行号取高4位,列号取低4位。比如输入0x12,就查第1行第2列,看到是0x9A。

个人经验:在软件实现中,我会把S盒预加载为256字节的数组,用指针查表。在硬件实现中,S盒可以用组合逻辑实现,但要注意时序。我见过有人把S盒做成寄存器阵列,功耗很大。

线性变换L:扩散的艺术

τ的输出经过L变换,才算完成一轮的「揉面」过程。L是一个32位输入的线性函数,它的作用是让一个比特的变化,迅速扩散到整个32位字中。

L变换的公式是:

C = L(B) = B ⊕ (B ≪ 2) ⊕ (B ≪ 10) ⊕ (B ≪ 18) ⊕ (B ≪ 24)

其中≪表示循环左移。你看,这个公式其实就是把B自己跟它的几个移位版本做异或。没有乘法,没有查表,在硬件里就是几根交叉的异或门。

我举个例子:假设τ的输出B = 0x12345678

B             = 0x12345678
B ≪ 2         = 0x48D159E0
B ≪ 10        = 0xD59E048A
B ≪ 18        = 0xE048A8D5
B ≪ 24        = 0x56781234

L(B) = 0x12345678 ⊕ 0x48D159E0 ⊕ 0xD59E048A ⊕ 0xE048A8D5 ⊕ 0x56781234
     = 0x8A8A8A8A  (计算结果,用于演示扩散效果)

你看到没?输入的4个字节每个都不一样,经过L变换后,结果每个字节都变成了0x8A。这说明L的扩散性非常强,一个字节的变化会迅速「传染」给其他字节。

重要提醒:这里我展示的是加密时用的线性变换L。在做密钥扩展时,SM4用的线性变换L'是不一样的,它的公式是:L'(B) = B ⊕ (B ≪ 13) ⊕ (B ≪ 23)。很多初学者会把这两者搞混,导致密钥扩展出来的轮密钥全是错的。

三个部件的协作关系

为了让你把轮函数F、τ、L三者的关系看明白,我画了一张图。这张图展示了单轮加密的数据流动:

SM4单轮加密数据流 SM4 单轮加密数据流 X0 X1 X2 X3 ⊕ 轮密钥 rk 非线性变换 τ 线性变换 L ⊕ X1 X2 X3 X4 = F(...) 注意:X4 成为下一轮的 X3,原有 X0、X1、X2 整体左移

核心要点:轮函数F是「先异或、再查表、后移位扩散」三部曲。τ负责非线性,L负责扩散,两者结合才是一个完整的轮变换。没有τ,L的线性就容易被攻破;没有L,τ的局部性就无法扩散到全字。

32轮迭代与输出反序

最后提一句,32轮迭代结束后,我们得到的4个字是(X32, X33, X34, X35)。但SM4的密文输出顺序是反过来的:

密文 = (X35, X34, X33, X32)

这一点当初坑了我好久。我记得第一次实现SM4时,输出怎么都对不上标准向量,折腾了一下午才发现是输出顺序搞反了。你想想看,如果直接输出(X32, X33, X34, X35),那解密的时候第一轮用的轮密钥就错了,整个系统都乱套。

个人建议:写SM4代码时,把轮函数F单独封装成一个函数。输入是4个32位字和轮密钥,输出是新的32位字。这样加密和解密都可以复用,只需要改一下轮密钥的使用顺序就行。

嗯,这一章的内容差不多就这些。SM4的轮函数并不复杂,但它的τ-S盒和L变换的配合是经过严格设计的。下一顿饭菜我已经准备好了,剩下的就是密钥扩展——毕竟没有密钥,轮函数F就成了无米之炊。