4. SM4对称加密算法:从Feistel结构到实战工作模式
SM4,国密标准里的对称加密算法。说实话,我第一次接触它时觉得跟AES有点像,但深入一看,结构完全不同。SM4用的是Feistel网络,密钥长度128位,分组长度也是128位。嗯,咱们一步步拆开看。
4.1 SM4算法结构:Feistel网络的魅力
Feistel网络是什么?说白了,就是把数据分成左右两半,每轮只处理一半,另一半直接复制过去。SM4用了32轮这样的操作。
我刚开始做国密改造时,有个同事问我:“为什么不用AES那种SPN结构?”我的回答是:Feistel网络有个天然优势——加密和解密可以用同一套逻辑,只是密钥使用顺序反过来。这在硬件实现上特别友好。
SM4核心参数:
- 分组长度:128位(16字节)
- 密钥长度:128位(16字节)
- 轮数:32轮
- 轮函数:F(X0, X1, X2, X3, rk) = X0 ⊕ T(X1 ⊕ X2 ⊕ X3 ⊕ rk)
每轮的结构其实很简洁。输入4个32位的字,经过一个合成置换T,再跟第一个字异或。T里面又包含非线性变换(S盒)和线性变换(L函数)。
我的经验:SM4的S盒设计得很巧妙。它基于仿射变换和逆映射,能抵抗差分密码分析和线性密码分析。我在做安全测评时,专门验证过S盒的差分均匀性,确实达到了理论最优值。
4.2 SM4密钥扩展:从16字节到32个轮密钥
SM4的密钥扩展,说白了就是把128位的初始密钥,变成32个32位的轮密钥。这个过程跟加密本身很像,也用了Feistel结构。
具体流程是这样的:
- 初始化:把128位密钥分成4个32位字 MK0, MK1, MK2, MK3
- 系统参数异或:K0 = MK0 ⊕ FK0, K1 = MK1 ⊕ FK1, K2 = MK2 ⊕ FK2, K3 = MK3 ⊕ FK3
- 32轮迭代:rki = Ki+4 = Ki ⊕ T'(Ki+1 ⊕ Ki+2 ⊕ Ki+3 ⊕ CKi)
这里FK是系统参数,CK是固定常数。T'跟加密用的T类似,只是线性变换L换成了L'。
| 参数 | 值 | 作用 |
|---|---|---|
| FK0 | 0xA3B1BAC6 | 系统参数,消除对称性 |
| FK1 | 0x56AA3350 | 系统参数,消除对称性 |
| FK2 | 0x677D9197 | 系统参数,消除对称性 |
| FK3 | 0xB27022DC | 系统参数,消除对称性 |
| CK[i] | 固定序列,共32个 | 轮常数,防止密钥间关联 |
避坑指南:我曾经在项目中遇到一个坑——密钥扩展的T'跟加密的T不一样,L'是L的逆。有个同事直接复制了加密的T函数,结果加解密对不上。排查了一整天才发现是这里的问题。记住:密钥扩展用L',加密用L。
4.3 SM4加解密流程:32轮迭代与逆序解密
加密流程其实很直观:
- 输入:128位明文,分成4个32位字 (X0, X1, X2, X3)
- 32轮迭代:Xi+4 = Xi ⊕ T(Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕ rki)
- 输出:最后一轮的结果 (X35, X34, X33, X32) 反序输出
注意最后要反序输出。为什么?因为Feistel结构最后一轮不做交换,反序一下才能得到正确的密文。
解密就更简单了——把轮密钥逆序使用就行。rk0到rk31变成rk31到rk0。流程完全一样。
// SM4加密核心代码(伪代码)
void sm4_encrypt(uint32_t rk[32], uint8_t plaintext[16], uint8_t ciphertext[16]) {
uint32_t X[36];
// 将明文分成4个字
bytes_to_words(plaintext, &X[0], &X[1], &X[2], &X[3]);
// 32轮迭代
for (int i = 0; i < 32; i++) {
X[i+4] = X[i] ^ T(X[i+1] ^ X[i+2] ^ X[i+3] ^ rk[i]);
}
// 反序输出
ciphertext[0..3] = word_to_bytes(X[35]);
ciphertext[4..7] = word_to_bytes(X[34]);
ciphertext[8..11] = word_to_bytes(X[33]);
ciphertext[12..15]= word_to_bytes(X[32]);
}
// 解密:rk逆序
void sm4_decrypt(uint32_t rk[32], uint8_t ciphertext[16], uint8_t plaintext[16]) {
uint32_t rk_rev[32];
for (int i = 0; i < 32; i++) {
rk_rev[i] = rk[31 - i];
}
sm4_encrypt(rk_rev, ciphertext, plaintext);
}
性能数据:SM4在软件实现上,单轮只需要4次S盒查表、4次异或和1次循环移位。我实测过,在ARM Cortex-M4上,128位数据加密大约需要1200个时钟周期。比AES-128略慢,但差距在10%以内。
4.4 SM4的工作模式:ECB、CBC、CTR
SM4本身只处理128位数据块。实际应用中,数据长度不固定,所以需要工作模式。我重点讲三种最常用的。
4.4.1 ECB模式(电子密码本模式)
ECB最简单——每个128位块独立加密。缺点也很明显:相同的明文块会生成相同的密文块。
我记得有一次做安全审计,客户用了ECB模式加密图片。结果加密后的图片还能看出轮廓。这就是ECB的典型问题。所以,除非数据长度固定且没有重复模式,否则别用ECB。
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ECB | 可并行、错误不扩散 | 相同明文产生相同密文 | 密钥加密、短数据 |
| CBC | 密文随机化、安全性高 | 不能并行、错误会扩散 | 文件加密、通用场景 |
| CTR | 可并行、可随机访问 | 计数器管理复杂 | 流媒体、数据库加密 |
4.4.2 CBC模式(密码分组链接模式)
CBC模式引入了初始化向量IV。每个明文块先跟上一个密文块异或,再加密。这样即使明文相同,密文也不同。
我个人的习惯是:通用数据加密首选CBC。但要注意两点:
- IV必须随机:每次加密用不同的IV,且IV不需要保密
- 填充问题:数据长度不是128位的倍数时,需要PKCS7填充
避坑指南:我曾经在项目中遇到一个CBC的坑——IV重用。当时为了省事,每次加密都用固定的IV。结果被安全团队发现,直接打回重做。记住:IV重用会泄露明文信息,特别是当数据有固定前缀时。
4.4.3 CTR模式(计数器模式)
CTR模式把SM4变成了流密码。它加密一个计数器序列,然后跟明文异或。计数器每次加1。
CTR模式有个很大的优点:可以随机访问。比如你要解密第100个块,不需要解密前面的99个。这在数据库加密场景下特别有用。
// CTR模式加密
void sm4_ctr_encrypt(uint8_t *plaintext, int len,
uint8_t key[16], uint8_t nonce[8]) {
uint8_t counter[16] = {0};
uint8_t keystream[16];
uint64_t ctr = 0;
// nonce + 计数器
memcpy(counter, nonce, 8);
for (int i = 0; i < len; i += 16) {
// 更新计数器
counter[15] = (ctr >> 0) & 0xFF;
counter[14] = (ctr >> 8) & 0xFF;
counter[13] = (ctr >> 16) & 0xFF;
counter[12] = (ctr >> 24) & 0xFF;
// 加密计数器生成密钥流
sm4_encrypt_block(key, counter, keystream);
// 异或明文
for (int j = 0; j < 16 && (i+j) < len; j++) {
plaintext[i+j] ^= keystream[j];
}
ctr++;
}
}
重要提醒:CTR模式的计数器绝对不能重复。如果同一个密钥下计数器值相同,两个密文块异或一下就能得到明文异或值。这在密码学上是致命的。所以,每次加密都要用不同的nonce,或者维护好计数器的状态。
嗯,SM4的内容就讲到这里。三种工作模式各有适用场景,ECB少用,CBC通用,CTR适合随机访问。密钥扩展和加解密流程其实不复杂,关键是把Feistel结构理解透。下次遇到SM4相关的项目,你应该能快速上手了。