ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

国密算法封装避坑指南:SM2/SM3/SM4参数、格式与校验

国密算法封装避坑指南:SM2/SM3/SM4参数、格式与校验 简介这份压缩包汇集了国密算法SM2/SM3/SM4的完整封装实现主要面向需要在项目中落地国密加密的Java或Android开发人员。内容以Java源码和工程配置为主包含11个java文件、15个xml配置、3个jar依赖库以及gradle构建脚本共52个文件压缩包仅3.15MB轻量易用。包中自带sm-1.0.jar封装库和bcprov-jdk16-1.46.jar加密提供者配合build.gradle与proguard-rules.pro文件可快速集成到既有工程。目录结构按模块划分清晰便于对照学习算法核心逻辑与调用方式。目前已有441人学习下载适合具备一定密码学基础、希望在业务系统中使用国密算法进行数据加密与签名验签的开发者参考能有效节省自行调研与重复封装的时间。1. 国密算法封装到底封装了什么把国密算法SM2、SM3、SM4从开源库搬到业务代码里卡住团队的地方从来不是算法本身而是封装边界。同样是SM2加密旧标准输出C1C2C3新标准要求C1C3C2两边都认为自己是国密联调时对不上SM3摘要的大小写、SM4的IV和Padding不同都能让加解密结果完全不可读。所以标题里的“算法封装”真正要解决的是三件事统一算法参数、固定编码格式、提供可替换的接口。下面的内容不依赖某个神秘jar包而是按一线项目里常见封装方案把SM2、SM3、SM4的原理、参数、代码和测试串起来。适合要自研封装、做国产化适配、或者正被跨平台联调折磨的工程师。拿到一个封装包时第一件事不是跑demo而是确认它对外的数据格式签名是DER还是原生的r||sSM2密文是C1C3C2还是C1C2C3密钥是PEM还是裸字节。这些格式不统一后续所有环节都会跟着错。2. SM3摘要与SM4对称加密封装前的参数与选型对称和摘要算法看起来简单但封装出错几乎都出在细节上。SM3和SM4的一个优点是性能好、内存占用小缺点是生态里很多参考实现只覆盖了最基础的调用没有处理并发、填充和跨平台编码。在封装之前先要对这两个算法本身有个准确判断。2.1 SM3/SM4与AES256的核心差异很多人把SM4和AES256放在同一档粗略看都是对称加密实际强度差了一个量级。AES256的分组也是128位但密钥长度是256位暴力破解强度比密钥只有128位的SM4高很多如果严格按密钥长度来对标SM4更接近AES128。SM3摘要输出256位结构与SHA256同源但在消息填充的字节序和初始值上并不相同不能拿SHA256的封装习惯直接套用。那AES256和SM2、SM4的区别到底是什么一句话就能说清AES256是对称算法一把密钥同时做加解密适合大数据量SM2是非对称算法公钥加密、私钥解密适合做数字签名和小数据量密钥传递SM4是对称算法用来给业务数据做批量加密。在商用密码场景里最常见的组合是SM2协商或分发密钥SM4加密业务数据SM3做完整性校验。封装时不要只提供一个算法栈要把这个组合关系固化到接口设计里。2.2 SM3的消息填充与长度扩展攻击SM3处理消息时先补位使消息长度模512等于448再在后面填充64位原始比特长度。最后的分组经过压缩函数输出256位摘要。这种结构决定了它和SHA256一样不适合直接把密钥放在数据前面当MAC用。如果你写出SM3(key data)这种代码攻击者即便不知道key只要知道摘要值和消息总长度就能推算出扩展后的合法摘要这个叫长度扩展攻击。所以封装SM3时如果业务需要消息认证码必须用HMAC-SM3不要自己拼接密钥。# OpenSSL 1.1.1 直接用命令行算SM3用于验证封装结果 echo -n abc | openssl dgst -SM3这个命令输出的是abc的SM3摘要应该用标准向量做比对。更重要的是命令行工具能用来判断测试环境中OpenSSL自身是否支持国密很多老系统卡在底层openssl没有SM3导致签名时直接报algorithm not available。大小写不影响摘要值但封装层建议统一输出小写十六进制避免上层做字符串比较时分心。2.3 SM4的模式、IV和Padding参数SM4密钥必须是16字节分组也是16字节这是硬规定。密码本没有任何选择余地但工作模式可以选这一选就产生了跨语言兼容问题。下面是封装时最常用的一组模式对照模式IV长度是否需要完整性保护推荐场景常见坑ECB0不需要只用于单块数据测试相同明文生成相同密文不适合数据库字段加密CBC16字节需要额外HMAC文件、消息加密IV不可复用否则前16字节明文会被恢复CTR16字节需要额外HMAC流式加密counter初始值固定易重放GCM12字节推荐自带认证网络传输、API加密不是国标模式合规场景慎用GCM不是国密标准里定义的模式但在真实项目里很有用因为BC和OpenSSL都支持。如果严格要求全部走国密规范我更习惯用CBC加上HMAC-SM3保证机密性和完整性分开管理。ECB模式在封装里要默认禁用除非接口文档里明确写了只做单分组测试。下面是一段常用的Java封装显式使用BouncyCastle providerimport 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 Sm4Util { static { if (Security.getProvider(BC) null) { Security.addProvider(new BouncyCastleProvider()); } } public static byte[] cbcEncrypt(byte[] key, byte[] iv, byte[] data) throws Exception { Cipher cipher Cipher.getInstance(SM4/CBC/PKCS7Padding, BC); // 每次调用都新Cipher避免并发下同一个实例内部状态被破坏 cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, SM4), new IvParameterSpec(iv)); return cipher.doFinal(data); } }这段代码里三个参数最容易改坏第一个是算法名里的SM4不能写成AES第二个是provider的字符串“BC”如果没有在Cipher.getInstance里带上很可能走错实现第三个是IV长度必须是16字节长度不对会在init阶段直接抛InvalidAlgorithmParameterException。Cipher实例不是线程安全的封装成静态工具时不要复用同一个实例。2.4 封装工具类要暴露什么接口我不建议把底层API都暴露给业务方否则每次升级都会波及调用方。封装类只需要稳定暴露几个方法sm3Hex(byte[])、sm4CbcEncrypt/Decrypt、hmacSm3Hex(byte[], byte[])再加上一个版本号常量。内部把Provider注册、模式选择、密钥校验都收敛到一处。业务代码只关心字节进、字节出不关心你用的是什么实现。3. SM2非对称加密与数字签名封装最容易翻车的区域SM2在构造上比SM3、SM4复杂很多因为它同时覆盖加密、签名、密钥交换三种用途。封装时最常遇到的问题不是算法跑不通而是不同平台对同一份数据产出的签名和密文格式不一样。下面按底层原理到代码实现的顺序把这部分讲清楚。3.1 SM2算法结构与RSA/ECDSA的定位差异SM2基于椭圆曲线推荐曲线是sm2p256v1公钥是曲线上的一个点包含x和y坐标私钥是一个大整数。和RSA相比相同安全强度下SM2密钥更短、计算更快和ECDSA相比SM2的数字签名过程是独立设计的不是标准ECDSA参数替换。“sm2数字签名”通常指使用SM3作为哈希随机数k参与运算最终输出r和s两个大整数。签名过程混入了ZA值这个ZA由用户ID、曲线参数和公钥共同计算得到。也就是说同样一段数据和同一把私钥如果双方计算ZA时用的用户ID不同验签就会失败。封装签名方法时必须在接口里显式暴露userID参数并给一个默认值同时注明默认值来自规范的推荐用户ID。3.2 公钥编码与C1C3C2密文排布SM2加密输出的密文由三部分组成C1是椭圆曲线上的随机点C3是摘要C2是真正的密文。国标GB/T 32918.4-2016给出的顺序是C1C3C2但历史上很多平台默认输出C1C2C3。封装包对外的文档如果只写“SM2加密”不写这三段顺序接收方很容易在解密时把摘要和密文位置搞混最终抛DataLengthMismatch异常。C1本身也有两种常见编码未压缩点以04开头后跟x、y坐标各32字节总长度65字节压缩点以02或03开头后跟32字节总长度33字节。C3固定32字节。所以SM2密文最小长度是33 32 明文长度最常用的是65 32 明文长度。封装时要固定选择一种点编码方式避免同一个包在Java和C侧生成不同长度的密文。3.3 Java里封装SM2签名、验签的最小代码下面这段代码可以跑通基本链路但注意它使用的是BouncyCastle的默认签名编码import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.*; public class Sm2SignExample { public static byte[] sign(PrivateKey privateKey, byte[] data) throws Exception { Security.addProvider(new BouncyCastleProvider()); Signature signature Signature.getInstance(SM3withSM2, BC); signature.initSign(privateKey); signature.update(data); return signature.sign(); } public static boolean verify(PublicKey publicKey, byte[] data, byte[] signBytes) throws Exception { Signature signature Signature.getInstance(SM3withSM2, BC); signature.initVerify(publicKey); signature.update(data); return signature.verify(signBytes); } }这段代码的签名输出是DER编码的ASN.1结构不是国标文档里直接定义的64字节r||s拼接。如果对方平台要求原始字节需要自己解析DER或者在密钥生成和签名时指定使用PlainDSAEncoding。另一个痛点是这里没有处理userIDBC内部默认使用“1234567812345678”这个字符串计算ZA。一旦对方用的是别的用户ID验签必然失败。要处理这种情况就需要自己实现ZA计算并把ZA参与签名更新的过程写清楚。3.4 国密算法逆向排查从字节流定位格式问题出现“国密算法逆向”这类搜索词多半是联调时本地解不开对方数据需要把两端数据切碎来看。我一般会把两端的密文导出成十六进制先看C1第一个字节。以04开头说明是未压缩点往后读65字节以02或03开头说明是压缩点往后读33字节。找到C1结束位置后再往后读32字节就是C3最后剩下的长度是C2明文密文段。如果总长度对不上再检查公钥是否被外层做了一个ASN.1封装。很多Java库导出的公钥是SubjectPublicKeyInfo结构前面多了一段算法标识直接用裸公钥字节去解析就会前后错位。签名也一样DER编码的签名开头通常是30原生r||s拼接开头则是靠近32大小的乱数看前两个字节就能分辨。4. 用SM2、SM3、SM4封装做数据库国密测试数据库国密测试是封装库最常被考到的场景因为只靠单元测试无法证明封装在真实存储链路里没问题。这里要解决的是字段加密、密文存储、解密回读以及测试数据如何构造四个问题。4.1 字段加密的两种落地形态第一种是应用层在insert前把字段加密存到VARBINARY或BLOB列中查询后解密第二种是数据库自带的透明加密对业务SQL无感。如果是自研封装我更推荐应用层加密因为能直接复用SM2/SM3/SM4的代码也方便做字段级授权管理。密钥不要放在JVM参数或配置明文里至少用环境变量或外部KMS。透明加密看着省事但测试覆盖不到应用层封装逻辑而且不同国产数据库实现差异很大。做数据库国密测试最终要验证的还是落到行上的字节是否满足预期。应用层加密能把算法封装和数据库隔离测试时也更容易定位是SQL问题还是加密问题。4.2 生成SM2加密数据的测试命令与SQL以一个user表为例手机号字段加密。测试步骤是生成SM2密钥对用公钥加密手机号把密文写入数据库再用私钥解密。下面是核心代码public class GmDbTest { public static void main(String[] args) throws Exception { Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); kpg.initialize(new ECGenParameterSpec(sm2p256v1), new SecureRandom()); KeyPair pair kpg.generateKeyPair(); String phone 13800138000; byte[] cipher encrypt(pair.getPublic(), phone.getBytes(java.nio.charset.StandardCharsets.UTF_8)); String sql INSERT INTO t_user(uid, phone_cipher) VALUES (?,?); // ps.setString(1, 10001); // ps.setString(2, Base64.getEncoder().encodeToString(cipher)); } }这里用Base64存储密文是为了让VARCHAR列在调试时能看到固定字符集避免二进制乱码。数据库列建议用VARCHAR(256)不要用CHAR(20)这种定长。SM2加密同一个明文每次生成的密文都不同因为C1是随机点。测试用例要覆盖三条密文不同但解密结果一致私钥能还原明文私钥错误时解密失败或抛异常。如果要回答“sm2如何做数据库国密测试”最直接的做法就是固定一套测试密钥跑通“加密 - 写入 - 读取 - 解密”四步最后用JUnit断言解密结果和原始明文相同。注意测试密钥要单独生成不能直接用生产密钥。4.3 密文字段长度估算与性能基准算法加密后长度增长SM4密文长度 明文长度 16字节PKCS7填充SM2密文长度 65 32 明文长度按上面的公式手机号11字节时SM2密文是108字节Base64编码后约144字符。设计字段时至少要给到VARCHAR(200)。如果字段加密前是变长文本比如身份证号直接算最大长度不要用平均值。性能上SM2公钥加密一次大约几毫秒到十几毫秒SM4加密每秒可以处理几十兆数据。批量数据场景建议用SM4加密业务字段再用SM2加密SM4的会话密钥避免每条记录都做一次非对称运算这也是热词“aes256和sm2、sm4的区别”在实际应用层的体现。4.4 连接层国密通信的验证如果数据库驱动支持GM-JDBC还需要验证连接是否真正启用了国密SSL。最简单的做法是先看驱动连接串里的sslMode参数再抓一次握手报文确认协商出的密码套件包含国密标识。这部分和算法封装关系不大但国产化项目验收时经常被问到。封装包里如果提供了这样的驱动记得单独写一个测试用例做连通性验证。5. 用标准测试向量做封装包的最后一轮校验封装包打zip之前我会先做一轮标准向量校验。SM3的标准向量是输入abc输出66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0。SM4的标准向量是密钥和明文都为0123456789abcdeffedcba9876543210时密文为681edf34d206965e86b3e94f536e4246。这两个向量可以在命令行直接验证#!/usr/bin/env bash set -euo pipefail # 校验SM3 echo -n abc | openssl dgst -SM3 # 校验SM4 ECB不带padding echo -n 0123456789abcdeffedcba9876543210 | xxd -r -p | \ openssl enc -sm4-ecb -K 0123456789abcdeffedcba9876543210 -nopad | xxd -p第一段命令输出大小写可能随OpenSSL版本变化比对前先统一转小写。第二段命令如果输出不是681edf34d206965e86b3e94f536e4246不要怀疑OpenSSL先检查密钥和明文是否各自多了一个换行符。SM2的测试向量比较长我习惯直接用BouncyCastle自带的SM2测试类跑一遍密钥生成、加密、解密、签名、验签不在命令行里硬比。最后一轮检查可以整理成一张清单检查项推荐方式失败时先看哪里SM3标准向量openssl dgst -SM3输入字节是否包含换行SM4标准向量openssl enc -sm4-ecbPadding有没有被默认加上SM2加密长度计算6532明文长度C1是否用了压缩点SM2签名字节数查看开头是30还是r值BC默认DER编码HMAC-SM3和独立库做交叉验证key归一化是否做了UTF-8我习惯把这些检查写进发布脚本每次打包前跑一遍确保重新编译的封装包能和上一版保持一致。如果测试向量没过先查代码里是否有人覆盖了Padding或修改了SM3的初始常量这是最常改坏的地方。本文还有配套的精品资源点击获取
返回列表