4. SM4 对称加密算法(下):工作模式、填充方式与多语言实现
上一讲我们把 SM4 的轮函数和密钥扩展拆开揉碎了讲。这一讲,咱们聊聊真正在工程里怎么用。
说实话,算法本身再漂亮,落地时选错工作模式或者填充方式,照样会出大问题。我见过不少项目,SM4 算法实现没问题,但就因为用了 ECB 模式处理结构化数据,结果被攻击者一眼看穿规律。嗯,这种坑咱们今天必须填上。
4.1 SM4 的四种核心工作模式
工作模式决定了「如何用同一个密钥加密多块数据」。SM4 的分组大小是 16 字节,但你要加密的数据不可能每次都刚好 16 字节。所以,模式的选择直接决定了安全性、性能和并行能力。
4.1.1 ECB 模式(电子密码本模式)
ECB 是最简单的模式。每 16 字节独立加密,互不影响。
- 优点: 可以并行加密/解密,速度快,出错只影响单个块。
- 致命缺点: 相同的明文块 → 相同的密文块。这在加密图片、数据库字段时是灾难。
4.1.2 CBC 模式(密码分组链接模式)
CBC 引入了「初始化向量(IV)」。每个明文块先与前一个密文块 XOR,再加密。第一个块与 IV 异或。
- 优点: 相同的明文块会产生不同的密文块,安全性远高于 ECB。
- 缺点: 加密必须串行(依赖前一个密文块),解密可以并行。IV 必须是不可预测的随机数。
4.1.3 CTR 模式(计数器模式)
CTR 把分组密码变成了流密码。它加密一个递增的计数器值,得到密钥流,再与明文 XOR。
- 优点: 加密和解密都可以完全并行,支持随机访问(可以只解密某一段数据)。
- 缺点: 绝对不能重复使用相同的「密钥 + 计数器组合」。否则,两个密文 XOR 就能消掉密钥流,直接得到明文的 XOR。
我个人很喜欢 CTR 模式,尤其是在嵌入式设备上。因为它不需要填充,而且可以预先生成密钥流,加密时只需要做 XOR 操作,速度极快。
4.1.4 GCM 模式(伽罗瓦/计数器模式)
GCM 是目前最推荐的工作模式。它在 CTR 的基础上增加了认证标签(Authentication Tag),同时提供加密和完整性校验。
- 优点: 加密 + 认证一步到位,能检测密文是否被篡改。支持附加认证数据(AAD),比如协议头。
- 缺点: 实现复杂度稍高,需要硬件加速才能达到最佳性能。
模式对比速查表
| 模式 | 安全性 | 并行加密 | 并行解密 | 是否需要填充 | 是否提供认证 |
|---|---|---|---|---|---|
| ECB | 低(模式分析) | 是 | 是 | 是 | 否 |
| CBC | 中(需随机 IV) | 否 | 是 | 是 | 否 |
| CTR | 高(需唯一 nonce) | 是 | 是 | 否 | 否 |
| GCM | 高(加密+认证) | 是 | 是 | 否 | 是 |
4.2 填充方式:PKCS7 与 ZeroPadding
ECB 和 CBC 模式要求明文长度是 16 字节的整数倍。如果不够,就需要填充。
4.2.1 PKCS7 填充
PKCS7 是业界最通用的填充方式。规则很简单:缺几个字节,就填充几个值为「缺的字节数」的字节。
- 如果明文正好是 16 的倍数,PKCS7 会额外填充一整块 16 个 0x10。
- 解密时,读取最后一个字节的值 n,然后删除最后 n 个字节。
4.2.2 ZeroPadding 填充
ZeroPadding 用 0x00 填充剩余字节。简单粗暴,但有个致命问题:如果明文末尾本身就有 0x00,解密后无法判断哪些 0x00 是填充、哪些是原始数据。
4.3 SM4 工作模式与填充关系图
下面这张图展示了不同工作模式如何处理数据块,以及填充在其中的位置。
4.4 多语言代码实现
理论说完了,直接上代码。我会用 Java、Python 和 Go 分别实现 SM4-CBC 和 SM4-GCM 模式。你想想看,实际项目中你大概率只需要这两种模式。
4.4.1 Java 实现(使用 Bouncy Castle)
Java 生态里,Bouncy Castle 是最成熟的国密支持库。我个人习惯用 org.bouncycastle.pqc.legacy.crypto.sm4 包。
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
public class SM4Example {
static {
Security.addProvider(new BouncyCastleProvider());
}
// SM4-CBC 加密
public static byte[] encryptCBC(byte[] key, byte[] iv, byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC");
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(data);
}
// SM4-GCM 加密(推荐)
public static byte[] encryptGCM(byte[] key, byte[] nonce, byte[] data, byte[] aad) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/GCM/NoPadding", "BC");
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, nonce);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
cipher.updateAAD(aad);
return cipher.doFinal(data); // 密文 + 16 字节认证标签
}
}
4.4.2 Python 实现(使用 gmssl 库)
Python 下我推荐 gmssl 库,它是纯 Python 实现,兼容性很好。
from gmssl.sm4 import CryptSM4, SM4_CBC, SM4_GCM
import os
# SM4-CBC 加密
def sm4_cbc_encrypt(key: bytes, data: bytes) -> bytes:
iv = os.urandom(16)
crypt = CryptSM4()
crypt.set_key(key, SM4_CBC)
crypt.set_iv(iv)
ciphertext = crypt.crypt_ecb(data) # 注意:gmssl 的 CBC 接口叫 crypt_ecb
return iv + ciphertext # IV 拼在前面
# SM4-GCM 加密
def sm4_gcm_encrypt(key: bytes, data: bytes, aad: bytes = b'') -> bytes:
nonce = os.urandom(12)
crypt = CryptSM4()
crypt.set_key(key, SM4_GCM)
crypt.set_iv(nonce)
crypt.set_aad(aad)
ciphertext = crypt.crypt_gcm(data)
tag = crypt.get_tag()
return nonce + ciphertext + tag # nonce + 密文 + 认证标签
crypt_ecb 但实际上处理的是 CBC 模式。一定要看文档确认。另外,Python 的 os.urandom 是安全的随机数生成器,不要用 random 模块生成 IV 或 nonce。
4.4.3 Go 实现(使用 tjfoc/gmsm 库)
Go 语言下,github.com/tjfoc/gmsm/sm4 是最主流的国密库。
package main
import (
"crypto/rand"
"github.com/tjfoc/gmsm/sm4"
)
// SM4-CBC 加密
func SM4CBCEncrypt(key, plaintext []byte) ([]byte, error) {
iv := make([]byte, sm4.BlockSize)
rand.Read(iv)
ciphertext, err := sm4.Sm4Cbc(key, plaintext, iv, true) // true = 加密
if err != nil {
return nil, err
}
return append(iv, ciphertext...), nil
}
// SM4-GCM 加密
func SM4GCMEncrypt(key, plaintext, aad []byte) ([]byte, error) {
nonce := make([]byte, 12)
rand.Read(nonce)
ciphertext, err := sm4.Sm4Gcm(key, nonce, plaintext, aad, nil, true)
if err != nil {
return nil, err
}
// ciphertext 包含密文 + 16 字节认证标签
return append(nonce, ciphertext...), nil
}
1. 生成随机 IV/nonce(CBC 用 16 字节,GCM 用 12 字节)
2. 将 IV/nonce 拼在密文前面一起存储或传输
3. 解密时先取出 IV/nonce,再解密剩余部分
4. GCM 模式一定要验证认证标签,否则等于没用
4.5 实战中的选择建议
说了这么多,到底怎么选?我直接给你一个决策树:
- 新项目? 无脑选 SM4-GCM。加密 + 认证一步到位,省心。
- 需要兼容旧系统? 选 SM4-CBC + PKCS7。这是最广泛支持的组合。
- 嵌入式或硬件资源受限? 选 SM4-CTR。不需要填充,可以预计算密钥流。
- 加密随机数据(如密钥)? 可以用 SM4-ECB,但仅限此场景。