SM4对称加密算法:从原理到实战
对称加密,说白了就是加密和解密用同一把钥匙。你在网上银行转账、用支付,背后都有对称加密在默默工作。国密标准里的SM4,就是咱们国家自己的对称加密算法。我最早接触SM4是在一个政务系统项目里,当时客户明确要求“必须用国密,不能用AES”。嗯,从那以后我就把SM4研究了个透。
SM4算法原理:Feistel结构
SM4的核心结构叫Feistel网络。这个名字听着唬人,其实原理很简单——把数据切成两半,左边不动,右边拿去跟轮密钥做运算,然后左右交换。重复这个操作若干轮,就完成了加密。
具体来说,SM4把128位明文分成4个32位的字。每一轮只更新其中一个字,其他三个字直接移位。我画个图你一看就明白:
SM4总共跑32轮。32轮之后,把四个字倒序输出,就是密文。解密时完全一样,只是轮密钥的顺序反过来。这个对称性,是Feistel结构最巧妙的地方。
核心要点:SM4的Feistel结构保证了加解密可以用同一套代码,只是轮密钥顺序不同。这在硬件实现上特别省资源。
密钥扩展与轮函数
SM4的密钥是128位,但需要生成32个32位的轮密钥。这个过程叫密钥扩展。我当初自己实现时,在这个环节踩过坑——轮密钥算错了,加解密死活对不上。
密钥扩展流程
密钥扩展分三步走:
- 初始化:把128位密钥分成4个32位字 MK[0..3]
- 异或固定参数:K[i] = MK[i] ⊕ FK[i],FK是系统参数(固定值)
- 迭代生成:rk[i] = K[i+4] = K[i] ⊕ T'(K[i+1] ⊕ K[i+2] ⊕ K[i+3] ⊕ CK[i])
这里的T'变换和加密轮函数里的T变换很像,只是线性移位不同。CK是32个固定常数,每个都不一样。
我的经验:调试密钥扩展时,建议先拿标准测试向量对一下前几个轮密钥。如果第一个轮密钥就错了,后面全白搭。我曾经花了一下午查bug,最后发现是CK数组抄错了一个字节。
轮函数F
每一轮的轮函数F包含四个操作:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 异或 | B = X[1] ⊕ X[2] ⊕ X[3] ⊕ rk |
| 2 | S盒替换 | 每个字节查S盒,非线性变换 |
| 3 | 线性移位L | B' = B ⊕ (B<<2) ⊕ (B<<10) ⊕ (B<<18) ⊕ (B<<24) |
| 4 | 输出 | F = B',与X[0]异或得到新X[3] |
S盒是SM4唯一的非线性部件。它把8位输入映射成8位输出,一共256个值。这个S盒是设计好的,你不能自己改。改了就不是SM4了。
注意:线性移位L在密钥扩展中用的是简化版L',只做13位和23位的循环左移。加密轮函数里用的是完整版L。这两个不一样,千万别搞混。
SM4的工作模式:ECB、CBC、CTR
算法本身只处理128位数据块。实际应用中,数据可能很长,也可能很短。这就需要工作模式来规定怎么处理多块数据。
ECB模式(电子密码本)
最简单粗暴的模式。每块数据独立加密,互不影响。
- 优点:可以并行加密,速度快
- 缺点:相同的明文块产生相同的密文块,有模式泄露风险
- 适用场景:加密随机数据(如密钥本身),不适合加密结构化数据
我个人很少用ECB。有一次做图片加密演示,用ECB模式加密后,图片轮廓居然还能看出来。这就是模式泄露的典型表现。
CBC模式(密码分组链接)
每个明文块先与前一个密文块异或,再加密。第一个块需要初始化向量IV。
- 优点:相同的明文块产生不同的密文块,安全性高
- 缺点:不能并行加密,一个块出错会影响后续所有块
- 适用场景:文件加密、数据库加密
避坑指南:IV必须随机生成,且每次加密都要用不同的IV。我曾经见过有人把IV写死在代码里,结果审计时被判定为安全漏洞。IV不是密钥,但同样重要。
CTR模式(计数器模式)
用计数器生成密钥流,再与明文异或。本质上是把分组密码当流密码用。
- 优点:可以并行加解密,支持随机访问
- 缺点:计数器不能重复,否则会泄露密钥流
- 适用场景:网络通信、磁盘加密
CTR模式是我在项目中用得最多的。它支持随机访问,解密一个块不需要解密前面的块。这在加密大文件时特别有用——你想读文件中间某一段,不用从头解密。
| 模式 | 并行性 | 错误传播 | 是否需要IV | 推荐场景 |
|---|---|---|---|---|
| ECB | 支持 | 无 | 否 | 随机数据加密 |
| CBC | 不支持 | 影响后续块 | 是 | 文件/数据库加密 |
| CTR | 支持 | 无 | 是 | 网络通信/流加密 |
Python实现SM4加解密
Python里实现SM4,最方便的是用gmssl库。这个库是国密算法的官方Python实现,支持SM2、SM3、SM4。
先安装:
pip install gmssl
ECB模式示例
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
# 密钥必须是16字节
key = b'1234567890123456'
data = b'Hello, SM4! This is a test message.'
# 加密
crypt_sm4 = CryptSM4()
crypt_sm4.set_key(key, SM4_ENCRYPT)
ciphertext = crypt_sm4.crypt_ecb(data)
print(f'密文(hex): {ciphertext.hex()}')
# 解密
crypt_sm4.set_key(key, SM4_DECRYPT)
plaintext = crypt_sm4.crypt_ecb(ciphertext)
print(f'明文: {plaintext.decode()}')
CBC模式示例
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
import os
key = b'1234567890123456'
iv = os.urandom(16) # 随机生成IV
data = b'Hello, SM4! This is a test message.'
# 加密
crypt_sm4 = CryptSM4()
crypt_sm4.set_key(key, SM4_ENCRYPT)
ciphertext = crypt_sm4.crypt_cbc(iv, data)
print(f'IV(hex): {iv.hex()}')
print(f'密文(hex): {ciphertext.hex()}')
# 解密
crypt_sm4.set_key(key, SM4_DECRYPT)
plaintext = crypt_sm4.crypt_cbc(iv, ciphertext)
print(f'明文: {plaintext.decode()}')
CTR模式示例
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
import os
key = b'1234567890123456'
iv = os.urandom(16) # CTR模式也需要IV(作为计数器的初始值)
data = b'Hello, SM4! This is a test message.'
# 加密
crypt_sm4 = CryptSM4()
crypt_sm4.set_key(key, SM4_ENCRYPT)
ciphertext = crypt_sm4.crypt_ctr(iv, data)
print(f'IV(hex): {iv.hex()}')
print(f'密文(hex): {ciphertext.hex()}')
# 解密(CTR模式加解密是同一个操作)
crypt_sm4.set_key(key, SM4_DECRYPT)
plaintext = crypt_sm4.crypt_ctr(iv, ciphertext)
print(f'明文: {plaintext.decode()}')
小技巧:CTR模式的加解密函数是一样的。因为CTR本质上是生成密钥流再异或,异或两次就还原了。所以crypt_ctr在加密和解密时调用同一个方法就行。
实战中的注意事项
最后分享几个我在项目中积累的经验:
- 密钥管理:密钥不要硬编码在代码里。用密钥管理系统或硬件安全模块(HSM)存储。
- IV生成:每次加密都要生成新的IV。用
os.urandom(16)生成,不要用随机数种子固定的伪随机数。 - 数据填充:ECB和CBC模式要求数据长度是16字节的倍数。gmssl库默认使用PKCS7填充,你不需要手动处理。
- 性能考量:CTR模式可以并行加速。如果加密大量数据,优先选CTR。
重要:生产环境中不要自己实现SM4算法。直接用gmssl、OpenSSL(1.1.1+版本支持SM4)或硬件加密卡。自己实现的算法容易有侧信道攻击漏洞,比如时间攻击、功耗分析。
SM4的算法原理和实现就讲到这里。你想想看,Feistel结构、密钥扩展、工作模式,这些概念其实都不复杂。关键是动手写代码跑一遍,才能真正理解。我建议你拿上面三个示例代码跑一下,看看输出结果,再改改数据试试。