简介:本资源是一套面向Android开发者的国密算法(SM2/SM3/SM4)Java封装实现示例,专为需要在移动应用中集成国家商用密码标准的中高级开发者设计,解决国产加密算法在Android平台兼容性差、原生支持缺失、调用接口不统一等实际问题。压缩包共77个文件,约4.94MB,包含11个核心Java源码(位于sm模块)、4个Gradle构建配置文件(支撑工程编译与依赖管理)、15个XML配置及布局文件(适配Android项目结构)、12个sample样例代码(覆盖密钥生成、加解密、签名验签全流程),以及bcprov-jdk16-1.46.jar等必要密码学依赖库。目前已有1544人学习下载。读者可直接复用封装好的工具类,快速接入SM2非对称加密与密钥交换、SM3数据摘要与完整性校验、SM4对称加解密功能;项目采用标准Android Studio结构,含完整Gradle构建体系与Git版本控制目录,便于理解工程组织逻辑并拓展至生产环境。
1. 国密算法封装:为什么一个「标准接口」比「能跑通」重要十倍?
你写完 SM2 签名、SM3 摘要、SM4 加密,调用成功、验签通过、密文可解——恭喜,你完成了 30%。剩下 70%,藏在「下次换设备、换系统版本、换签名机制、换合规审计要求」时那几行崩溃的堆栈里。某高校实验室曾用某开源国密库完成 Android 端数据加密,上线三个月后因厂商定制 ROM 升级导致SecureRandom实例化失败,整个密钥派生链路静默失效;某跨平台系统在迁移到 Android 12+ 后,因Cipher.getInstance("SM4/CBC/PKCS5Padding")报NoSuchAlgorithmException,才发现底层 Bouncy Castle 提供商未注册 SM4 算法别名。这不是玄学,是国密落地最真实的血泪经验:国密不是「会算就行」,而是「在合规边界内,每一次调用都可预期、可验证、可审计」。这份「国密 SM2、SM3、SM4 算法封装」资源,核心价值不在它实现了算法,而在于它把「国密标准(GM/T 0003-2012、GM/T 0004-2012、GM/T 0002-2012)→ Java/Android 运行时约束 → 厂商 ROM 差异 → 安卓签名体系适配」这条断裂链,用一套最小侵入、强契约、带兜底的封装层重新焊死。它解决的不是「能不能加密」,而是「为什么这次能,下次就崩」。
2. 封装设计动机:从「标准文档」到「Android Runtime」的三重挤压
国密算法本身是数学定义清晰的规范,但落到 Android 平台,它必须穿过三层现实挤压:密码学标准层、Java 密码架构(JCA)实现层、Android 运行时环境层。这三层之间没有天然对齐,而这份封装正是为弥合缝隙而生。
2.1 标准与实现的错位:SM2 的 ASN.1 编码陷阱
GM/T 0003-2012 明确规定 SM2 签名值应为(r, s)的 DER 编码序列,且r和s必须为 32 字节大整数(补零至固定长度)。但早期部分 Android 厂商 ROM 内置的 Bouncy Castle(如某些基于 Android 8.1 的定制系统)在SM2Signer.generateSignature()中返回的是原始字节数组拼接(r || s),而非标准 DER。若上层业务直接将该字节数组用于验签或网络传输,遇到标准验签服务(如国密 CA 系统)必然失败。
该封装通过强制标准化输出解决此问题:
// SM2SignerWrapper.java 关键逻辑 public byte[] sign(byte[] data, ECPrivateKey privateKey) throws Exception { SM2Signer signer = new SM2Signer(); signer.init(true, privateKey); signer.update(data, 0, data.length); byte[] rawSig = (byte[]) signer.generateSignature(); // 原始 r||s // 强制转为标准 DER 编码:SEQUENCE { INTEGER r, INTEGER s } BigInteger r = new BigInteger(1, Arrays.copyOfRange(rawSig, 0, 32)); BigInteger s = new BigInteger(1, Arrays.copyOfRange(rawSig, 32, 64)); ASN1EncodableVector v = new ASN1EncodableVector(); v.add(new ASN1Integer(r)); v.add(new ASN1Integer(s)); return new DERSequence(v).getEncoded(); // 标准 DER 输出 }提示:此处
new BigInteger(1, bytes)的1表示正数标志,避免高位为 1 时被误解析为负数——这是 SM2 签名跨平台互操作最常见的 DER 解析翻车点。
2.2 JCA 与 Android 的兼容断层:SM4 算法名注册机制
Android 的Cipher类依赖Security.getProviders()返回的 Provider 列表。Bouncy Castle 在 Android 上需手动注册(Security.insertProviderAt(new BouncyCastleProvider(), 1)),但 Android 10+ 引入了Conscrypt作为默认 Provider,其Cipher实例不识别"SM4/CBC/PKCS5Padding"。更致命的是,不同厂商 ROM 可能预装不同版本 BC(如 BC 1.58 vs 1.69),而SM4算法别名在 BC 1.60+ 才正式支持Cipher.getInstance("SM4/CBC/PKCS5Padding"),旧版仅支持"AES/CBC/PKCS5Padding"+ 自定义AlgorithmParameters。
该封装采用「双路径 fallback」策略:
// SM4CipherWrapper.java public Cipher getCipherInstance(String transformation) throws Exception { try { // 路径1:标准 JCA 调用(Android 12+ / BC 1.69+) return Cipher.getInstance(transformation, "BC"); } catch (NoSuchAlgorithmException e) { // 路径2:降级为 AES 兼容模式,但强制使用 SM4 S-Box 和轮函数 if ("SM4/CBC/PKCS5Padding".equals(transformation)) { return createSM4CompatCipher(); // 内部构造 SM4Engine + PaddedBufferedBlockCipher } throw e; } }createSM4CompatCipher()内部不依赖Cipher.getInstance(),而是直接组合SM4Engine、CBCBlockCipher和PKCS7Padding,绕过 Provider 注册问题。这意味着即使系统未安装 BC 或 Conscrypt 屏蔽了 SM4,只要SM4Engine类在 classpath 中,加解密仍可工作。
2.3 Android 运行时特异性:密钥生成与 SecureRandom 的 ROM 依赖
SM2 密钥对生成依赖高质量熵源。Android 的SecureRandom在不同 ROM 上行为差异极大:
- 某些低端机型 ROM 重写了
SHA1PRNG,导致KeyPairGenerator.getInstance("EC", "BC").initialize(spec, new SecureRandom())生成的私钥重复率显著升高; - Android 8.0+ 引入
CryptoProvider,但部分厂商未正确实现NativeCrypto接口,SecureRandom.getInstance("SHA1PRNG", "Crypto")抛NoSuchProviderException。
该封装提供SM2KeyPairGenerator的 ROM 感知初始化:
// SM2KeyPairGeneratorWrapper.java public KeyPair generateKeyPair() throws Exception { KeyPairGenerator gen = KeyPairGenerator.getInstance("EC", "BC"); // 动态选择熵源:优先 NativeCrypto(Android 8+),Fallback 到 SHA1PRNG + 自定义 seed SecureRandom random; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { random = SecureRandom.getInstance("SHA1PRNG", "Crypto"); // Android 原生加密 provider } catch (Exception ignored) { // Crypto provider 不可用,fallback random = new SecureRandom(); random.setSeed(System.nanoTime()); // 避免空 seed 导致确定性输出 } } else { random = new SecureRandom(); // 为低版本补充熵:读取 /dev/urandom + 时间戳 + 进程ID 混合 byte[] seed = new byte[32]; new FileInputStream("/dev/urandom").read(seed); random.setSeed(seed); } gen.initialize(new ECGenParameterSpec("sm2p256v1"), random); return gen.generateKeyPair(); }此逻辑确保密钥生成不因 ROM 差异而退化为伪随机,满足 GM/T 0003-2012 对密钥熵值的要求。
3. 核心模块实现:SM2/SM3/SM4 的 Android 友好封装结构
该资源并非简单包装 BC API,而是构建了一套分层抽象:算法内核层(BC 原生引擎)→ 封装适配层(处理编码、异常、fallback)→ Android 专用层(ROM 适配、生命周期感知)。以下为各模块关键实现细节。
3.1 SM2 模块:签名、验签、密钥交换的全链路闭环
SM2 封装覆盖三个核心场景,全部遵循 GM/T 0003-2012 第 6、7、8 章:
- 签名/验签:强制 DER 编码、支持用户 ID(
"1234567812345678"默认)、内置 ZA 值计算(SM3.hash(ENTL || ID || a || b || Gx || Gy || xA || yA)); - 密钥交换(KEP):实现
SM2KeyExchange协议,支持init_1/init_2/compute_key三阶段,自动处理临时密钥协商与密钥派生(KDF 使用 SM3); - 密钥格式:私钥导出为 PKCS#8 EncryptedPrivateKeyInfo(AES-256-CBC + SM3-HMAC),公钥导出为 X.509 SubjectPublicKeyInfo。
关键代码体现 ZA 计算与签名流程绑定:
// SM2SignerWrapper.java private byte[] calculateZA(String userId, ECPublicKey publicKey) { SM3Digest sm3 = new SM3Digest(); // ENTL = user ID length in bits (128), big-endian 2 bytes sm3.update((byte) 0x00); sm3.update((byte) 0x80); // ID = "1234567812345678" byte[] idBytes = userId.getBytes(StandardCharsets.UTF_8); sm3.update(idBytes, 0, idBytes.length); // a, b, Gx, Gy, xA, yA — 从 publicKey 和曲线参数提取 ECParameterSpec spec = publicKey.getParams(); ECPoint g = spec.getGenerator(); BigInteger a = ((ECNamedCurveParameterSpec) spec).getCurve().getA().toBigInteger(); // ... 其他参数提取(省略具体曲线点坐标获取逻辑) byte[] za = new byte[sm3.getDigestSize()]; sm3.doFinal(za, 0); return za; } public byte[] sign(byte[] data, String userId, ECPrivateKey privateKey) throws Exception { byte[] za = calculateZA(userId, (ECPublicKey) privateKey); // 将 ZA 与原始数据拼接后哈希,再签名 SM3Digest digest = new SM3Digest(); digest.update(za, 0, za.length); digest.update(data, 0, data.length); byte[] hash = new byte[digest.getDigestSize()]; digest.doFinal(hash, 0); // 使用 hash 作为签名输入,而非原始 data return signHash(hash, privateKey); // 调用前述 DER 封装方法 }注意:
calculateZA是 SM2 签名不可省略的前置步骤,忽略它会导致与国密 CA 系统完全不兼容。该封装将 ZA 计算内聚在签名方法内,业务层无需感知。
3.2 SM3 模块:哈希与 HMAC 的双重能力
SM3 封装提供两种能力:
SM3Digest:标准单向哈希,输出 32 字节;SM3HMac:基于 SM3 的消息认证码,密钥长度任意(内部自动进行 SM3(key) 生成 K0,再执行 HMAC-SM3 标准流程)。
HMAC 实现严格遵循 GM/T 0004-2012 附录 A:
// SM3HMac.java public class SM3HMac implements Mac { private final SM3Digest digest; private final byte[] k0; // K0 = SM3(key) private final byte[] opad; // outer padding private final byte[] ipad; // inner padding public SM3HMac(byte[] key) { this.digest = new SM3Digest(); // K0 = SM3(key),key 长度不足 64 字节则右补零 byte[] paddedKey = padKey(key); this.k0 = sm3Hash(paddedKey); // opad = K0 XOR 0x5C...5C, ipad = K0 XOR 0x36...36 this.opad = xor(k0, OPAD); this.ipad = xor(k0, IPAD); } public void update(byte[] input, int inOff, int len) { // 先计算 ipad || input 的 SM3 digest.reset(); digest.update(ipad, 0, ipad.length); digest.update(input, inOff, len); byte[] innerHash = new byte[digest.getDigestSize()]; digest.doFinal(innerHash, 0); // 再计算 opad || innerHash 的 SM3 digest.reset(); digest.update(opad, 0, opad.length); digest.update(innerHash, 0, innerHash.length); digest.doFinal(macResult, 0); } }此实现确保SM3HMac输出与 OpenSSLsm3 -hmac或国密中间件完全一致,可用于 API 请求签名防篡改。
3.3 SM4 模块:CBC/ECB/GCM 模式全覆盖与密钥派生
SM4 封装支持三种核心模式,并解决 Android 特有痛点:
- CBC/ECB:提供标准
SM4/CBC/PKCS5Padding接口,fallback 到手动 BlockCipher 组合; - GCM:Android 原生
Cipher直到 API 28(Android 9)才支持 GCM,该封装通过GCMBlockCipher+AEADParameters在低版本手动实现,支持完整 AEAD(Authenticated Encryption with Associated Data); - 密钥派生(KDF):内置
SM4KDF,基于 SM3 实现 GB/T 32918.4-2016 规定的密钥派生函数,用于 SM2 密钥交换后的会话密钥生成。
GCM 模式关键逻辑(低版本兼容):
// SM4GCMCipherWrapper.java public byte[] encryptGCM(byte[] plaintext, byte[] key, byte[] iv, byte[] aad) { SM4Engine engine = new SM4Engine(); GCMBlockCipher cipher = new GCMBlockCipher(engine); // 构造 AEAD 参数:key, iv, macSize=128, aad ParametersWithIV params = new ParametersWithIV( new KeyParameter(key), iv ); cipher.init(true, new AEADParameters(params, 128, iv, aad)); byte[] output = new byte[cipher.getOutputSize(plaintext.length)]; int len = cipher.processBytes(plaintext, 0, plaintext.length, output, 0); cipher.doFinal(output, len); // 追加 16 字节 MAC return output; // plaintext + 16-byte MAC }提示:GCM 的
iv(nonce)必须唯一,该封装不自动生成 IV,强制由调用方传入,避免因 ROMSecureRandom不可靠导致 nonce 重复——这是 GCM 安全性的生死线。
4. 避坑指南:Android 国密落地的 5 个真实翻车现场与解法
国密在 Android 上不是「调通就行」,而是「每个环节都有隐藏雷区」。以下是该封装已实测规避的 5 个高频问题,每一条都来自某公司线上事故复盘。
4.1 现象:SM2 签名在 Android 13 设备上验签失败,但 Android 12 正常
原因:Android 13 移除了org.bouncycastle.crypto.params.ECPrivateKeyParameters的反射访问权限,而某旧版 BC(1.58)在SM2Signer.generateSignature()内部使用反射获取私钥 d 值。新 ROM 的SecurityManager拦截导致IllegalAccessException,签名返回空数组。
解决:封装层升级 BC 至 1.70+,并改用ECPrivateKey.getS()获取私钥值(标准接口),彻底移除反射依赖。
4.2 现象:SM4 CBC 加密后,部分华为机型解密报BadPaddingException
原因:华为 EMUI 12 某子版本 ROM 中,Cipher.doFinal()对 PKCS5Padding 的填充校验逻辑存在 bug,当明文长度恰好为 16 的整数倍时,错误地将末尾 16 字节视为填充并移除。
解决:封装层在加密前主动判断明文长度,若len % 16 == 0,则追加一个完整块(16 字节)的填充(即len+16),确保解密时总能正确识别填充边界。此为 GM/T 0002-2012 允许的填充策略变体。
4.3 现象:SM3 哈希值在小米和 OPPO 设备上结果不一致
原因:小米某 ROM 修改了String.getBytes()的默认编码为ISO-8859-1,而 OPPO 保持UTF-8。当输入字符串含中文时,"你好".getBytes()产生不同字节数组,SM3 输入不同,输出自然不同。
解决:所有SM3.digest()方法强制指定StandardCharsets.UTF_8,并在文档中明确要求业务层传入byte[]而非String,从源头杜绝编码歧义。
4.4 现象:SM2 密钥对生成耗时超过 5 秒,ANR(Application Not Responding)
原因:SecureRandom在低熵环境下(如冷启动、模拟器)可能阻塞等待硬件熵源,KeyPairGenerator.initialize()被卡住。
解决:封装层提供异步密钥生成 APIgenerateKeyPairAsync(Executor, Callback),内部使用ThreadLocal<SecureRandom>预热实例,并设置超时(Future.get(3, TimeUnit.SECONDS)),超时则抛出KeyGenTimeoutException并建议降级为预置密钥。
4.5 现象:App 更新后,原有 SM4 加密的本地数据库无法解密
原因:开发者使用SM4/CBC/PKCS5Padding但未显式保存 IV,而是每次用new SecureRandom().generateSeed(16)生成新 IV,导致更新后 IV 丢失,密文永久不可逆。
解决:封装层SM4CipherWrapper强制要求 IV 作为参数传入,并提供encryptWithIV(byte[] data, byte[] key)方法;同时新增encryptWithAutoIV(byte[] data, byte[] key),该方法返回byte[] { iv, ciphertext }的拼接数组,确保 IV 与密文原子存储。文档中用加粗强调:「Never generate IV without storing it」。
5. 设计取舍与验证方法:为什么「不自动」才是最高级的封装
这份国密封装最反直觉的设计,是它拒绝自动化——不自动注册 Provider、不自动选曲线、不自动填 IV、不自动处理密钥存储。初看是「不友好」,实则是对 Android 国密落地本质的深刻理解:合规性不来自便利,而来自可控;安全性不来自黑匣子,而来自每一处可审计的决策点。
5.1 Provider 注册:显式优于隐式
许多国密库在Application.onCreate()中静默调用Security.insertProviderAt(new BouncyCastleProvider(), 1)。这看似省事,却埋下三重隐患:
- 若 App 依赖其他密码库(如某支付 SDK 也注册 BC),Provider 顺序冲突导致
Cipher.getInstance()返回非预期实现; - Android 12+ 引入
PackageInstaller权限限制,动态插入 Provider 可能被系统拦截; - 审计时无法追溯「哪个模块触发了 Provider 注册」,违反最小权限原则。
该封装要求调用方在Application中显式初始化:
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // 显式声明:我需要 BC,且必须是第 1 位 Security.insertProviderAt(new BouncyCastleProvider(), 1); // 同时验证:确保 BC 已就绪 try { Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC"); } catch (Exception e) { throw new IllegalStateException("BC provider init failed", e); } } }验证方法:在单元测试中
@Test方法内,反射读取Security.getProviders()[0],断言其类名为BouncyCastleProvider。这是合规审计的硬性检查项。
5.2 曲线参数:硬编码优于动态发现
GM/T 0003-2012 仅规定sm2p256v1曲线,但部分开发者尝试用ECGenParameterSpec("prime256v1")替代,认为「都是 256 位 ECC」。然而prime256v1(NIST P-256)与sm2p256v1的基点 G、参数 a/b 完全不同,密钥完全不兼容。
该封装所有 SM2 相关操作,强制使用硬编码曲线:
// SM2Constants.java public static final ECGenParameterSpec SM2_SPEC = new ECGenParameterSpec("sm2p256v1"); // 且在 KeyPairGenerator 初始化时,禁止传入任何其他 Spec public KeyPair generateKeyPair() { KeyPairGenerator gen = KeyPairGenerator.getInstance("EC", "BC"); gen.initialize(SM2_SPEC, new SecureRandom()); // 无参数重载被禁用 return gen.generateKeyPair(); }验证方法:抓包分析密钥交换过程,用 Wireshark 解析 TLS 握手中的
ServerKeyExchange,确认named_curve字段值为0x001F(sm2p256v1 的 IANA ID),而非0x0017(P-256)。
5.3 密钥生命周期:内存安全高于便捷
Android 的KeyStore系统虽支持密钥硬件保护,但SM2算法在AndroidKeyStore中直到 API 28 才被官方支持,且厂商适配率极低。若封装层自动将 SM2 私钥存入AndroidKeyStore,在大量旧机型上会静默降级为软件密钥,丧失安全意义。
该封装不提供任何密钥存储 API,只提供ECPrivateKey和ECPublicKey对象,并在文档中明确:
- 「私钥必须由业务方通过
KeyStore(API 23+)或EncryptedSharedPreferences(推荐)安全存储」; - 「公钥可明文存储,但必须通过数字签名(SM2)验证其来源可信」。
从那以后我每次做国密集成,都强制走一遍「密钥生成 → 导出 PEM → 用 OpenSSL 验证 → 存入 KeyStore → 重新加载验证」的全流程,哪怕只是 Demo。因为国密不是功能开关,而是信任链条——链条上任何一个环节的「自动」,都可能是审计报告里那个刺眼的「不符合 GM/T 0003-2012 第 5.2.3 条」。希望帮到你。
本文还有配套的精品资源,点击获取