国密 SM4 对称加密:原理与 Python 加解密实战
从零跑通:一段代码加密再解密看到往返一致,讲清 ECB/CBC 与 key/iv/编码四个坑。
做接口时经常遇到:请求体或响应体不是明文 JSON,而是一串看不懂的 base64/十六进制,前端用一段 JS 加解密。国内很多系统用的是国密 SM4。这篇把 SM4 讲清楚,并写成从零跑通的教程——一段完整代码,加密再解密还原,你能亲眼看到"往返一致"。
SM4 是什么
SM4 是中国国家密码标准(GB/T 32907)里的对称分组加密算法。三个词拆开说:
- 对称:加密解密用同一把密钥。和 RSA 那种"公钥加密、私钥解密"不一样,你拿到密钥就能同时加解密。
- 分组:一次只处理固定长度一块数据。SM4 的分组和密钥都是 16 字节。数据长了切成一块块,不够 16 字节要补齐(padding)。
- 可以理解成"国产 AES"——用法、分组大小、思路都和 AES 几乎一样,只是内部算法是国密自己的一套。
对逆向最重要的一句:SM4 是标准算法,只要拿到那把 16 字节密钥(CBC 模式还要 16 字节 IV),加解密就是调库的事。 难点从来不是算法,而是"密钥藏在哪"。
第一步:装环境
pip install gmssl
第二步:完整代码,直接复制
新建 demo_sm4.py,整段贴进去。它把一段 JSON 加密成 base64,再解密回来,最后比对是否一致:
# demo_sm4.py 依赖:pip install gmssl
import base64
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
KEY = "1234567890abcdef" # 16 字节密钥(演示用;真实场景 key 来自抓包/接口)
def sm4_ecb_encrypt(key, text):
c = CryptSM4(); c.set_key(key.encode(), SM4_ENCRYPT)
return base64.b64encode(c.crypt_ecb(text.encode())).decode() # 密文常用 base64 装
def sm4_ecb_decrypt(key, b64):
c = CryptSM4(); c.set_key(key.encode(), SM4_DECRYPT)
return c.crypt_ecb(base64.b64decode(b64)).decode() # 先还原字节再解密
if __name__ == "__main__":
plain = '{"user":"zhang","code":1000}'
enc = sm4_ecb_encrypt(KEY, plain)
dec = sm4_ecb_decrypt(KEY, enc)
print("原文 :", plain)
print("加密后:", enc)
print("解密后:", dec)
print("往返一致:", plain == dec)
第三步:运行,看结果
python demo_sm4.py
输出:
原文 : {"user":"zhang","code":1000}
加密后: Hp7/NNpolA7qJzIoIE6jr7gPqI3NVnS7NNH9P6WsfUs=
解密后: {"user":"zhang","code":1000}
往返一致: True
往返一致: True 说明加密解密对上了。真实场景里,接口返回的那串密文,就用 sm4_ecb_decrypt 加上正确的 key 解出明文 JSON;要构造加密请求体,就用 sm4_ecb_encrypt。
两种模式:ECB 和 CBC
数据切成好几块后,"块与块之间怎么处理"有不同模式,最常见 ECB 和 CBC:

- ECB(电子密码本):每块各自独立加密。最简单,只需一把密钥。缺点是相同明文块会加出相同密文块,安全性差,但很多系统图省事就用它(上面代码就是 ECB)。
- CBC(密码块链接):每块加密前先和上一块密文异或,第一块和一个 16 字节的 IV(初始向量)异或。相同明文也能加出不同密文,更安全。用 CBC 需要两样:密钥 + IV。
CBC 的解密只比 ECB 多传一个 IV:
def sm4_cbc_decrypt(key, iv, b64):
c = CryptSM4(); c.set_key(key.encode(), SM4_DECRYPT)
return c.crypt_cbc(iv.encode(), base64.b64decode(b64)).decode() # 多一个 iv
逆向时先判断模式:JS 里加密只传一个 key,多半是 ECB;还传了 iv,就是 CBC。
逆向时的四个坑
① key/iv 的编码方式要对上。 密钥可能是普通字符串(.encode() 得 16 字节),也可能是 16 进制字符串(32 个十六进制字符,要 bytes.fromhex()),还可能是 base64。看 JS 里怎么把 key 传进加密函数,用一样的方式转字节。转错了解出来就是乱码。
key_bytes = bytes.fromhex("0123...") # JS 里 key 是 16 进制
key_bytes = "abcdef0123456789".encode() # JS 里 key 是普通 16 位字符串
② 密文载体是 base64 还是 hex,别搞混。 base64 含大小写字母数字和 +/=,用 base64.b64decode;纯十六进制只有 0-9a-f,用 bytes.fromhex。用错第一步就报错。
③ padding。 分组 16 字节,不够要补,最常见是 PKCS7,gmssl 默认按这个处理。如果解出来的明文尾部有几个多余字节(像 \x05\x05\x05\x05\x05),是 padding 没去掉,说明对方补齐方式不同,得手动去。
④ 密钥到底在哪。 这才是真正花时间的地方。key/iv 可能写死在 JS 里,也可能登录后服务端下发、每次会话都变。写死的直接抠出来用;动态的要顺着 JS 找到"服务端把 key 发给前端"的那个接口,先拿到 key 再解密。
总结一句:遇到 SM4 不要慌,它就是国产 AES,标准算法、gmssl 三行搞定。真正要下功夫的是三件事——哪种模式、key/iv 什么编码、密钥从哪来。这三样对上,再看不懂的加密报文也能还原成明文。
评论 0