SM4 国密对称加密实战:ECB/CBC/CTR/GCM 四种模式完整指南

实践教程 · 2026-06-03 · 114 阅读

前言

SM4(原名 SMS4)是中国国家密码管理局发布的商用对称加密算法,对应标准 GM/T 0001-2012《SM4 分组密码算法》。自 2012 年发布以来,已成为金融、政务、通信等领域国密改造的核心算法之一。2016 年随 GM/T 0001-2012 正式发布为行业标准,2021 年被 ISO/IEC 18033-3:2010/Amd 1:2018 采纳为国际标准。

与 AES 相比,SM4 在软件实现上性能有差距(AES 有 AES-NI 硬件加速),但作为国密合规的必选项,掌握其正确使用方式至关重要。

本文不是 API 文档的翻译,而是聚焦四个实际问题:

  • 四种模式(ECB/CBC/CTR/GCM)怎么写对,代码可直接复制运行
  • 生产环境怎么封装,包括 PBKDF2 密钥派生、IV 管理、异常处理
  • SM4 到底比 AES 慢多少,有真实测试数据
  • 哪些坑我踩过了,帮你少走弯路
环境要求:Python 3.9+,cryptography >= 3.4(本文测试版本 41.x/42.x)。

BASH
pip install cryptography

一、SM4 算法基础

参数
分组长度128 位(16 字节)
密钥长度128 位(16 字节)
轮数32 轮
结构非平衡 Feistel(Unbalanced Feistel)
标准GM/T 0001-2012
SM4 的密钥长度固定 128 位,不支持 AES-192/AES-256 那样的可变密钥长度。32 轮迭代,每轮使用一个 32 位的轮密钥,共需 32 个轮密钥(由密钥扩展算法从 128 位主密钥生成)。

SM4 的 S 盒是一个 8 位输入、8 位输出的固定置换表,与 AES 的 S 盒结构不同。其轮函数采用非线性变换(S 盒)与线性变换(L 变换)的组合,32 轮迭代后完成一轮完整加密。

与 AES 的结构差异: AES 采用 SPN(Substitution-Permutation Network)结构,而 SM4 采用非平衡 Feistel 结构。两者在软件实现上的性能差异主要来自 AES-NI 指令集的硬件加速,而非算法本身的复杂度差异。

二、四种加密模式完整实现

2.1 ECB 模式(电子密码本)

警告:ECB 模式不推荐用于生产环境。 相同的明文块产生相同的密文块,会泄露数据模式。著名的 "ECB penguin" 问题就是最直观的安全演示。这里仅用于学习和调试。

2.2 CBC 模式(密码块链接)

CBC 是经典的分组加密模式,每个明文块在加密前与前一个密文块异或(第一个块与 IV 异或)。需要随机 IV,且 IV 不需要保密,但必须不可预测。

关键要点:

  • IV 必须每次加密随机生成,绝对不能复用同一个 IV
  • IV 通常拼接在密文前面一起传输(iv + ciphertext
  • CBC 不支持并行加密(链式依赖),但可以并行解密
  • 如果只需要机密性,CBC 够用;如果需要防篡改,用 GCM
  • CBC 存在 padding oracle 攻击风险,生产环境建议用 GCM

2.3 CTR 模式(计数器模式)

CTR 模式将分组密码转换为流密码,不需要填充,天然支持并行加解密。

关键要点:

  • 不需要填充,明文可以是任意长度(包括 0 字节)
  • nonce 绝对不能重复使用(否则密钥流会重复,导致明文泄露)
  • CTR 模式下加密和解密使用完全相同的代码逻辑
  • 支持随机访问(可以单独解密任意位置的密文块)
  • 与 CBC 一样不提供完整性保护,需要额外加 HMAC

2.4 GCM 模式(Galois/计数器模式)

GCM 是 CTR 模式的扩展,在提供机密性的同时提供认证(完整性保护)。它是目前推荐的首选模式,提供 AEAD(Authenticated Encryption with Associated Data)能力。

关键要点:

  • GCM 提供 AEAD(认证加密与附加数据),是目前最推荐的模式
  • nonce 推荐 12 字节,GCM 对 nonce 重复使用极其安全敏感(比 CTR 更严重)
  • AAD(附加认证数据)不参与加密,但参与认证,适合传输元数据(如消息头、用户 ID、时间戳)
  • Tag 长度通常为 16 字节(128 位),提供约 2^-128 的伪造概率
  • GCM 有 2^32 个块(约 64GB)的加密量限制,超过后安全性下降

三、生产级加密工具类

下面是一个完整的、可直接用于生产环境的 SM4 加密工具类,包含 PBKDF2 密钥派生、随机 IV/nonce 管理、密文打包格式、Base64 接口。

四、SM4 vs AES 性能对比

以下数据在同一台机器上实测(Python 3.11, cryptography 41.x, Linux x86_64, Intel CPU 支持 AES-NI)。测试方法:加密 1MB 随机数据,重复 50 次取平均值。

模式SM4 耗时AES-128 耗时AES 加速比SM4 吞吐AES 吞吐
ECB0.457s0.030s15.4x~112 MB/s~1707 MB/s
CBC0.481s0.068s7.1x~106 MB/s~753 MB/s
CTR0.503s0.030s16.8x~102 MB/s~1707 MB/s
GCM0.463s0.038s12.3x~110 MB/s~1347 MB/s
结论:

  • AES 比 SM4 快 7~17 倍,差距来自 AES-NI 硬件指令加速
  • SM4 纯软件实现吞吐量约 100~110 MB/s,对大多数应用足够
  • 如果你的场景是 HTTPS/TLS 握手(数据量小),SM4 的性能差距可以忽略
  • 如果你在加密 GB 级别的大文件,需要考虑性能影响或硬件加速方案
关于硬件加速: 部分国产 CPU(如飞腾 FT-2000、鲲鹏 920、海光)已支持 SM4 硬件指令扩展。在支持 SM4 硬件加速的环境中,OpenSSL 3.x 会自动使用硬件指令,性能可接近 AES-NI 的 AES。如果你的部署环境支持,务必确保 OpenSSL 编译时启用了 SM4 硬件加速支持。

测试脚本(可自行验证):

五、六个真实踩坑记录

坑 1:GCM nonce 重复使用

现象: 加密结果看起来正常,但安全性完全崩溃。

原因: GCM 模式下,同一个密钥 + 同一个 nonce 加密两个不同的明文,攻击者可以异或两份密文得到两份明文的异或值。如果攻击者知道其中一份明文,可以直接恢复另一份明文。这被称为 "forbidden attack"。

错误示例:

PYTHON
# 错误:固定 nonce
NONCE = b'\x00' * 12
for msg in messages:
    cipher = Cipher(algorithms.SM4(key), modes.GCM(NONCE))  # 危险!
    ...

正确做法: 每次加密都用 os.urandom(12) 生成随机 nonce。12 字节随机 nonce 的碰撞概率约为 2^-96,在 2^48 次加密后才有约 50% 的碰撞概率(生日攻击),对绝大多数应用足够安全。

PYTHON
# 正确:随机 nonce
nonce = os.urandom(12)
cipher = Cipher(algorithms.SM4(key), modes.GCM(nonce))

如果你的加密量极大(超过 2^32 次),应考虑使用 nonce 构造方案(如 AES-GCM-SIV 的 nonce 派生方式),或定期轮换密钥。

坑 2:CBC 模式下 IV 复用

现象: 加密能跑通,但安全性低于预期。

原因: CBC 模式下,同一个 IV 加密相同明文会得到相同密文,泄露了"两条消息内容相同"这一信息。更严重的是,如果攻击者知道一个明文-密文对,可以推导出其他使用相同 IV 加密的消息第一个明文块与已知明文块之间的异或关系。

正确做法: 每次加密随机生成 IV,并随密文一起传输。上面的 SM4Cipher 类已经正确处理了这一点——每次 encrypt() 调用都会生成新的随机 IV。

坑 3:PBKDF2 salt 没有随机化

现象: 相同密码总是产生相同密钥。

原因: PBKDF2 的 salt 必须是随机的。如果 salt 固定或省略,相同的密码总是派生出相同的密钥,攻击者可以预先计算彩虹表,一次计算就能破解所有使用相同密码的用户数据。

错误示例:

PYTHON
# 错误:固定 salt
salt = b"fixed-salt-12345"
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=16, salt=salt, iterations=100000)
key = kdf.derive(password.encode())

正确做法: 每次加密生成随机 salt,并将 salt 与密文一起存储(salt 不需要保密)。

PYTHON
# 正确:随机 salt
salt = os.urandom(16)
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=16, salt=salt, iterations=100000)
key = kdf.derive(password.encode())

坑 4:解密时不验证密文完整性(CBC/CTR 模式)

现象: 密文被篡改后解密不会报错,只是输出乱码。

原因: CBC 和 CTR 模式不提供认证(完整性保护)。攻击者可以修改密文,解密后的明文也会被相应修改,程序不会感知。

攻击示例: 在 CTR 模式下,翻转密文的一个比特会导致明文中对应位置的比特也被翻转。如果你加密的是 JSON 数据 {"role":"user"},攻击者可以在不知道密钥的情况下精确修改特定字节,将其变为 {"role":"admin"}

在 CBC 模式下,修改前一个密文块会影响下一个明文块的对应比特(当前块的解密结果会完全损坏,但下一个块只有对应位置被翻转)。

解决方案:

  • 优先使用 GCM 模式(自带认证)
  • 如果必须用 CBC/CTR,额外计算 HMAC:HMAC-SHA256(key_mac, ciphertext),存储在密文旁边
  • 推荐 Encrypt-then-MAC 顺序:先加密,再对密文计算 MAC
  • 注意 MAC 的密钥必须与加密密钥不同(从同一个 master key 派生两个子密钥)

坑 5:密码直接当密钥使用

现象: 代码能跑,但安全性极差。

原因: 用户密码通常是低熵的(可打印 ASCII,长度有限),直接截断或填充为 16 字节作为密钥,暴力破解非常容易。一个 8 位纯小写字母密码只有 26^8 ≈ 2×10^11 种可能,现代 GPU 可以在几小时内穷举。

错误示例:

PYTHON
# 错误:密码直接当密钥
key = password.encode().ljust(16, b'\0')[:16]
cipher = Cipher(algorithms.SM4(key), modes.GCM(os.urandom(12)))

正确做法: 使用 PBKDF2、scrypt 或 Argon2 等密钥派生函数,加入随机 salt 和足够多的迭代次数。

PYTHON
# 正确:PBKDF2 密钥派生
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes

salt = os.urandom(16)
kdf = PBKDF2HMAC(
    algorithm=hashes.SHA256(),
    length=16,
    salt=salt,
    iterations=100_000,  # OWASP 2023 推荐最低值
)
key = kdf.derive(password.encode())

进阶建议: 如果你的应用对安全要求极高,考虑使用 Argon2id(通过 argon2-cffi 库)替代 PBKDF2。Argon2 是 2015 年密码哈希竞赛的获胜者,对 GPU/ASIC 攻击有更强的抵抗力。

坑 6:二进制密文用字符串函数处理

现象: 在 Windows 上加密,Linux 上解密失败;或者密文存入数据库后解密失败;或者通过 HTTP 传输后解密失败。

原因: 密文是二进制数据。常见损坏场景:

  • Windows 下用文本模式读写文件(\r\n 换行符转换)
  • 存入不支持二进制字段的数据库列(如某些 ORM 的 String 字段)
  • 经过 JSON 序列化时没有 Base64 编码(JSON 不支持二进制)
  • 通过 HTTP 传输时字符集转换(如 Latin-1 编码)
解决方案:
  • 文件操作始终使用二进制模式:open("file", "rb") / open("file", "wb")
  • 密文存入文本字段前先 Base64 编码
  • 网络传输时对密文做 Base64 或 Hex 编码
  • 上面的 SM4Cipher.encrypt_b64() / decrypt_b64() 方法已经处理了这个问题
PYTHON
# 错误:直接 JSON 序列化二进制密文
import json
ciphertext = cipher.encrypt(b"hello")
json.dumps({"data": ciphertext})  # TypeError: Object of type bytes is not JSON serializable

# 正确:Base64 编码后序列化
json.dumps({"data": base64.b64encode(ciphertext).decode()})

六、模式选择指南

场景推荐模式原因
通用加密(推荐默认)GCM同时提供机密性 + 完整性,防篡改
数据库字段加密GCMCBCGCM 防篡改;CBC 更兼容旧系统
流式数据 / 网络传输CTRGCM不需要填充,支持流式处理
需要随机访问密文CTR可以独立解密任意密文块
与国密 TLS 集成GCMGM/T 0024-2014(SSL VPN 技术规范)推荐
仅调试/测试ECB最简单,但绝不可用于生产
决策流程:

CODE
需要完整性保护?
├── 是 → GCM
└── 否 → 需要流式/随机访问?
         ├── 是 → CTR(额外加 HMAC)
         └── 否 → CBC(额外加 HMAC)

七、与国密算法体系的集成

在实际的国密改造项目中,SM4 通常不会单独使用,而是与其他国密算法配合:

  • SM2(非对称加密,GM/T 0003-2012):用于密钥协商和数字签名。典型场景是用 SM2 协商出 SM4 的会话密钥。
  • SM3(哈希算法,GM/T 0004-2012):用于消息摘要,替代 SHA-256。可用于 HMAC-SM3 实现消息认证。
  • SM9(标识密码,GM/T 0044-2016):基于身份的加密,无需证书。
典型的国密 TLS 握手流程(GM/T 0024-2014 SSL VPN 技术规范):

  • 客户端发送 ClientHello,包含支持的国密算法套件
  • 服务器选择 SM2/SM4/SM3 套件,返回 ServerHello + SM2 证书
  • 双方使用 SM2 进行密钥协商,生成预主密钥
  • 通过 KDF(基于 SM3)派生出 SM4 会话密钥和 HMAC-SM3 密钥
  • 后续通信用 SM4-GCM 加密,用 SM3 计算消息摘要
Python 生态中,gmssl 库提供了 SM2/SM3/SM4 的完整实现,cryptography 库(3.4+)原生支持 SM4。对于需要完整国密算法栈的项目,可以组合使用这两个库。

总结

  • 优先使用 GCM 模式,它同时提供机密性和完整性保护
  • 永远不要复用 IV/nonce,每次加密必须随机生成
  • 密码必须通过 PBKDF2 等 KDF 派生密钥,不能直接使用
  • 密文是二进制数据,存储和传输时注意编码(Base64)
  • SM4 比 AES 慢 7~17 倍(软件实现),但 100MB/s 的吞吐量对大多数场景足够
  • CBC/CTR 模式不提供完整性保护,如需防篡改,额外加 HMAC 或改用 GCM
  • 关注硬件加速:国产 CPU 的 SM4 指令扩展可以大幅缩小与 AES-NI 的性能差距

参考来源

  • GM/T 0001-2012《SM4 分组密码算法》,国家密码管理局
  • GM/T 0002-2012《SM3 密码杂凑算法》,国家密码管理局
  • GM/T 0003-2012《SM2 椭圆曲线公钥密码算法》,国家密码管理局
  • GM/T 0024-2014《SSL VPN 技术规范》,国家密码管理局
  • GM/T 0044-2016《SM9 标识密码算法》,国家密码管理局
  • Python cryptography 库官方文档:https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/
  • NIST SP 800-38D:Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM)
  • OWASP 2023 Password Storage Cheat Sheet:https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html