4、加密与解密异常:密文格式不规范、C1C2C3顺序搞反、填充模式不兼容
加密解密这块,说实话,是 SM2 落地时最容易出幺蛾子的地方。我见过太多团队,算法本身跑得没问题,但一联调就报错——解密失败、数据对不上、甚至直接崩溃。追根溯源,十有八九是这三个坑:密文格式不规范、C1C2C3 顺序搞反、填充模式不兼容。
咱们一个一个说。
4.1 密文格式:ASN.1 还是拼接?这是个问题
SM2 的密文由三部分组成:C1(椭圆曲线点,65 字节或 97 字节)、C2(密文数据,长度等于明文)、C3(杂凑值,32 字节)。但问题来了——这三块怎么组装?
目前主流有两种格式:
| 格式类型 | 描述 | 典型场景 |
|---|---|---|
| ASN.1 DER 编码 | 按标准语法封装,带类型标签和长度 | GM/T 标准、证书体系、HSM 硬件 |
| 简单拼接 | C1 || C2 || C3 或 C1 || C3 || C2 | 开源库、轻量级实现、自定义协议 |
我个人习惯,优先用 ASN.1 格式。为什么?因为标准明确,互操作性最好。但很多项目图省事,直接拼接——结果就是,A 库加密的密文,B 库解不了。
⚠️ 注意: 如果你用拼接格式,一定要在协议文档里写清楚顺序和长度。否则半年后换库,谁都记不住当初怎么拼的。
举个例子。我在一个物联网项目里遇到过:设备端用 OpenSSL 的 SM2 加密,服务端用 GmSSL 解密。OpenSSL 默认输出 ASN.1,GmSSL 默认期望拼接格式。两边一对接,解密全失败。最后我在服务端加了个格式检测——先看第一个字节是不是 0x30(ASN.1 SEQUENCE 标签),是就走 ASN.1 解析,否则按拼接处理。这才搞定。
4.2 C1C2C3 顺序:搞反了就是灾难
这个坑,我敢说 80% 的 SM2 新手都踩过。标准里其实写得很清楚:C1 || C2 || C3。但有些实现偏偏用 C1 || C3 || C2,甚至 C2 || C1 || C3。
为什么会这样?
嗯,这里有个历史原因。SM2 的早期草案和某些国际标准(比如 ISO/IEC 18033-2)对顺序的定义不一样。有些库为了兼容其他算法,顺手就把 C3 放中间了。结果就是——你加密出来的东西,换个库就解不开。
💡 核心原则: 加密方和解密方必须使用完全相同的顺序。这不是谁对谁错的问题,是约定必须一致的问题。
我曾经帮一个金融客户排查问题:他们的加密机输出的是 C1||C3||C2,但 Java 端的 SDK 按 C1||C2||C3 解析。解密出来的数据前 32 字节全是乱码,后面才是明文。你想想看,这要是交易报文,后果多严重。
怎么避免?我建议你在接口设计阶段就明确约定顺序,并且在代码里加个魔数校验。比如:
// 示例:在密文头部加 1 字节标识顺序
// 0x01: C1||C2||C3
// 0x02: C1||C3||C2
byte flag = ciphertext[0];
if (flag == 0x01) {
// 按 C1||C2||C3 解析
} else if (flag == 0x02) {
// 按 C1||C3||C2 解析
} else {
throw new IllegalArgumentException("未知的密文格式");
}
当然,这只是个思路。实际项目中你可以用版本号、协议 ID 等方式来标识。关键是——别让解密方去猜顺序。
4.3 填充模式:不兼容的噩梦
SM2 加密时,C2 的长度必须等于明文长度。但明文长度不一定是 16 的倍数啊?这就涉及填充了。
常见的填充模式有:
- NoPadding:明文长度必须正好是分组大小的倍数,否则报错
- PKCS7Padding:缺多少补多少,每个字节的值就是缺的字节数
- ISO10126Padding:最后一个字节是填充长度,前面填充随机数
- ZeroPadding:补 0x00,但无法区分数据和填充
问题来了——SM2 本身是流密码模式,它不需要填充!C2 的长度就是明文长度,多一个字节都不行。
⚠️ 常见误区: 很多人把 SM2 当成分组密码来用,自动加了 PKCS7 填充。结果解密后多出一串 0x08 0x08... 的尾巴。解密方如果没做去填充处理,数据就多了几个字节。
我记得有个项目,A 方用 Bouncy Castle 的 SM2 加密,B 方用自研库解密。Bouncy Castle 默认不填充,但自研库在解密后自动去掉了 PKCS7 填充。结果就是:加密 "Hello"(5 字节),解密出来只剩 "He"(前 2 字节),后面 3 字节被当成填充删掉了。
排查了整整两天,最后发现是填充模式不匹配。解决方案很简单:双方约定使用 NoPadding,明文长度由上层协议保证。
4.4 一张图理清加密异常的全貌
说了这么多,咱们用一张图把这三个问题的关系串起来:
4.5 实战避坑清单
最后,我把自己踩过的坑整理成一份清单,你照着检查,能省不少时间:
- 确认密文格式:加密方输出的是 ASN.1 还是拼接?解密方支持哪种?
- 确认 C1C2C3 顺序:在接口文档里白纸黑字写清楚,别口头约定
- 确认填充模式:SM2 不需要填充,统一用 NoPadding
- 加个格式检测:解密时先判断密文格式,避免硬编码
- 写单元测试:用已知的测试向量验证加解密一致性
- 联调时先打日志:把密文 hex 打印出来,人工比对长度和结构
🔧 小技巧: 联调时,让双方各自加密一段固定明文(比如 "SM2Test"),然后交换密文的 hex 字符串。如果两边能互相解密成功,说明格式、顺序、填充都没问题。这一步走通了,后面就稳了。
说白了,SM2 加密解密本身并不复杂。复杂的是约定——你用什么格式,我按什么顺序,大家用不用填充。只要这三件事对齐了,99% 的异常都不会发生。