4. SM4 轮函数与迭代结构详解
SM4 的加密核心就两块:轮函数和32 轮迭代。你先别着急看整个算法,把轮函数拆开讲清楚,迭代其实就是一个循环套用。我当年第一次接触国密标准时,也被那几张公式图绕晕过……后来自己画了一张流程图,瞬间就通了。
4.1 轮函数 F 的数学骨架
每一轮做的事情其实很简洁。给定输入 Xi = (A, B, C, D),当前轮密钥 rki,轮函数输出一个 32 位中间值,然后和 A 异或生成新的 B(在迭代中会交换位置)。公式长这样:
F(X_i, rk_i) = B ⊕ L( τ( B ⊕ C ⊕ D ⊕ rk_i ) )
别被符号吓着。说白了,B ⊕ C ⊕ D ⊕ rk_i 就是一次简单的异或,只有 32 位。然后丢进一个非线性变换 τ,再经过线性变换 L。你看,一个轮函数就拆成了三步。
我在项目中遇到过不少同学自己实现时把异或顺序搞反,结果加密结果和标准库对不上。其实你严格按照这个顺序写,一个字都不要改。
4.2 S 盒与非线性变换 τ
τ 叫“非线性字节变换”。它把输入的 32 位拆成四个字节 (a, b, c, d),每个字节查一遍 S 盒,再拼回 32 位。SM4 的 S 盒是一个 16×16 的查找表(256 个字节),和 AES 的 S 盒完全不同。
uint8_t sm4_sbox[256]。硬件实现则用组合逻辑直接映射,但那是另一回事。
查表很简单:输入字节的低 4 位做列索引,高 4 位做行索引。比如输入 0x3F,行 3 列 F 对应的值我查出来是 0xAA(实际上这个值要按标准表去查)。
| 输入字节 | 高4位(行) | 低4位(列) | S盒输出 |
|---|---|---|---|
| 0x00 | 0 | 0 | 0xD6 |
| 0x3F | 3 | F | 0xAA |
| 0xFF | F | F | 0x53 |
表里每个值都是固定写死的,你不需要理解它怎么产生的(其实是仿射变换+逆元映射),直接用就好。
4.3 线性变换 L 与循环移位
τ 输出 32 位结果 Z,然后送入线性变换 L。L 的定义是:
L(Z) = Z ⊕ (Z <<< 2) ⊕ (Z <<< 10) ⊕ (Z <<< 18) ⊕ (Z <<< 24)
其中 <<< n 表示循环左移 n 位。注意是循环移位,不是逻辑移位。我见过新手写成 << 不带循环,结果整个扩散都错了。
((z << n) | (z >> (32-n)))。我曾经在嵌入式平台上没注意到小端序问题,导致移位效果不对,调试了一整天……最后发现是内存里字节顺序没处理好。
4.4 32 轮迭代——轮着来就对了
现在把轮函数装进迭代里。SM4 的轮数固定 32 轮,每轮输入是 4 个 32 位字 (Xi, Xi+1, Xi+2, Xi+3),输出下一轮的 (Xi+1, Xi+2, Xi+3, Xi+4)。本质上就是一个 4 字滑动窗口。
对应伪代码:
for (i = 0; i < 32; i++) {
uint32_t f = F(X[i], X[i+1], X[i+2], X[i+3], rk[i]);
X[i+4] = X[i] ^ f;
}
加密完成后,最后四个字 (X[35], X[34], X[33], X[32]) 就是密文,注意顺序是反的!标准规定密文输出顺序为 (X35, X34, X33, X32)。解密时只要把轮密钥逆序使用,迭代逻辑一模一样。嗯,这里一定要注意顺序,我当初在对接测试时就因为输出了正向顺序导致解析失败。
关键参数一瞥:
- 分组大小:128 位(4 个 32 位字)
- 密钥长度:128 位
- 轮数:32
- 每轮输入:4 个字,输出:1 个新字
- 解密:轮密钥逆序,算法结构不变
4.5 实战中的常见误区
- 轮密钥序号搞错。 标准里第 i 轮用 rk[i],从 0 到 31。有人写循环时把索引和值弄反了。
- S 盒查表实现写成 16x16 二维数组 但忘记转成字节索引。直接用一维数组最保险。
- 解密时忘了逆序轮密钥。 我记得有次和同事联调,他直接把加密密钥数组原样传入解密函数,结果算出乱码。他看了半小时才反应过来。
- 循环左移的位数写错。 2、10、18、24,少一个都不行。
<< 2 写成了 << 2 但没补循环。从那以后我要求团队所有硬件实现必须做全回归测试,对比软件参考实现。
总而言之,轮函数本身就是模块化的:异或→S盒→线性变换→异或。记住了这个流水线,写代码时一行一行对应着来,不太会出错。你试试看。