3. SM4加解密流程逐步拆解
SM4 是我国商用密码体系中的分组密码算法,分组长度固定为 128 位,密钥长度同样为 128 位,采用 32 轮非线性迭代结构。理解 SM4 的关键,不是记住某个公式,而是把“128 位分组如何进入、32 轮如何迭代、轮密钥如何参与、解密为何能复用同一结构”这四件事串成一条清晰的链路。下面按数据实际流动的顺序逐步拆解。
一、128 位分组:输入、输出与字节序约定
SM4 一次只处理一个 128 位明文分组,输出一个 128 位密文分组。128 位在实现中通常表示为 4 个 32 位字,记为 X0、X1、X2、X3。这里必须先明确一个容易出错的约定:字节序。标准中明文按大端方式映射为字,即第一个字节是最高有效字节。若实现环境是小端机器,直接按内存顺序取字会导致结果与标准测试向量不一致,因此工程上常在读入后做一次字节序转换。
例如,明文十六进制为 0123456789abcdeffedcba9876543210,按大端映射后:
X0 = 0x01234567
X1 = 0x89abcdef
X2 = 0xfedcba98
X3 = 0x76543210
这 4 个字就是 32 轮迭代的初始状态。轮函数每轮只更新其中一个字,其余三个字顺次后移,因此 32 轮结束后仍需按相反顺序组合回 128 位密文。
二、32 轮迭代:轮函数 T 的组成
SM4 的轮函数可概括为“异或—S 盒替换—线性变换”三步。设第 i 轮输入为 (X_i, X_{i+1}, X_{i+2}, X_{i+3}),轮密钥为 rk_i,则:
X_{i+4} = X_i ⊕ T(X_{i+1} ⊕ X_{i+2} ⊕ X_{i+3} ⊕ rk_i)
其中 T 由非线性变换 τ 和线性变换 L 复合而成。τ 对 4 个字节分别查 S 盒,S 盒是固定的 8 位输入输出表,提供非线性混淆;L 则对 32 位字做循环移位与异或:
L(B) = B ⊕ (B<<<2) ⊕ (B<<<10) ⊕ (B<<<18) ⊕ (B<<<24)
这里的 <<< 表示 32 位循环左移。可以看到,每一轮只产生一个新字,旧字依次前移,这种结构称为“非平衡 Feistel 变体”,也是 SM4 解密能复用加密结构的基础。
三、轮密钥使用:密钥扩展与 32 个 rk
加密使用的 32 个轮密钥 rk_0 到 rk_31 并非直接来自主密钥,而是由 128 位主密钥经密钥扩展生成。主密钥同样按大端分为 4 个字 MK0~MK3,先与系统参数 FK 异或得到 K0~K3,再迭代生成:
K_{i+4} = K_i ⊕ T'(K_{i+1} ⊕ K_{i+2} ⊕ K_{i+3} ⊕ CK_i)
rk_i = K_{i+4}
注意 T' 与轮函数中的 T 结构相似,但线性变换改为 L'(B)=B⊕(B<<<13)⊕(B<<<23),且 CK_i 是固定参数。轮密钥的生成顺序与使用顺序一致:第 0 轮用 rk_0,第 31 轮用 rk_31。工程实现中通常一次性扩展出 32 个字存入数组,避免每轮重复计算。
四、加密与解密的对称性实现
SM4 最优雅的地方在于:解密并不需要另写一套逆轮函数。由于轮函数只对“后三个字”做变换,且每轮产生的新字参与后续运算,把加密的 32 个轮密钥逆序使用,就能还原明文。也就是说,解密时第 0 轮用 rk_31,第 1 轮用 rk_30,……,第 31 轮用 rk_0,其余结构完全不变。
这一点在实现上非常关键:加密和解密可以共用同一个 sm4_round() 函数,只需传入不同的轮密钥数组顺序。若把轮密钥扩展结果存为 rk[32],加密时按 rk[0..31] 调用,解密时按 rk[31..0] 调用即可。下面给出一个轮函数的伪代码示例:
uint32_t sm4_round(uint32_t x0, uint32_t x1, uint32_t x2, uint32_t x3, uint32_t rk) {
uint32_t b = x1 ^ x2 ^ x3 ^ rk;
uint32_t t = tau(b); // 4 字节 S 盒替换
t = t ^ ROTL(t, 2) ^ ROTL(t, 10) ^ ROTL(t, 18) ^ ROTL(t, 24);
return x0 ^ t;
}
调用时,加密循环为 for i in 0..31: x4 = sm4_round(x0,x1,x2,x3, rk[i]),解密循环为 for i in 31..0: x4 = sm4_round(x0,x1,x2,x3, rk[i])。最后统一做反序输出,即可得到正确结果。
五、结果验证与常见错误
验证实现是否正确,最直接的方法是使用标准测试向量。例如主密钥与明文均为 0123456789abcdeffedcba9876543210 时,标准加密结果应为 681edf34d206965e86b3e94f536e4246。若结果不符,优先检查三类问题:一是字节序是否按大端映射;二是轮密钥是否逆序使用;三是循环左移是否误写成普通左移。这三处是 SM4 工程实现中最常见的偏差来源。
把上述流程串起来看:128 位分组提供固定输入输出宽度,32 轮迭代完成混淆与扩散,轮密钥按序注入,解密则通过轮密钥逆序复用同一结构。理解这条主线后,无论用 C、Python 还是硬件描述语言实现 SM4,都能保持结构一致、结果可验证。