☰
国密算法SM2/SM3/SM4工程化封装:Android端从零落地与避坑指南
2026/10/15 1:24:49 网站建设 项目流程

简介:这份资源是面向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、decrypt;SM3 暴露了hash和hashHex;SM4 暴露了encryptECB、encryptCBC、decryptECB、decryptCBC。所有方法统一抛出一个自定义的CryptoException,把底层各种NoSuchAlgorithmException、InvalidKeyException包一层,调用方不用 catch 一堆异常类型。

密钥和密文的表示格式也做了统一:公钥私钥默认输出 Hex 字符串,密文默认 Base64,签名值默认 Hex。这样跨语言对接时(比如 Android 端加密、服务端 Java 解密)不会因为编码格式对不上而翻车。下面这张表是我整理的各算法关键参数,配置时直接对照改:

算法密钥长度分组/输出长度常用模式默认编码
SM2256 位签名 64 字节无(椭圆曲线)Hex
SM3无密钥摘要 32 字节无Hex
SM4128 位分组 16 字节ECB / CBCBase64

注意:SM4 的 ECB 模式不需要 IV,CBC 模式必须传 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_no=20240101"; 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,什么时候必须上 CBC

SM4 的 ECB 模式实现简单,相同明文块加密结果相同,适合加密短小的、无规律的密钥材料;CBC 模式引入 IV,相同明文每次加密结果不同,适合加密业务数据。封装里两种模式都提供了,但我的建议是:只要数据长度超过一个分组(16 字节),就用 CBC。

// 生成 128 位密钥 String sm4Key = SM4Util.generateKeyHex(); // ECB 加密(无 IV) String ecbCipher = SM4Util.encryptECB("hello gm", sm4Key); // CBC 加密(随机 IV,IV 随密文一起返回) 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 的标准向量是密钥和明文全零时输出681edf34d206965e86b3e94f536e4246,SM2 的签名验签可以用固定私钥和消息验证。

// 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 里,等于白做。

从那以后我每次接入新的国密封装,都强制走一遍「标准向量 → 往返测试 → 跨平台对接验证」三步,少一步都不敢往业务里合。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询