3、对称加密算法:AES、SM4 的原理与密钥生成要求
对称加密,说白了就是加密和解密用同一把钥匙。你要是用过 SSH 的密钥对,会对比出差别——对称加密快得多,适合大量数据。但在密钥分发上,它就没那么方便了。这一讲我们重点聊两个算法:AES 和 SM4。一个国际标准,一个国密标准。我当年在设计一个 IoT 设备安全启动方案时,就纠结过到底用谁——后来发现不少国内厂商其实更信任 SM4,而国外客户只认 AES。两种都得会,今天我把它们串起来讲透。
3.1 AES 算法原理
AES(高级加密标准)是 Rijndael 算法的固定版本,分组长度 128 位,密钥长度支持 128、192、256 位。我建议你直接记 128 位密钥的版本,它最常用,而且 SM4 的密钥长度也是 128 位,对比起来方便。
AES 的核心是 SPN 结构(Substitution-Permutation Network)。说白了就是“替换-置换”的循环。每一轮都做四件事:
- 字节代换(SubBytes):用 S 盒把每个字节换成另一个值。非线性的,防微分攻击。
- 行移位(ShiftRows):把状态矩阵的每一行按不同偏移量左移。这招能让列之间开始混合。
- 列混合(MixColumns):每一列当成多项式,在 GF(2⁸) 域内乘一个固定矩阵。这一步是扩散的关键。
- 轮密钥加(AddRoundKey):把当前状态和这一轮的轮密钥异或。简单但重要——密钥就是在这里注入的。
最后一轮不做列混合。嗯,我记得第一次看标准文档时,觉得“为啥少一步”?后来想通了,为了解密时对称性好实现。
小小提示:AES 的 S 盒是精心设计的,找不出不动点(即 S(a)=a 的解只有一个)。如果你自己改 S 盒,很可能引入后门。我在项目中见过有人想“优化”S 盒,结果把安全强度降了一截——千万别这么干。
3.2 AES 密钥生成要求
密钥扩展(Key Expansion)是 AES 里最容易出 Bug 的地方。128 位密钥需要扩展出 11 个轮密钥(第 0 轮+10 轮)。过程是这样的:
- 原始密钥直接作为前 4 个字(W[0]~W[3])。
- 对于每个
i,如果i % 4 == 0,则先对 W[i-1] 做 RotWord(循环左移一个字节)、SubWord(每个字节过 S 盒),再与轮常数 Rcon[i/4] 异或,最后与 W[i-4] 异或。 - 其他情况:直接 W[i-1] 与 W[i-4] 异或。
轮常数 Rcon 是 GF(2⁸) 中的幂,值很小,但能打破对称性。我在一次 FIPS 认证测试中,就是因为轮常数写错了一个字节,导致加解密结果对不上——debug 了三天,最后发现是 Rcon[1] 应该是 0x01 而不是 0x02。你想想看,一个字节的差异,整个密钥树都歪了。
常见坑:密钥扩展必须是确定性可逆的。加密端和解密端用的轮密钥顺序相反。解密时需要用 Inverse Cipher,轮密钥也要逆序使用。如果你做硬件实现,别忘了把轮密钥提前算好存起来,而不是每轮现场算——会卡时序的。
3.3 SM4 算法原理
SM4 是中国国家密码管理局发布的商用密码算法,分组 128 位,密钥 128 位,32 轮迭代。它的结构也是 Feistel 网络的变种(实际上是 Unbalanced Feistel,但咱们不纠结名字)。
每一轮只处理 32 位,剩下 96 位直接复制。SM4 的轮函数 F 包含三个部分:
- 非线性变换 τ:把输入的 32 位拆成 4 个字节,每个字节过 S 盒。SM4 的 S 盒和 AES 不一样,是自设计的,也是 CFB 模式相容。
- 线性变换 L:对 τ 的输出做循环移位和异或。
B ^ (B <<< 2) ^ (B <<< 10) ^ (B <<< 18) ^ (B <<< 24)。这个 L 保证了扩散性。 - 轮密钥加(异或):L 结果再与轮密钥 rk 异或。
说实话,SM4 的轮函数比 AES 的列混合简单,硬件实现面积更小。但在软件上,因为 SM4 是 32 位操作,低端 MCU 上反而比 AES 快——我在 STM32F0 上做过 benchmark,同样密钥长度 SM4 快 20% 左右。
一点体会:SM4 的 S 盒是固定的,但你可以把它做成查找表。如果你用查表法,注意 S 盒需要 256 字节,可以放常量数组。不过我在一个资源受限的智能卡项目中,用的是组合逻辑直接算 S 盒的逆元+仿射变换——因为 ROM 只剩 1KB 了,你看,工程就是取舍。
3.4 SM4 密钥生成要求
SM4 的密钥扩展也产生 32 个 32 位的轮密钥 rk[0]~rk[31]。过程:
- 将 128 位原始密钥 MK 拆成 4 个 32 位字 MK[0]~MK[3]。
- 用固定系统参数 FK[0]~FK[3] 做初始化:
K[i]=MK[i] ^ FK[i](i=0..3)。 - 对于 i=0..31:
- 先对 K[i+1] ^ K[i+2] ^ K[i+3] ^ CK[i] 做 τ(查 S 盒),再做线性变换(与 L 不同,这里是 L':B ^ (B <<< 13) ^ (B <<< 23))。
- 结果异或 K[i] 得到 K[i+4]。
- 轮密钥 rk[i] = K[i+4]
注意这里的 CK 是固定系统参数(32 个常数),能破坏轮密钥之间的简单关系。FK 则是初始异或,让主密钥不直接暴露在 K 中。我早年设计一个加密芯片时,一开始没注意 FK 的作用,直接拿 MK 当 K 算——后来发现这样容易侧信道分析。加上 FK 后,攻击者不知道翻转哪些比特来获取主密钥信息了。
| 维度 | AES-128 | SM4 |
|---|---|---|
| 分组长度 | 128 bit | 128 bit |
| 密钥长度 | 128/192/256 bit | 128 bit |
| 轮数 | 10 (128-bit key) | 32 |
| 结构 | SPN | Feistel 变种 |
| 每轮操作单位 | 16 bytes 矩阵 | 32 bits 字 |
| S 盒 大小 | 8×8,256 bytes | 8×8,256 bytes |
| 扩散实现 | MixColumns + ShiftRows | L 线性变换 + 迭代 |
| 密钥扩展轮常数 | Rcon(每 4 字) | CK(每轮) + FK |
你可能会问:既然 SM4 有 32 轮,比 AES 的 10 轮多得多,是不是更安全?其实不是线性关系。AES 每一轮的扩散很强,而 SM4 每一轮只动 32 位,所以需要更多轮来达到全扩散。实际安全强度都够,但 AES 已经经受了 20 多年的公开密码分析,SM4 相对年轻,但国密局也做过安全评估,目前没发现什么结构性弱点。
工程建议:如果你做面向国内政府/金融项目,必须用 SM4(符合 GM/T 0002 标准)。如果做国际化产品,推荐 AES-128。两边都要支持时,我建议把密钥扩展和轮运算做成可配置模块——硬件上复用 S 盒查表和外设,软件上分别实现,别混用安全参数。
下面我给出一段简单的 SM4 密钥扩展的 C 代码片段(只做示意,实际生产还要加防侧信道处理):
// SM4 密钥扩展简化实现
#include <stdint.h>
#define ROTL(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
extern uint8_t SM4_SBOX[256]; // 256 字节 S 盒
extern uint32_t FK[4], CK[32]; // 系统参数
void sm4_key_expand(uint32_t MK[4], uint32_t rk[32]) {
uint32_t K[36];
int i;
for (i = 0; i < 4; i++) K[i] = MK[i] ^ FK[i];
for (i = 0; i < 32; i++) {
uint32_t tmp = K[i+1] ^ K[i+2] ^ K[i+3] ^ CK[i];
// 非线性变换 τ
uint8_t *b = (uint8_t *)&tmp;
b[0] = SM4_SBOX[b[0]];
b[1] = SM4_SBOX[b[1]];
b[2] = SM4_SBOX[b[2]];
b[3] = SM4_SBOX[b[3]];
// 线性变换 L'
tmp = tmp ^ ROTL(tmp, 13) ^ ROTL(tmp, 23);
K[i+4] = K[i] ^ tmp;
rk[i] = K[i+4];
}
}
这段代码里,我把轮密钥直接存到 rk 数组里。实际使用时,如果内存紧张,可以边生成边用,不用全存。但要注意,SM4 加密/解密时轮密钥次序是反的——加密用 rk[0..31],解密用 rk[31..0]。我当年第一次调 SM4 时忘了反转轮密钥,解密结果跟随机数似的……最后看了好久标准文档才意识到。
3.5 密钥生成的安全边界要求
讲完具体算法,我想强调几个通用原则。不管是 AES 还是 SM4,密钥生成环节都容易出安全问题。我总结了三条:
- 密钥必须随机且不可预测。别用 time()、ID 或者伪随机种子(如 rand())。一定要用硬件真随机数发生器(TRNG)。我在一个项目里遇到过用软件 rand() 生成密钥的“天才”操作——测试时一切正常,上线后密钥空间只有 2^32,两天就被暴力破解了。
- 轮密钥必须保密。密钥扩展后的轮密钥和主密钥同等级。如果攻击者拿到了任何一个轮密钥,可以通过逆推得到主密钥(或者至少得到等价密钥)。所以轮密钥在内存中要定时擦除,或者放在安全内存区。
- 防侧信道攻击。如果你的设备是硬件实现,注意功率分析、电磁辐射。我建议用掩码(Masking)技术:在密钥扩展过程中混入随机数,然后在运算时再去盲化。虽然增加面积和延迟,但对高安全场景是必须的。
警告:千万不要把密钥直接硬编码在固件里!我曾见过某智能锁厂商直接把 AES 密钥写在 flash 的 0x08000000 地址——用 binwalk 一抽就出来了。正确做法是:密钥从安全元素(SE)、OTP 或者可信执行环境(TEE)中读取,并且只能由加密引擎使用,不能暴露给主 CPU。
3.6 知识体系图
我画了一张图,把 AES 和 SM4 的核心流程以及密钥扩展的逻辑放在一起对比,方便你理解整个对称加密的结构框架。
看图的思路:左右对比 AES 和 SM4 的主流程。AES 经过 10 轮(最后少列混合),SM4 经过 32 轮加一个反序输出。密钥扩展两者都基于主密钥产生轮密钥,但具体算法不同。你可以在脑子里大致记住:AES 是 SPN、轮数少、每轮处理全块;SM4 是 Feistel 变种、轮数多、每轮处理 32 bit。
3.7 实战小贴士
关于模式选择。AES 和 SM4 本身只是分组算法,使用时必须搭配操作模式(ECB、CBC、CTR、GCM 等)。我个人强烈不建议用 ECB,因为它会泄露明文模式——我曾经用 ECB 加密一张 BMP 位图,结果还能看出图的轮廓。CBC 和 GCM 是常用的,GCM 还能提供认证加密(AEAD),省去 HMAC 的额外开销。
关于轮密钥保护。如果你在裸机上实现,一旦轮密钥被 DMA 或调试器读到,整个加密系统就废了。我建议开启 MPU/MMU 保护,把轮密钥所在内存页设为只读/不可执行,并且在使用完毕后立即用 memset 覆盖,然后用编译器屏障防止优化掉(用 volatile 或者 secure_memzero)。
关于侧信道。AES 的 S 盒查找表很容易被缓存时序攻击(Cache Timing Attack)。SM4 同样存在这个风险。如果你在云环境或多租户硬件上跑,最好使用 bitslice 实现或 AES-NI 指令。我在一次云计算竞赛中见过,攻击者通过测量 AES-SIV 的执行时间,就能恢复出密钥的 Hamming weight——所以别小看时序。
又一次警示:SM4 的 FK 和 CK 是公开的参数,不是秘密。有些人误以为这些参数需要保密,其实它们只是为了防止密钥扩展中出现简单代数关系——公开并不影响安全性。真正的秘密只有主密钥(和它派生出的轮密钥)。
好了,这章我们梳理了 AES 和 SM4 的原理、密钥生成要求,以及一些工程落地的经验。记住,对称加密是整个安全架构的基石,密钥生成又是基石的基石——你花再多时间在这上面都不为过。下次做设计时,希望能带着这些边界意识去选型和实现。