SM4对称加密算法原理

SM4,咱们国密的对称加密标准。说实话,我第一次接触它是在一个物联网安全项目里,客户要求必须用国密,不能用AES。当时我心想,不就是个对称加密嘛,能有多大区别?结果一上手才发现,SM4的结构和AES还真不一样,轮函数、密钥扩展都有自己的门道。

今天我就把SM4的底裤扒开给你看。算法结构、轮函数、密钥扩展、加解密流程,一个不落。

1. SM4算法整体结构

SM4是一个分组密码算法,分组长度128位,密钥长度也是128位。说白了,一次加密16个字节的数据,密钥也是16个字节。

它的结构是典型的Feistel网络。你想想看,Feistel结构有个好处——加解密几乎一样,只是轮密钥的使用顺序相反。这在嵌入式实现上特别省事,代码量小,容易优化。

核心参数一览:

  • 分组长度:128位(16字节)
  • 密钥长度:128位(16字节)
  • 轮数:32轮
  • 结构:Feistel网络
  • 轮密钥长度:32位(4字节)

SM4的加解密流程可以用下面这张图概括。我习惯把这种图贴在项目文档的第一页,方便团队快速理解。

SM4算法整体结构图 SM4算法整体结构(Feistel网络) 明文(128位) 拆分 (X0, X1, X2, X3) 32轮迭代 (i=0,1,...,31) 每轮: Xi+4 = Xi ⊕ T(Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕ rki) T = 非线性变换τ + 线性变换L 反序变换 (X35,X34,X33,X32) 密文(128位) 注意 加解密结构相同 仅轮密钥顺序相反 解密时rk0→rk31变为rk31→rk0 密钥扩展 输入:MK0,MK1,MK2,MK3 输出:rk0,rk1,...,rk31

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位的轮密钥。过程分两步:

  1. 密钥初始化:用系统参数FK对密钥进行白化
  2. 轮密钥生成:迭代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空间
  • 密钥扩展和加解密分离,可以预计算
  • 安全性经过充分验证,可以放心使用