国密算法SM2/SM3/SM4工程化封装:Android端从零落地与避坑指南
发布时间:2026/10/15 1:24:52 锦皓数字建站

简介这份资源是面向Android开发者的国密算法封装示例聚焦在Java环境下实现SM2、SM3、SM4三种中国国家密码算法帮助开发者解决Android基础库不直接支持国密、需要自行引入JNI或第三方库的兼容性与性能问题。压缩包共77个文件约4.94MB以xml配置、java源码、gradle构建脚本、jar依赖库及png资源为主其中sm模块集中承载算法实现代码gradle相关文件负责项目构建与依赖管理jar包提供底层密码学支持。目前已有1543人学习下载。资源将SM2非对称加密与签名、SM3散列校验、SM4对称块加密封装为简洁接口调用只需几行代码并附带完整的Android工程结构与构建配置便于开发者直接集成到应用中为本地存储或网络传输的数据隐私与完整性提供更高级别的安全保障适合需要快速落地国密方案的中高级Android开发者参考。1. 国密算法封装从零跑通 SM2/SM3/SM4 的工程化落地很多做 Android 端安全模块的同行第一次接触国密算法时都会卡在同一个地方算法标准文档看得懂但真到工程里调用发现要么是 Java 层 BouncyCastle 的 API 晦涩难记要么是 JNI 层 OpenSSL 的国密分支编译不过最后项目里散落着一堆复制粘贴的加解密代码换个项目就得重写一遍。这份「国密 SM2、SM3、SM4 算法封装」资源解决的就是这个断层——它把三种算法的密钥生成、加解密、签名验签、摘要计算统一收口成一套可复用的接口Android 端和纯 Java 环境都能直接接入。适合正在做金融、政务、物联网设备认证相关模块的开发者尤其是需要在 Android 上落地国密改造、又不想被底层库编译折腾的人。下面我按实际拆包和复现的顺序把选型理由、调用方式、参数边界和踩过的坑一次讲清。2. 三种算法各管什么SM2/SM3/SM4 的职责划分与选型依据2.1 为什么不能只用一种算法打天下国密算法不是「一个算法走天下」的设计SM2、SM3、SM4 各自对应不同的密码学职能混用或者错用会直接导致安全强度不达标。SM2 是基于椭圆曲线推荐曲线参数 sm2p256v1的非对称算法承担密钥交换、数字签名和公钥加密SM3 是密码杂凑算法输出 256 位摘要用于完整性校验和签名过程中的消息摘要SM4 是对称分组密码分组长度 128 位密钥长度 128 位负责大批量数据的加解密。常见做法是用 SM2 做身份认证和密钥协商协商出的会话密钥交给 SM4 加密业务数据SM3 则穿插在签名和校验环节。这个组合的逻辑是——非对称算法安全但慢对称算法快但密钥分发难杂凑算法保证数据没被篡改。封装资源把这三种能力放在同一套 API 风格下调用时不用在 BouncyCastle、OpenSSL、自研实现之间来回切换。选型上还有一个现实问题Android 系统自带的 Provider 对国密支持参差不齐。低版本 Android 根本没有 SM 系列实现高版本虽然部分厂商 ROM 内置了但 API 不统一。所以这份封装的价值在于「自带实现、不依赖系统 Provider」保证从 Android 7 到 Android 14 行为一致。2.2 封装层的接口设计思路拆开这份资源核心是一组工具类加配置类。SM2 部分暴露了generateKeyPair、sign、verify、encrypt、decryptSM3 暴露了hash和hashHexSM4 暴露了encryptECB、encryptCBC、decryptECB、decryptCBC。所有方法统一抛出一个自定义的CryptoException把底层各种NoSuchAlgorithmException、InvalidKeyException包一层调用方不用 catch 一堆异常类型。密钥和密文的表示格式也做了统一公钥私钥默认输出 Hex 字符串密文默认 Base64签名值默认 Hex。这样跨语言对接时比如 Android 端加密、服务端 Java 解密不会因为编码格式对不上而翻车。下面这张表是我整理的各算法关键参数配置时直接对照改算法密钥长度分组/输出长度常用模式默认编码SM2256 位签名 64 字节无椭圆曲线HexSM3无密钥摘要 32 字节无HexSM4128 位分组 16 字节ECB / CBCBase64注意SM4 的 ECB 模式不需要 IVCBC 模式必须传 16 字节 IV且 IV 每次加密建议随机生成不要硬编码在代码里。2.3 依赖引入与最小可运行环境资源本身不强制绑定构建工具但 Android 项目里我一般用 Gradle 引入。如果你拿到的是源码包直接把sm包拷进src/main/java下即可如果是 aar走本地依赖。下面是最小接入步骤// app/build.gradle 中确认 Java 版本不低于 1.8 android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { // 若资源以 aar 形式提供放入 libs 后引入 implementation files(libs/gm-crypto-release.aar) }逻辑说明国密实现里用到了java.security和部分javax.crypto接口Java 8 是底线。参数上sourceCompatibility和targetCompatibility必须一致否则编译期可能出现NoClassDefFoundError。如果资源是纯源码跳过 dependencies 那行直接拷贝包路径。环境确认完之后先跑一个 SM3 摘要做冒烟测试因为 SM3 不涉及密钥最容易验证封装是否正常工作// 冒烟测试对固定字符串做 SM3 摘要 String input abc; String digest SM3Util.hashHex(input.getBytes(StandardCharsets.UTF_8)); // 预期输出66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0 System.out.println(digest);逻辑说明hashHex内部先做字节数组摘要再转十六进制省去手动转换。参数上传入的字节数组编码必须和对方约定一致UTF-8 是通用选择。如果输出和预期值对不上先检查输入字符串有没有多余空格再检查封装里的 SM3 实现是否被混淆规则改坏。3. SM2 签名验签与加解密曲线参数、ID 值和 C1C3C2 顺序3.1 SM2 签名的完整调用链SM2 签名和验签是国密改造里最容易出问题的环节因为标准里有个「用户标识 ID」的概念默认值是1234567812345678但很多实现要求调用方显式传入。封装资源把 ID 作为可选参数不传就用默认值。签名流程是私钥 消息 ID → 签名值r 和 s 拼接验签是公钥 消息 签名值 ID → true/false。// 生成密钥对 KeyPair pair SM2Util.generateKeyPair(); String privateKeyHex SM2Util.getPrivateKeyHex(pair); String publicKeyHex SM2Util.getPublicKeyHex(pair); // 签名 String message order_no20240101; byte[] sign SM2Util.sign(message.getBytes(StandardCharsets.UTF_8), privateKeyHex); String signHex HexUtil.encode(sign); // 验签 boolean ok SM2Util.verify(message.getBytes(StandardCharsets.UTF_8), signHex, publicKeyHex); System.out.println(verify result: ok);逻辑说明sign方法内部先对消息做 SM3 摘要再用私钥对摘要签名所以外部不需要再手动调 SM3。参数上privateKeyHex必须是 64 位十六进制字符串32 字节publicKeyHex有两种格式——带04前缀的未压缩格式130 位和不带前缀的128 位封装默认输出带前缀格式验签时内部会自动兼容。如果验签失败第一个要查的就是双方用的 ID 是否一致。3.2 加解密中的 C1C3C2 与 C1C2C3 顺序坑SM2 加密后的密文结构是 C1椭圆曲线点 C3摘要 C2密文数据但早期一些实现用的是 C1C2C3 顺序导致跨系统对接时解密失败。这份封装默认按国标 GM/T 0009-2012 的 C1C3C2 顺序输出同时提供了一个开关可以切换。// 加密默认 C1C3C2 顺序 String plain 6222020200112233445; String cipher SM2Util.encrypt(plain.getBytes(StandardCharsets.UTF_8), publicKeyHex); // 解密 byte[] decrypted SM2Util.decrypt(cipher, privateKeyHex); System.out.println(new String(decrypted, StandardCharsets.UTF_8)); // 若对方系统要求 C1C2C3构造时切换 SM2Util.setCipherOrder(CipherOrder.C1C2C3);逻辑说明encrypt返回 Base64 字符串内部已经把 C1、C3、C2 按顺序拼接。参数上setCipherOrder是全局静态设置建议在 Application 初始化时统一设定不要在业务代码里反复切换。解密失败时除了检查密钥还要确认密文是不是被 Base64 解码过一次——有些网络传输层会自动做一次编码到了解密层再解一次就成乱码了。3.3 密钥格式转换与跨平台对接Android 端生成的密钥经常需要给服务端用服务端可能是 Java、Go 或 C密钥格式不统一是常态。封装提供了 Hex、Base64、PEM 三种输出以及从 PKCS#8 私钥和 X.509 公钥导入的方法。// 导出 PEM 格式私钥 String pemPrivate SM2Util.privateKeyToPem(privateKeyHex); // 从 PEM 导入 String importedHex SM2Util.pemToPrivateKeyHex(pemPrivate);逻辑说明PEM 格式带-----BEGIN PRIVATE KEY-----头尾方便直接存文件或配置中心。参数上导入时如果 PEM 里带换行符封装内部会先做清洗不用手动去换行。跨平台对接时建议统一用未压缩公钥带04前缀 C1C3C2 密文顺序这两个组合的兼容性最好。4. SM4 对称加密ECB/CBC 模式选择、填充方式与 IV 管理4.1 模式选择什么时候用 ECB什么时候必须上 CBCSM4 的 ECB 模式实现简单相同明文块加密结果相同适合加密短小的、无规律的密钥材料CBC 模式引入 IV相同明文每次加密结果不同适合加密业务数据。封装里两种模式都提供了但我的建议是只要数据长度超过一个分组16 字节就用 CBC。// 生成 128 位密钥 String sm4Key SM4Util.generateKeyHex(); // ECB 加密无 IV String ecbCipher SM4Util.encryptECB(hello gm, sm4Key); // CBC 加密随机 IVIV 随密文一起返回 String cbcCipher SM4Util.encryptCBC(hello gm, sm4Key); // cbcCipher 格式为 Base64(IV 密文)逻辑说明encryptCBC内部自动生成 16 字节随机 IV并把 IV 拼在密文前面一起 Base64 输出解密时自动拆分。参数上sm4Key必须是 32 位十六进制字符串。ECB 模式不需要 IV但相同明文输出相同密文如果业务数据有规律会泄露信息模式所以只建议用于加密密钥本身。4.2 填充方式PKCS5Padding 与 NoPadding 的边界Java 的 SM4 实现通常用PKCS5Padding但 PKCS5 是 8 字节块的设计用在 16 字节块的 SM4 上实际是 PKCS7。封装默认用 PKCS5Padding同时支持 NoPadding 让调用方自己处理填充。// 默认 PKCS5Padding String c1 SM4Util.encryptCBC(data, sm4Key); // NoPadding 模式明文必须是 16 字节整数倍 SM4Util.setPadding(Padding.NO_PADDING); String c2 SM4Util.encryptCBC(1234567890abcdef, sm4Key);逻辑说明切换到 NoPadding 后如果明文长度不是 16 的整数倍底层会直接抛异常这是预期行为。参数上setPadding同样是全局设置和setCipherOrder一样建议在初始化阶段定好。实际项目里我一般保持默认 PKCS5Padding除非对接方明确要求 NoPadding。4.3 IV 的生成、传输与存储CBC 模式下 IV 不需要保密但必须不可预测。封装用SecureRandom生成 IV每次加密都不同。IV 随密文一起传输是标准做法但要注意IV 不能重复使用同一个密钥加密多条消息否则会削弱安全性。// 手动指定 IV仅用于对接固定 IV 的老系统 byte[] iv HexUtil.decode(00000000000000000000000000000000); String cipher SM4Util.encryptCBCWithIv(data, sm4Key, iv);逻辑说明encryptCBCWithIv用于兼容那些 IV 写死的旧系统。参数上 IV 必须是 16 字节传错长度会抛IllegalArgumentException。如果对接方给的是全零 IV说明对方系统可能比较老这时候要评估是否需要在传输层额外加保护。5. 避坑与排查国密封装在 Android 上的五类翻车现场5.1 混淆后加解密结果对不上现象Debug 包一切正常Release 包签名验签失败或解密报错。原因是 ProGuard 把封装里的反射调用或 JNI 方法名混淆了。解决在proguard-rules.pro里保留国密相关类和方法。-keep class com.example.gm.** { *; } -keepclassmembers class com.example.gm.** { *; } -dontwarn com.example.gm.**5.2 Android 低版本上SecureRandom阻塞现象在 Android 4.x 到 5.x 设备上首次调用密钥生成时卡住几秒甚至十几秒。原因是旧版本 Android 的SecureRandom初始化依赖/dev/random熵池。解决封装里已经改用SHA1PRNG并显式播种如果还有问题在 Application 启动时预热一次。// 预热 SecureRandom避免首次调用阻塞 new SecureRandom().nextInt();5.3 SM2 加密长数据失败现象加密超过一定长度的字符串时抛异常或解密后数据截断。原因是 SM2 作为非对称算法单次加密数据长度受曲线参数限制sm2p256v1 下一般不超过 100 多字节。解决长数据用 SM4 加密SM2 只用来加密 SM4 的密钥这就是数字信封的思路。5.4 服务端解密报InvalidCipherTextException现象Android 端加密的密文传到服务端服务端解密失败。原因通常是密文顺序C1C3C2 vs C1C2C3或编码格式Base64 vs Hex不一致。解决双方先对齐密文顺序和编码再用同一组测试向量验证。封装里提供了SM2Util.getTestVector()可以输出标准测试数据。5.5 SM3 摘要结果和在线工具不一致现象本地算出的 SM3 值和某些在线工具对不上。原因是输入编码不同——在线工具可能按 GBK 或 Latin-1 处理而代码里用的是 UTF-8。解决统一用 UTF-8并且确认输入字符串没有隐藏的 BOM 头或换行符。封装里的hashHex接受字节数组编码由调用方控制这样最不容易出歧义。6. 进阶技巧用测试向量验证封装正确性封装类库最怕的就是「看起来能跑实际算错了」。国密算法有标准测试向量我习惯在接入完成后先跑一遍向量验证确认实现和国标一致。SM3 的标准向量是abc对应66c7f0f4...SM4 的标准向量是密钥和明文全零时输出681edf34d206965e86b3e94f536e4246SM2 的签名验签可以用固定私钥和消息验证。// SM4 标准测试向量验证 String key 0123456789abcdeffedcba9876543210; String plain 0123456789abcdeffedcba9876543210; String expected 681edf34d206965e86b3e94f536e4246; String actual SM4Util.encryptECBNoPad(HexUtil.decode(plain), key); System.out.println(expected.equalsIgnoreCase(actual) ? SM4 vector PASS : SM4 vector FAIL);逻辑说明encryptECBNoPad是不做填充的 ECB 加密专门用于向量验证。参数上密钥和明文都必须是 32 位十六进制字符串。如果输出和预期不一致说明底层实现有问题不要继续往上叠业务逻辑。除了标准向量我还会做一轮「往返测试」随机生成 100 组数据加密再解密比对原文。这个测试能覆盖填充、编码、IV 拼接等环节的边界问题。// 往返测试随机数据加解密一致性 for (int i 0; i 100; i) { String data RandomStringUtils.randomAlphanumeric(i * 3 1); String cipher SM4Util.encryptCBC(data, sm4Key); String back SM4Util.decryptCBC(cipher, sm4Key); if (!data.equals(back)) { throw new AssertionError(round trip failed at length data.length()); } }逻辑说明循环长度从 1 到 298 字节覆盖了不足一个分组、刚好一个分组、跨多个分组的情况。参数上RandomStringUtils可以用任意随机字符串工具替代。这个测试跑通之后基本可以确认封装在常规数据长度下没有问题。还有一个容易被忽略的点密钥的存储。封装只负责算法不负责密钥管理。Android 上建议把 SM4 密钥放到 Android Keystore 里SM2 私钥至少用 EncryptedSharedPreferences 存不要明文写在代码或 SharedPreferences 里。我见过太多项目算法用得没问题密钥直接硬编码在 BuildConfig 里等于白做。从那以后我每次接入新的国密封装都强制走一遍「标准向量 → 往返测试 → 跨平台对接验证」三步少一步都不敢往业务里合。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。