4、SM4加解密流程:Feistel网络结构、轮函数F详解、加解密过程对比
好,咱们进入正题。SM4的加解密流程,说白了就是一套精心设计的轮转结构。我当年第一次看国标文档时,说实话也被那一堆符号绕得有点晕。但后来自己动手实现了一遍,发现核心逻辑其实很清晰——就是Feistel网络加上一个精巧的轮函数F。
4.1 Feistel网络结构:SM4的骨架
SM4采用的是典型的平衡Feistel网络结构。什么叫平衡?就是每次把128位数据块分成左右两半,各64位,然后一轮一轮地“搅和”。
我个人习惯把Feistel网络想象成一个“拧毛巾”的过程:左边拧到右边,右边拧到左边,反复拧,最后水(明文特征)就均匀分布了。
核心公式:
第i轮:Xi+1 = (Xi-1 ⊕ F(Xi, RKi)) || Xi
其中Xi-1是左半部分,Xi是右半部分,RKi是第i轮轮密钥。
嗯,这里要注意:SM4的Feistel结构和DES有个明显区别——它的左右两半在每一轮都会交换位置,但解密时不需要像DES那样反过来用轮密钥顺序。为什么?因为SM4的轮函数F设计得足够对称,解密就是加密的逆过程,轮密钥倒着用就行。
我曾经在给一个嵌入式设备移植SM4时,发现硬件加速器只实现了加密路径,解密得靠软件模拟。当时我就利用Feistel网络的可逆性,直接把加密函数当解密用,省了一笔硬件改版费用。这个技巧,你后面做工程优化时可能会用到。
4.2 轮函数F详解:SM4的心脏
轮函数F是整个SM4算法最核心的部分。它接收两个输入:右半部分64位数据和当前轮密钥32位,输出32位数据。结构上分为三步:
- 异或加轮密钥:把32位输入和32位轮密钥做异或
- S盒非线性变换:把32位结果分成4个8位,分别查S盒
- 线性变换L:对S盒输出做循环移位和异或组合
说白了,F函数 = L(S(X ⊕ RK))。就这么简单。
避坑指南:我曾经在实现S盒查表时,把S盒的索引和输出搞反了。SM4的S盒是8进8出,输入0x00输出0xD6。如果你发现加解密结果全是乱的,先检查S盒对不对。
线性变换L的公式如下:
L(B) = B ⊕ (B <<< 2) ⊕ (B <<< 10) ⊕ (B <<< 18) ⊕ (B <<< 24)
这里的<<<表示循环左移。你想想看,这个L变换其实就是在32位空间里做扩散,让一个比特的变化能影响到尽可能多的比特位。我刚开始学的时候,觉得这公式是拍脑袋想出来的。后来做差分分析才发现,这个移位组合是经过精心设计的——它能保证4轮就能达到完全扩散。
4.3 加解密过程对比:一张表说清楚
SM4的加解密过程,本质上是一模一样的操作,唯一的区别是轮密钥的使用顺序。加密时从RK0用到RK31,解密时从RK31用到RK0。
| 步骤 | 加密 | 解密 |
|---|---|---|
| 输入 | 128位明文 + 32位轮密钥RK0~RK31 | 128位密文 + 32位轮密钥RK31~RK0 |
| 轮数 | 32轮 | 32轮 |
| 轮函数 | F(Xi, RKi) | F(Xi, RK31-i) |
| 输出变换 | 最后一轮后做反序变换R | 最后一轮后做反序变换R |
| 结果 | 128位密文 | 128位明文 |
这里有个细节很多人会忽略:最后一轮做完32轮迭代后,还需要做一个反序变换R。R就是把(X32, X33, X34, X35)这四个32位字倒序输出为(X35, X34, X33, X32)。
为什么要有这个反序?因为Feistel网络最后一轮输出时,左右两半没有交换。如果不做反序,解密时就没法直接用同样的结构。这个设计,说白了就是为了让加解密代码能复用同一套逻辑。
注意:SM4的加解密硬件实现时,轮密钥生成和加解密可以流水线并行。但如果你用软件实现,记得把轮密钥提前算好存起来,别每轮都重新算一遍——我见过有人这么干,性能直接掉了30%。
4.4 加解密流程对比图
下面这张图把加密和解密的流程放在一起对比,你一眼就能看出它们的对称性。
4.5 加解密代码实现对比
下面这段代码展示了SM4加解密的核心逻辑。你会发现,加密和解密函数几乎一模一样,唯一的区别就是轮密钥数组的遍历方向。
// SM4加密核心函数
void sm4_encrypt(uint32_t plaintext[4], uint32_t rk[32], uint32_t ciphertext[4]) {
uint32_t x[36];
int i;
// 初始赋值
for (i = 0; i < 4; i++) x[i] = plaintext[i];
// 32轮迭代,轮密钥正向使用
for (i = 0; i < 32; i++) {
x[i+4] = x[i] ^ F(x[i+1] ^ x[i+2] ^ x[i+3] ^ rk[i]);
}
// 反序变换R
ciphertext[0] = x[35];
ciphertext[1] = x[34];
ciphertext[2] = x[33];
ciphertext[3] = x[32];
}
// SM4解密核心函数
void sm4_decrypt(uint32_t ciphertext[4], uint32_t rk[32], uint32_t plaintext[4]) {
uint32_t x[36];
int i;
// 初始赋值
for (i = 0; i < 4; i++) x[i] = ciphertext[i];
// 32轮迭代,轮密钥反向使用
for (i = 0; i < 32; i++) {
x[i+4] = x[i] ^ F(x[i+1] ^ x[i+2] ^ x[i+3] ^ rk[31 - i]);
}
// 反序变换R
plaintext[0] = x[35];
plaintext[1] = x[34];
plaintext[2] = x[33];
plaintext[3] = x[32];
}
工程经验:我建议你在实现时,把加密和解密合并成一个函数,通过一个方向参数来控制轮密钥的遍历顺序。这样代码量减半,也更容易维护。我在一个IoT项目里就是这么干的,固件体积直接少了2KB。