Java国密算法SM2/SM3/SM4加解密实战:密钥生成、签名验签与数据加密 简介面向Java开发者的国密算法实现资源完整覆盖SM2非对称加解密、SM3消息摘要、SM4对称加解密三大核心国产密码算法适用于政企信息系统国产化改造、接口安全加固及国密算法学习研究场景。资源包共9个文件包含8个Java源码文件与1个Maven工程配置文件整体大小仅14KB代码紧凑且目录清晰便于直接导入工程参考。已有785人浏览学习具备较强实践参考价值。借助该资源读者可以快速理解SM2密钥对生成、SM3摘要计算、SM4加解密工具类的实现思路并通过附带的测试用例掌握各类算法的调用方式从而降低国密算法的落地门槛提升项目开发效率。无论是密码学初学者还是需要快速集成国密能力的开发者都能获得可直接改造复用的代码缩短开发排错时间。1. Java 实现国密 SM2-SM3-SM4 加解密先分清三条技术路线密评、等保改造、政企对接这三类需求一出现Java 工程师接到的第一份资料往往就是这个标题的 zip里面是 SM2、SM3、SM4 三个算法的工具类、密钥脚本和调用示例。很多人复制出来就跑不通卡在模式不匹配上。先说清楚一个基本认知SM2 是非对称加密负责数字信封和数字签名SM3 输出 32 字节摘要SM4 是对称分组加密适合大批量数据。三者是配套关系不是替代关系任务是把 RSA/AES 的国际链路换成国标算法。适合对接政务系统、做密评整改或想替换加密组件的后端工程师。2. SM2 非对称加解密与数字签名的 Bouncy Castle 实现2.1 先选依赖Bouncy Castle 覆盖国密曲线的理由JDK 标准库到目前为止都没有内置 SM2 曲线所以第一步是引入 Bouncy Castle。Maven 坐标用 bcprov-jdk18on具体版本选当时的最新版即可。BC 从 1.57 之后的版本开始陆续补齐国密算法到 1.6x 这一代sm2p256v1 曲线的密钥生成、SM2Engine 加解密、SM3withSM2 签名已经是完整可用的状态市面上常见的国密工具包底层也大多直接封装 BC。选 BC 还有一层原因它同时提供两套 API。JCE 风格Security.addProvider 之后走 Cipher / Signature代码短和 KeyStore、X.509 证书体系天然兼容低层 org.bouncycastle.crypto API 不依赖 Provider 注册适合封装进自己的工具类也便于控制密文格式。下面的代码两种都会出现实际项目里建议低层 API 负责密钥生成和 SM2 加解密JCE API 负责签名验签和证书解析。2.2 生成 SM2 密钥对并用 PKCS8/X.509 格式落盘SM2 密钥生成的参数不是随便选的。代码里用 GMNamedCurves 取 sm2p256v1这是 GM/T 0003 规定的推荐曲线等价于 OID 1.2.156.10197.1.301import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.generators.ECKeyPairGenerator; import org.bouncycastle.crypto.params.*; import org.bouncycastle.crypto.util.PrivateKeyInfoFactory; import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory; import java.security.SecureRandom; X9ECParameters x9 GMNamedCurves.getByName(sm2p256v1); ECDomainParameters domain new ECDomainParameters( x9.getCurve(), x9.getG(), x9.getN(), x9.getH()); ECKeyPairGenerator gen new ECKeyPairGenerator(); gen.init(new ECKeyGenerationParameters(domain, new SecureRandom())); AsymmetricCipherKeyPair pair gen.generateKeyPair(); ECPrivateKeyParameters pri (ECPrivateKeyParameters) pair.getPrivate(); ECPublicKeyParameters pub (ECPublicKeyParameters) pair.getPublic(); // 导出为标准化编码便于落盘和跨系统交换 byte[] pkcs8 PrivateKeyInfoFactory.createPrivateKeyInfo(pri).getEncoded(); byte[] spki SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(pub).getEncoded();参数说明ECDomainParameters 的四个参数分别对应曲线方程、基点 G、阶 n 和余因子 h签名验签的标量运算全部依赖它们。导出私钥选 PKCS8、公钥选 X.509 SubjectPublicKeyInfo是因为这两类编码是加密机、KMS、CA 证书体系通用的格式直接存 D 值私钥大整数再加 Base64 的写法在自研工具里能跑遇到密码机联调就没法互通了。还原时用 PrivateKeyFactory.createKey / PublicKeyFactory.createKey 解析字节数组即可。如果项目里更习惯 JCE 风格也可以这样生成KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); kpg.initialize(new ECGenParameterSpec(sm2p256v1), new SecureRandom()); KeyPair kp kpg.generateKeyPair();两种生成的密钥对象类型不同混用前需要互转。我的习惯是工具类内部统一用低层参数对象对外暴露时才转成 JCE 的 BCECPublicKey / BCECPrivateKey。2.3 SM2 加解密必须说清楚的 C1C3C2 输出结构SM2 加密的输出和 RSA 完全不同密文由三块拼接而成。C1 是椭圆曲线上的一个点非压缩格式固定 65 字节以 0x04 开头C3 是明文的 SM3 摘要固定 32 字节C2 是真正的密文长度等于明文长度。标准推荐顺序是 C1C3C2但早期不少实现输出 C1C2C3——对接老系统时这通常是第一道坎。SM2Engine engine new SM2Engine(SM2Engine.Mode.C1C3C2); engine.init(true, new ParametersWithRandom(pub, new SecureRandom())); byte[] ciphertext engine.processBlock(plainText, 0, plainText.length); SM2Engine decodeEngine new SM2Engine(SM2Engine.Mode.C1C3C2); decodeEngine.init(false, pri); byte[] recovered decodeEngine.processBlock(ciphertext, 0, ciphertext.length);参数说明Mode 只有 C1C3C2 和 C1C2C3 两种取值加解密必须一致init 的第一个布尔值 true 为加密、false 为解密。加密侧必须用 ParametersWithRandom 包住公钥参数和 SecureRandom不带随机源会初始化失败。原始密文比明文多 97 字节这个膨胀是算法本身决定的不是代码写错转 Base64 后膨胀会再加约三分之一。SM2 的适用场景是短的密钥材料和小报文超过 1KB 的数据建议换数字信封随机生成 SM4 密钥加密业务数据再用 SM2 公钥加密这把 SM4 密钥。提示跨系统对接时先确认对方密文是 C1C3C2 还是 C1C2C3。顺序反了解密结果是乱码而不是异常排查起来很费时间。2.4 SM3withSM2 数字签名与验签的最小闭环SM2 签名在标准里的定义是用 SM3 对消息求摘要再用 SM2 私钥对摘要做椭圆曲线签名BC 把这条链路封装成了 JCE 的 SM3withSM2 算法名。用 JCE 风格写的完整闭环如下Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); kpg.initialize(new ECGenParameterSpec(sm2p256v1), new SecureRandom()); KeyPair kp kpg.generateKeyPair(); ECPublicKey pubKey (ECPublicKey) kp.getPublic(); ECPrivateKey priKey (ECPrivateKey) kp.getPrivate(); // 签名 Signature signer Signature.getInstance(SM3withSM2, BC); signer.initSign(priKey, new SecureRandom()); signer.update(message, 0, message.length); byte[] signature signer.sign(); // 验签 Signature verifier Signature.getInstance(SM3withSM2, BC); verifier.initVerify(pubKey); verifier.update(message, 0, message.length); boolean passed verifier.verify(signature);参数说明initSign 的第二个参数必须是加密安全随机数。SM2 签名每一次都要生成一个随机数 kk 如果可预测或被人拿到私钥可以直接被算出来。签名结果是 DER 编码长度在 70 字节左右浮动验签时只需要公钥和原始消息。这里也是 Java 面试题里常被追问的点为什么 SM2 曲线是 256 位安全强度却按 128 位算——椭圆曲线离散对数问题的通用攻击复杂度是 2^(n/2)256 位曲线对应 128 位安全强度和 RSA-2048 在同样的强度分类里。3. SM3 摘要与 SM4 对称加密参数、模式与密钥派生3.1 SM3 与 SM2 的固定搭配SM3 输出 256 位摘要设计目标对标 SHA-256。国密体系里它承担三类工作一是 SM2 签名和 SM2 加密内部自带的哈希上一章 C3 那 32 字节就是 SM3 输出二是报文传输时的完整性校验给对方加一个摘要字段比对三是做 KDF 派生密钥。单独调用时代码很短SM3Digest sm3 new SM3Digest(); sm3.update(data, 0, data.length); byte[] digest new byte[32]; sm3.doFinal(digest, 0); // 转 hex 输出常见实现会是 64 位小写字符串 String hex Hex.toHexString(digest);参数说明update 可以分多次调用适合流式处理大文件doFinal 计算完后 SM3Digest 内部状态自动重置同一个实例可继续复用。SM3 按 512 位分组吸收数据框架上和 SHA-256 一致。要注意 SM2 签名标准里绑定的哈希就是 SM3不要自行换成 SHA-256标准验签端只接受 SM3withSM2 这条链路。3.2 SM4 的密钥长度、IV 与填充CBC 模式的完整代码SM4 是分组密码密钥和分组都是 128 位16 字节共 32 轮迭代设计对标 AES-128。Java 侧通过 BC 的 SM4/CBC/PKCS7Padding 就可以直接使用不需要自己实现轮函数byte[] sm4Key new byte[16]; // 16 字节会话密钥 byte[] iv16 new byte[16]; // 16 字节 IV随机生成 Cipher cipher Cipher.getInstance(SM4/CBC/PKCS7Padding, BC); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, SM4), new IvParameterSpec(iv16)); byte[] ciphertext cipher.doFinal(plainBytes); Cipher decoder Cipher.getInstance(SM4/CBC/PKCS7Padding, BC); decoder.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sm4Key, SM4), new IvParameterSpec(iv16)); byte[] plain decoder.doFinal(ciphertext);参数说明sm4Key 必须是 16 字节少一位都会抛 InvalidKeyExceptioniv16 同样 16 字节。CBC 模式下 IV 不需要保密但要保证随机同一个密钥下绝不能重复用同一个 IV常见做法是每次加密随机生成 IV拼在密文最前面解密时先截取前 16 字节再初始化。PKCS7Padding 和 Java 里常见的 PKCS5Padding 对 16 字节分组是同一套规则写哪个都能跑。需要认证加密时用 SM4/GCM/NoPaddingBC 较新版本支持GCM 下 IV 推荐 12 字节认证标签会自动拼在密文尾部。注意不要图省事用默认的 SM4/ECB。ECB 模式下相同明文块产生相同密文块手机号这类字典值字段加密后直接暴露重复关系生产环境默认禁用。3.3 AES256 和 SM2、SM4 的区别一张表说清选型阶段被问最多的就是 AES256 和国密算法的关系。直接给结论SM2 对标 RSASM4 对标 AES-128AES-256 是国际算法里密钥长度更长的选项对比项SM2SM4AES-256算法类型椭圆曲线非对称对称分组对称分组密钥长度256 位曲线等效 128 位安全强度128 位256 位分组大小不适用128 位128 位适合用途数字签名、数字信封、密钥协商批量数据加密批量数据加密国际对应RSAAES-128AES-256 本身合规属性GM/T 0003 国标GM/T 0004 国标NIST 标准选择逻辑不复杂内部系统没有合规约束时AES-256 完全够用一旦要对接政务、金融、密评的链路必须使用国密算法是硬性要求才需要把非对称换 SM2、对称换 SM4。SM4 密钥长度 128 位在穷举攻击的视角下和 AES-128 同级AES-256 密钥更长但合规场景里没有选择空间。这个对比也是 java 面试八股里高频出现的题目把强度对标和合规驱动两条答清楚就到位了。3.4 用 SM3 KDF 从主密钥派生 SM4 子密钥生产环境不会把 SM4 密钥写死在配置文件里而是由 KMS 或密码机下发主密钥业务侧按场景派生数据密钥。国密标准的 KDF 用 SM3 做底层思路是把共享数据和计数器拼接后循环散列直到凑够目标长度public static byte[] sm3Kdf(byte[] source, int klen) { int rounds (klen 31) / 32; ByteArrayOutputStream out new ByteArrayOutputStream(); for (int i 1; i rounds; i) { SM3Digest sm3 new SM3Digest(); sm3.update(source, 0, source.length); byte[] counter new byte[]{ (byte) (i 24), (byte) (i 16), (byte) (i 8), (byte) i }; sm3.update(counter, 0, 4); byte[] h new byte[32]; sm3.doFinal(h, 0); out.write(h); } return Arrays.copyOf(out.toByteArray(), klen); }参数说明第 i 轮把 4 字节大端计数器和源数据拼在一起做 SM3输出 32 字节循环 rounds 轮后按 klen 截断。klen 传 16 得到 SM4 密钥传 32 可以当 HMAC 或 AES-256 密钥用。实际落地要给每一轮增加用途标识或随机盐例如把 pay-v1 这类 context 拼进 source防止两个业务域派生结果完全一致。这个 KDF 同样覆盖 SM2 密钥交换后的协商密钥处理是国密标准里复用度最高的基础函数。4. 对接证书与数据库CFCA 国密证书解析和库表字段改造4.1 解析 CFCA 签发的 SM2 证书并取公钥验签真实业务里 SM2 公钥很少自己生成更多是从 CA 签发的国密证书里取CFCA 是常见的一家。证书整体仍是 X.509 格式但签名算法 OID 指向 SM3withSM21.2.156.10197.1.501需要 BC 来解析CertificateFactory cf CertificateFactory.getInstance(X.509, BC); X509Certificate cert (X509Certificate) cf.generateCertificate( new FileInputStream(cfca_sm2.cer)); // 用证书公钥验签 Signature sig Signature.getInstance(SM3withSM2, BC); sig.initVerify(cert.getPublicKey()); sig.update(originData); boolean ok sig.verify(signBytes); // 证书链校验当前证书由上级 CA 签发 cert.verify(rootCert.getPublicKey());参数说明cert.getPublicKey() 返回的 ECPublicKey 自带 SM2 曲线参数可以直接初始化验签对象。cert.verify(rootCert.getPublicKey()) 会根据当前证书的签名算法自动选择 SM3withSM2不需要手动指定。容易踩的坑是 CertificateFactory 不指定 Provider 时部分 JDK 版本走默认 SUN 实现遇到 SM2 签名 OID 直接抛 CertificateParsingException把 X.509 后面显式加上 BC 就能绕开。4.2 SM4 加密敏感字段字段加宽计算与密文存储格式数据库字段做国密改造第一个要回答的问题是字段要加宽多少。SM4-CBC 按 16 字节分组带 PKCS7 填充后密文长度 ((明文长度 / 16) 1) * 16。手机号明文字节 11加密后 16身份证 18 个字符 UTF-8 编码是 18 字节加密后 32。存储用 Hex 还要再翻倍16 字节密文变 32 个字符32 字节变 64 个字符。所以改造 DDL 通常是 VARCHAR(11) 改 VARCHAR(64)、VARCHAR(64) 改 VARCHAR(128)如果采用 IV 前置拼接长度还要再加 32 个 hex 字符。我一般建议统一用 Hex 而不是 Base64 落库理由是字符集兼容性好、DBA 能直接核对长度、分库分表中间件不会误处理CHAR/VARCHAR 语义也和原来一致。存储格式约定成 IV 密文 的 hex 串解密时先还原字节数组取前 16 字节做 IV。ALTER TABLE t_user MODIFY COLUMN mobile VARCHAR(64) COMMENT 手机号SM4-CBCIV前置hex存储;值写入交给 Java 侧完成不要在 SQL 里解密。补充一点如果目标库是 OceanBase它的透明加密TDE在存储层也支持国密 SM4开启后磁盘落盘自动加密但业务字段级加密仍然必要两层解决的是不同问题——TDE 防拖库字段加密防查询侧越权。4.3 sm2 如何做数据库国密测试从单字段回环到存量迁移数据库国密测试最容易做错的地方是把字段加密的主力算法选成 SM2。SM2 密文最少比明文多 97 字节相同的明文每次密文都不同字段膨胀和性能都不可接受。字段级数据加密用 SM4SM2 在数据库场景只承担签名、信封等外围能力。测试步骤按下面顺序走单字段回环选一条记录Java 加密后写库再读出来解密与原明文逐字节比对。长度校验遍历全表校验密文 hex 长度不超过新字段长度。批量迁移按主键分批 select 明文加密后批量 update记录失败行和重试队列。查询兼容验证确认加密列无法再走 LIKE 模糊查询原唯一索引改为对密文建索引或加一列明文哈希做匹配。批量迁移的伪代码如下try (PreparedStatement ps conn.prepareStatement( UPDATE t_user SET mobile ? WHERE user_id ?)) { for (User u : batch) { byte[] ct sm4Encrypt(iv, u.getMobile().getBytes(StandardCharsets.UTF_8)); ps.setString(1, Hex.toHexString(ct)); ps.setLong(2, u.getUserId()); ps.addBatch(); } ps.executeBatch(); }参数说明IV 每条记录独立生成不要用固定 IV 跑全表。迁移期间要预留双写窗口——新数据直接写密文历史数据处理完要做一次全量解密比对比对通过才切读流量。加密列上原来的普通索引基本作废这是等保改造里最容易漏评估的一项。注意加密列上的原唯一索引在迁移前就要评估改造方案等切流量后再补索引回滚会变得非常被动。5. 从 Demo 到生产SM2/SM4 模式错位、随机数与压测验证5.1 C1C3C2 与 C1C2C3 模式判断跨系统联调时对方给你的密文如果解不开先别怀疑密钥先看密文结构。Base64 解码后前 3 字节是 04 两个坐标段说明 C1 在前接着判断第 66 到第 97 字节如果这一段和你本地算的 SM3 摘要一致顺序是 C1C3C2如果不一致很可能是 C1C2C3把 32 字节 C3 挪到 C2 之后即可用同一个引擎验证。这个判断在对接加密机和异构语言时能省下半天时间。5.2 用 JMH 快速压测 SM2 与 SM4 的量级算法替换上线前最好压一把。用 JMH 写两个 Benchmark一个打 SM2 签名一个打 SM4-CBC 单块加密先跑 -f 1 -wi 2 -i 3 拿粗略估计Benchmark public byte[] sm2SignBench() throws Exception { // 预热完再调用注意 SM2Signer 每次要重新 init return doSm2Sign(message); } Benchmark public byte[] sm4EncryptBench() throws Exception { return doSm4Encrypt(plainBlock); }量级判断就一条SM4 的吞吐比 SM2 高两个数量级以上SM2 的签名验签在毫秒级。如果压测发现 SM2 验签扛不住 QPS考虑批量验签或在网关层做验签结果缓存SM4 侧性能通常不是瓶颈先检查是否每次请求都重新加载 KeyStore 或重新初始化 Cipher。5.3 随机数质量与签名格式兼容生产代码里禁止用 new Random() 参与 SM2 签名。可预测的随机数意味着可预测的签名随机数 k而 k 一旦被还原私钥可以直接从签名中反推出来。统一用 SecureRandom 的 NativePRNG 或从 KMS 获取随机源。还有一个对端兼容点BC 的 SM3withSM2 验签接收的是 DER 编码签名对端如果给的是裸 r||s 拼接64 字节需要先按 ASN.1 封装或改用 SM2Signer 原生验签反之亦然。签名格式和密文模式是国密联调里两个最容易踩的兼容性坑写工具类时把它们对外暴露成可配置项后面接任何系统都不用改核心代码。本文还有配套的精品资源点击获取