SM4对称加密算法原理
SM4,咱们国密的对称加密标准。说实话,我第一次接触它是在一个物联网安全项目里,客户要求必须用国密,不能用AES。当时我心想,不就是个对称加密嘛,能有多大区别?结果一上手才发现,SM4的结构和AES还真不一样,轮函数、密钥扩展都有自己的门道。
今天我就把SM4的底裤扒开给你看。算法结构、轮函数、密钥扩展、加解密流程,一个不落。
1. SM4算法整体结构
SM4是一个分组密码算法,分组长度128位,密钥长度也是128位。说白了,一次加密16个字节的数据,密钥也是16个字节。
它的结构是典型的Feistel网络。你想想看,Feistel结构有个好处——加解密几乎一样,只是轮密钥的使用顺序相反。这在嵌入式实现上特别省事,代码量小,容易优化。
核心参数一览:
- 分组长度:128位(16字节)
- 密钥长度:128位(16字节)
- 轮数:32轮
- 结构:Feistel网络
- 轮密钥长度:32位(4字节)
SM4的加解密流程可以用下面这张图概括。我习惯把这种图贴在项目文档的第一页,方便团队快速理解。
2. 轮函数详解
SM4的轮函数是整个算法的核心。每一轮做的事情其实很简单:用4个32位的字,生成一个新的32位字。
具体公式长这样:
Xi+4 = Xi ⊕ T(Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕ rki)
其中T函数又分为两步:非线性变换τ和线性变换L。
2.1 非线性变换τ
τ变换就是4个S盒并行查表。SM4只有一个S盒,8位输入8位输出,重复用4次。
我记得第一次看SM4的S盒时,觉得它和AES的S盒不太一样。AES的S盒有代数结构,SM4的S盒更像是精心挑选的随机置换。实际测试下来,它的非线性度和差分均匀性都很好。
我的经验:在嵌入式平台上,S盒查表是性能瓶颈。我建议把S盒放在SRAM里,不要放在Flash。Flash查表有等待周期,SRAM快得多。如果MCU有TCM,放TCM里效果最好。
2.2 线性变换L
L变换是一个32位的线性变换,公式如下:
L(B) = B ⊕ (B <<< 2) ⊕ (B <<< 10) ⊕ (B <<< 18) ⊕ (B <<< 24)
说白了就是循环移位再异或。这个设计很巧妙,计算量小,扩散效果好。
注意:密钥扩展中的L变换和加密轮函数中的L变换不一样!密钥扩展用的是L':
L'(B) = B ⊕ (B <<< 13) ⊕ (B <<< 23)
这个坑我踩过。当时移植代码时直接复制了加密的L函数到密钥扩展里,结果加解密对不上。查了半天才发现是L和L'的区别。
3. 密钥扩展算法
密钥扩展就是把16字节的密钥,变成32个32位的轮密钥。过程分两步:
- 密钥初始化:用系统参数FK对密钥进行白化
- 轮密钥生成:迭代32次,每次生成一个轮密钥
系统参数FK是固定的:
FK0 = 0xA3B1BAC6
FK1 = 0x56AA3350
FK2 = 0x677D9197
FK3 = 0xB27022DC
密钥扩展的伪代码如下:
// 输入:MK = MK0 || MK1 || MK2 || MK3
// 输出:rk[0..31]
// 步骤1:密钥初始化
K0 = MK0 ⊕ FK0
K1 = MK1 ⊕ FK1
K2 = MK2 ⊕ FK2
K3 = MK3 ⊕ FK3
// 步骤2:生成32个轮密钥
for i = 0 to 31:
Ki+4 = Ki ⊕ T'(Ki+1 ⊕ Ki+2 ⊕ Ki+3 ⊕ CKi)
rki = Ki+4
其中CK是固定参数,每个轮次不同。CK的生成规则是:CKij = (4i+j)×7 mod 256。这个公式我建议你记一下,调试时经常需要手动计算某个CK值。
4. 加解密流程
加解密流程完全对称,只是轮密钥顺序相反。我直接上代码,这样最清楚:
// SM4加密
void sm4_encrypt(uint32_t rk[32], uint32_t x[4]) {
uint32_t i, tmp;
for (i = 0; i < 32; i++) {
tmp = x[1] ^ x[2] ^ x[3] ^ rk[i];
tmp = sm4_sbox_lookup(tmp); // τ变换
tmp = sm4_linear_L(tmp); // L变换
x[0] ^= tmp;
// 循环移位
uint32_t t = x[0];
x[0] = x[1];
x[1] = x[2];
x[2] = x[3];
x[3] = t;
}
// 反序输出
// 输出顺序:x[3], x[2], x[1], x[0]
}
// SM4解密
void sm4_decrypt(uint32_t rk[32], uint32_t x[4]) {
uint32_t i, tmp;
for (i = 0; i < 32; i++) {
// 注意:使用逆序轮密钥
tmp = x[1] ^ x[2] ^ x[3] ^ rk[31 - i];
tmp = sm4_sbox_lookup(tmp);
tmp = sm4_linear_L(tmp);
x[0] ^= tmp;
uint32_t t = x[0];
x[0] = x[1];
x[1] = x[2];
x[2] = x[3];
x[3] = t;
}
// 反序输出
}
你看,加密和解密唯一的区别就是rk[i]变成了rk[31-i]。这就是Feistel结构的好处。
5. 嵌入式实现要点
在实际项目中,SM4的实现有几个关键点需要注意:
| 优化方向 | 具体做法 | 效果 |
|---|---|---|
| S盒查表 | 放在SRAM/TCM中,避免Flash等待 | 速度提升30%-50% |
| 循环展开 | 32轮循环手动展开为8轮×4次 | 减少循环开销 |
| 查表合并 | 将τ和L合并为4张32位大表 | 速度提升2-3倍,但多占4KB ROM |
| 密钥预计算 | 加密前一次性算好32个轮密钥 | 避免每次加密都算密钥 |
避坑指南:我曾经在一个Cortex-M0的项目里,直接把SM4的S盒定义成const uint8_t放在Flash里。结果加密一次要2ms,完全达不到要求。后来把S盒搬到SRAM里,时间降到了0.8ms。嗯,这个教训很深刻。
6. 安全性分析
SM4的安全性经过多年验证,目前没有有效的攻击方法。它的S盒非线性度高,线性变换扩散性好,32轮迭代足够抵抗差分和线性密码分析。
不过有一点要注意:SM4和AES一样,是分组密码,需要配合工作模式使用。ECB模式不安全,建议用CBC或CTR模式。如果对完整性有要求,可以加上MAC或使用GCM模式。
总结一下SM4的特点:
- 结构简单,适合硬件实现和嵌入式软件实现
- 加解密共用代码,节省ROM空间
- 密钥扩展和加解密分离,可以预计算
- 安全性经过充分验证,可以放心使用