国密SM2/SM3文件加解密实战:从编码到jar交付的避坑指南 简介这份资源面向Java后端开发、安全合规与国密改造相关技术人员聚焦国密算法在实际业务中的落地问题。包内提供基于SM2与SM3的完整实现既能对普通字符串进行加密解密也能直接处理文件覆盖密钥生成、签名验签、摘要计算等常见场景适合需要将国密能力接入现有Java项目的开发者参考。资源共5个文件以4个Java源码文件与1个jar依赖包为主源码按工具类拆分便于按需调用与二次封装jar包为运行所需加密库下载后无需额外配置即可编译使用压缩包整体约4.89MB。目前已有427人学习下载。借助其中的工具类与依赖读者可快速理解国密加解密的调用方式掌握字符串与文件两类数据的处理差异并在此基础上完成接口加密、敏感数据保护等改造减少自行摸索算法细节与依赖冲突的时间成本。1. 国密 SM2/SM3 文件加解密为什么你的 jar 包一换机器就解不开做过政务、金融或国企对接的开发者大概率都遇到过这样的需求一份字符文件要在两端之间传输对方明确要求用国密算法SM2 做非对称加解密、SM3 做摘要校验最后交付物里还得带一个能直接跑的 jar 包。听起来不难但真正动手时坑往往不在算法本身而在编码、填充模式、密钥格式和 jar 依赖这几件事上。我见过太多案例本地跑得好好的换一台机器就报解密失败或者中文文件解出来全是乱码。这篇笔记就围绕「SM2/SM3 国密字符文件加解密 jar 包交付」这个完整链路把选型理由、代码实现、参数设置和踩坑记录一次讲透。适合正在做国密改造的后端工程师、需要交付独立工具包的开发者以及被 BouncyCastle 依赖版本折腾过的人。2. 先把 SM2 和 SM3 的分工理清楚谁加密、谁校验、谁不该混用2.1 SM2 是非对称加密SM3 是摘要算法职责完全不同很多人第一次接触国密会把 SM2 和 SM3 当成「两个可以互相替代的加密算法」这是最常见的认知偏差。SM2 是基于椭圆曲线的非对称算法有公钥和私钥能做加密解密也能做签名验签SM3 是密码杂凑算法输出 256 位摘要本质上是不可逆的只能用来做完整性校验不能用来还原原文。所以在一个文件加解密场景里合理的分工是用 SM2 的公钥加密文件内容或者加密一个对称密钥用 SM3 对原文或密文做摘要接收方解密后再算一次 SM3比对摘要是否一致确认文件没被篡改。如果你拿 SM3 去「解密」那方向就错了它根本没有解密这个概念。常见做法是小文件直接用 SM2 加密整个内容大文件先用 SM2 加密一个随机对称密钥再用对称算法加密文件体SM3 负责校验。标题里说的是「字符文件」通常体积不大直接 SM2 加密内容就够了但摘要校验这一步不能省。2.2 为什么字符文件比二进制文件更容易翻车字符文件的核心问题是编码。一个 UTF-8 的中文文件读成字节数组再加密解密后如果按 GBK 去还原必然乱码。更隐蔽的是换行符Windows 下是\r\nLinux 下是\n如果加解密两端对换行做了不同处理SM3 摘要就会对不上但你会以为是密钥错了。我的习惯是加解密全程只操作byte[]绝不在中间环节做new String()转换。只有最后输出文件时才按明确的字符集写回。这样能避开绝大多数编码玄学。2.3 依赖选型BouncyCastle 是绕不开的但版本要锁死Java 原生 JCE 对国密支持不完整实际项目里基本都靠 BouncyCastle。关键点在于BC 的bcprov-jdk15on和bcprov-jdk18on在国密 API 上有差异不同小版本对 SM2 的默认参数处理也不完全一致。如果你交付的 jar 包里嵌了一个版本对方环境里又有一个旧版本类加载顺序一乱就会出现「本地能跑、对方报 NoSuchAlgorithmException」。我一般会在 pom 里显式锁定一个版本并且用 shade 插件把依赖打进 jar避免运行时被外部 classpath 污染。下面是一个最小可用的依赖配置dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.78.1/version !-- 锁定版本避免运行时被外部旧版覆盖 -- /dependency参数说明bcprov-jdk18on适配 JDK 1.8 及以上1.78.1 对 SM2/SM3 的 API 比较稳定。不要用provided作用域否则打出来的 jar 在对方机器上会缺类。3. 用 Java 跑通 SM2 加解密的最小实现密钥、编码、填充三个参数3.1 生成 SM2 密钥对并正确保存SM2 的密钥对生成不复杂但保存格式有讲究。公钥是 64 字节X 和 Y 各 32 字节通常前面加一个04标识未压缩点私钥是 32 字节的大整数。如果你直接把getEncoded()的结果 Base64 存下来换一个库去解析可能就对不上。import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.*; import java.util.Base64; public class Sm2KeyGen { public static void main(String[] args) throws Exception { Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); // 指定 SM2 曲线不能省略否则默认曲线不是国密 kpg.initialize(new org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec(sm2p256v1)); KeyPair kp kpg.generateKeyPair(); String pub Base64.getEncoder().encodeToString(kp.getPublic().getEncoded()); String pri Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded()); System.out.println(PUB pub); System.out.println(PRI pri); } }逻辑说明这里用的是标准 JCE 接口配合 BC Providersm2p256v1是国密推荐的椭圆曲线参数。参数说明ECNamedCurveGenParameterSpec必须显式指定曲线名如果只写kpg.initialize(256)生成的可能是 secp256r1那就不是 SM2 了加密出来的东西对方解不开。3.2 SM2 加密C1C3C2 和 C1C2C3 的顺序坑SM2 加密结果由三部分组成C1 是椭圆曲线点C3 是摘要C2 是密文。历史上存在两种排列顺序旧标准用 C1C2C3新标准用 C1C3C2。BC 库默认输出 C1C3C2但很多老系统对接时要求 C1C2C3。如果你不确认对方要哪种解密必然失败。import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; public class Sm2Encrypt { public static byte[] encrypt(byte[] data, BCECPublicKey publicKey) { ECPublicKeyParameters param new ECPublicKeyParameters( publicKey.getQ(), // 公钥点 publicKey.getParameters()); SM2Engine engine new SM2Engine(SM2Engine.Mode.C1C3C2); // 显式指定顺序 engine.init(true, new ParametersWithRandom(param, new SecureRandom())); try { return engine.processBlock(data, 0, data.length); } catch (Exception e) { throw new RuntimeException(SM2 加密失败, e); } } }逻辑说明SM2Engine.Mode.C1C3C2是显式声明输出顺序不要依赖默认值。参数说明ParametersWithRandom保证每次加密的随机数不同同一个明文两次加密结果不一样这是正常且必要的。如果对方系统只认 C1C2C3把 Mode 改成C1C2C3即可但两端必须一致。3.3 SM3 摘要加盐和编码要提前约定SM3 本身很简单但「加不加盐」「盐怎么拼」是接口约定问题。我一般把盐拼在原文前面并且盐也用 UTF-8 字节避免字符串拼接时的编码差异。import org.bouncycastle.crypto.digests.SM3Digest; public class Sm3Util { public static byte[] digest(byte[] data, String salt) { SM3Digest digest new SM3Digest(); if (salt ! null) { byte[] saltBytes salt.getBytes(java.nio.charset.StandardCharsets.UTF_8); digest.update(saltBytes, 0, saltBytes.length); // 盐在前 } digest.update(data, 0, data.length); byte[] out new byte[32]; // SM3 固定 256 位 digest.doFinal(out, 0); return out; } }逻辑说明SM3Digest是流式接口update 的顺序决定最终摘要。参数说明输出固定 32 字节转十六进制字符串时用%02x补齐不要用Integer.toHexString否则前导零会丢导致两端摘要字符串长度不一致。3.4 文件读写全程 byte[]最后一步才转字符串import java.nio.file.*; public class FileCrypto { public static byte[] readFile(String path) throws Exception { return Files.readAllBytes(Paths.get(path)); // 直接读字节不做字符转换 } public static void writeFile(String path, byte[] data) throws Exception { Files.write(Paths.get(path), data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } }逻辑说明Files.readAllBytes和Files.write是二进制安全的不会插入任何平台相关的换行转换。参数说明TRUNCATE_EXISTING保证覆盖写避免旧内容残留导致文件变长。如果输出的是 Base64 文本再单独做一次编码不要和文件读写混在一起。4. 把依赖和代码打成可交付 jarshade 配置与入口类设计4.1 为什么普通 jar 到对方机器上会缺类mvn package默认打出来的 jar 只包含你自己的 classBouncyCastle 是外部依赖不会进去。对方如果没配 classpath一运行就NoClassDefFoundError。解决办法是用 maven-shade-plugin 打成 fat jar把所有依赖合并进去。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration createDependencyReducedPomfalse/createDependencyReducedPom transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.crypto.Main/mainClass /transformer /transformers filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude /excludes /filter /filters /configuration /execution /executions /plugin逻辑说明ManifestResourceTransformer指定入口类filters排除签名文件否则合并后的 jar 会因为签名不匹配报SecurityException。参数说明createDependencyReducedPom设为 false 可以避免生成多余的 pom 文件。4.2 入口类设计命令行参数怎么定交付给别人的 jar最好用命令行参数驱动而不是改代码。我一般设计成四个参数模式enc/dec、输入文件、输出文件、密钥文件。public class Main { public static void main(String[] args) throws Exception { // args[0]enc/dec, args[1]输入, args[2]输出, args[3]密钥 String mode args[0]; String in args[1]; String out args[2]; String keyFile args[3]; if (enc.equals(mode)) { // 读取公钥加密写 SM3 摘要到同名 .sm3 文件 } else if (dec.equals(mode)) { // 读取私钥解密校验 SM3 } else { System.out.println(用法: java -jar crypto.jar enc|dec 输入 输出 密钥文件); } } }逻辑说明参数顺序固定方便写脚本调用。参数说明密钥文件里存 Base64 字符串公钥和私钥分开文件不要混在一个文件里靠行号区分容易读错。4.3 运行验证一条命令确认 jar 可用java -jar crypto.jar enc plain.txt plain.enc pub.key java -jar crypto.jar dec plain.enc plain.dec pri.key diff plain.txt plain.dec echo 一致逻辑说明加密后再解密用 diff 比对原文和还原结果。参数说明如果 diff 无输出说明完全一致如果有差异先看是不是换行符问题再看 SM3 摘要是否匹配。5. 避坑与排查国密文件加解密最常见的 5 个翻车现场5.1 解密报 InvalidCipherTextException现象加密正常解密时抛InvalidCipherTextException或mac check failed。原因密文的 C1C3C2 顺序和对方不一致或者密文在传输中被 Base64 解码后多了换行。解决先确认两端 Mode 一致再把密文用 Base64 编码后传输解码时用Base64.getMimeDecoder()容忍换行或者干脆去掉所有空白字符再解码。5.2 中文解密后乱码现象英文正常中文变问号或方块。原因加密前用String.getBytes()没指定字符集走了平台默认编码。解决全程StandardCharsets.UTF_8读写文件都用字节流最后展示时才转字符串。5.3 SM3 摘要对不上但内容看起来一样现象解密出来的文本肉眼看着一样SM3 比对失败。原因换行符差异Windows 的\r\n和 Linux 的\n在字节层面不同。解决在摘要计算前统一换行符或者约定传输时用二进制模式禁止 FTP 的 ASCII 模式。5.4 换机器后 NoSuchAlgorithmException: EC现象本地正常对方环境报找不到 EC 算法。原因对方 JRE 没装 BC Provider或者 jar 里的 BC 版本和对方 classpath 里的冲突。解决用 fat jar 把 BC 打进去并在代码里显式Security.addProvider(new BouncyCastleProvider())不要依赖外部配置。5.5 大文件加密内存溢出现象加密几十兆的文件时 OOM。原因SM2 加密本身不适合大文件一次性读入内存再加密堆撑不住。解决超过 1MB 的文件改用混合加密SM2 只加密一个随机 AES 密钥文件体用 AES 流式处理SM3 分块更新摘要。6. 进阶技巧用混合加密和分块摘要处理大文件前面说的都是小字符文件但实际项目里迟早会遇到大文件。SM2 直接加密大文件有两个硬伤一是性能差椭圆曲线运算比对称加密慢几个数量级二是内存占用高processBlock需要完整数据。我的做法是混合加密随机生成一个 16 字节的 AES 密钥用 SM2 公钥加密这个密钥文件体用 AES-CTR 模式流式加解密SM3 分块更新。// 混合加密核心逻辑示意 byte[] aesKey new byte[16]; new SecureRandom().nextBytes(aesKey); byte[] encryptedKey Sm2Encrypt.encrypt(aesKey, publicKey); // SM2 只加密密钥 // 文件体用 AES-CTR 流式处理SM3 同步 update SM3Digest digest new SM3Digest(); try (InputStream in Files.newInputStream(Paths.get(src)); OutputStream out Files.newOutputStream(Paths.get(dst))) { byte[] buf new byte[8192]; int len; while ((len in.read(buf)) 0) { digest.update(buf, 0, len); // 分块更新摘要 byte[] enc aesCtr(buf, 0, len, aesKey); out.write(enc); } }逻辑说明SM2 只负责保护 AES 密钥文件体走对称加密性能提升明显。参数说明AES-CTR 不需要填充适合流式缓冲区 8192 字节是经验值太小会增加 IO 次数太大收益递减。SM3 分块 update 的结果和一次性 update 完全一致不用担心摘要对不上。验证方法加密后记录 SM3 摘要解密后重新计算比对一致才算成功。我一般会在输出文件旁边生成一个.sm3文件存十六进制摘要方便对方校验。最后说个血泪经验国密对接最怕的不是算法难而是「约定不清」。C1C3C2 还是 C1C2C3、盐拼前面还是后面、换行怎么处理这些必须在动手前和对方白纸黑字确认否则调三天都调不通。我现在养成的习惯是先写一个最小 demo 和对方互发一组测试向量确认无误再写正式代码。希望帮到你。本文还有配套的精品资源点击获取