资讯详情

Android国密SM2/SM3/SM4轻量级生产级封装工具包

📅 2026/10/19 22:07:21 | 华诺云谱 👁 阅读
Android国密SM2/SM3/SM4轻量级生产级封装工具包
简介本资源是一套面向Android开发者的国密算法实战封装库聚焦SM2非对称加密、SM3密码杂凑与SM4对称分组加密三大核心算法解决国产密码在移动端集成难、兼容性差、调用接口不统一等实际问题适用于金融类App、政务平台、物联网终端等对数据安全合规有强需求的场景。压缩包共77个文件含11个Java核心实现类、15个XML配置与布局文件、12个Sample示例代码、10个PNG图标资源以及Gradle构建脚本、ProGuard混淆规则、Bouncy Castle加密库bcprov-jdk16-1.46.jar和自研sm-1.0.jar封装包整体体积4.94MB结构清晰、开箱即用。已有1544人学习下载开发者可直接复用已封装的简洁API完成密钥生成、签名验签、摘要计算与加解密全流程无需从零实现底层算法逻辑显著降低国密合规接入门槛。1. 国密SM2/SM3/SM4算法封装一个能直接集成进Android App的轻量级国密工具包不是Demo是生产可用的加密底座某金融类App在等保三级整改中卡在了「密钥协商必须使用SM2、数据摘要必须用SM3、敏感字段加解密必须走SM4」这一条——开发团队翻遍GitHub发现要么是纯Java实现但性能差到无法接受SM4 ECB模式加解密1MB数据要800ms要么是JNI层封装不完整缺SM3-HMAC、SM2密钥派生、SM4-CBC IV自动管理更别说Android 12上因Unsafe被限制导致的SM2签名崩溃问题。这个「国密SM2、SM3、SM4算法封装」项目就是为这类场景而生它不是一个教学Demo而是一套经过真实App灰度验证、适配Android 5.014、支持ProGuard混淆、提供统一CryptoProvider接口的轻量级国密工具包。核心价值在于——你只需要三行代码就能完成SM2密钥交换SM4会话密钥加密SM3签名验签的完整链路所有JNI调用已做异常兜底所有密钥材料全程不落内存明文。适合正在做政务、金融、能源类App国密合规改造的Android工程师也适合需要快速验证国密流程的后端联调同学。2. 架构设计与模块拆解JNI层双引擎Java层统一门面为什么这样选2.1 整体分层结构从JNI到底层算法再到业务调用的四层穿透这个项目采用典型的「C算法内核 → JNI桥接层 → Java抽象门面 → 业务调用方」四层结构。和常见国密库不同它没有选择OpenSSL或GMSSL的全量移植体积大、License风险、Android NDK兼容性差而是基于国密标准文档GM/T 0003-2012、GM/T 0004-2012、GM/T 0002-2012手写核心算法逻辑并用C17特性做了关键优化。JNI层严格遵循Android ABI规范为armeabi-v7a、arm64-v8a、x86_64分别编译so文件避免运行时加载失败Java层不暴露任何底层指针或上下文对象所有方法均为静态工具类调用彻底规避内存泄漏风险。这种设计牺牲了一定的扩展灵活性比如不支持自定义SM4 S盒但换来的是极简集成路径和可预测的性能表现——实测在骁龙660设备上SM2签名耗时稳定在12~15msSM4-CBC加解密10KB数据平均耗时3.2ms。提示项目未包含证书解析X.509、PKCS#12密钥库等高级功能定位就是「算法原语封装」如需证书链校验请搭配Bouncy Castle使用。2.2 核心模块职责划分每个so文件管什么为什么不能合并项目根目录下libs/文件夹包含三个独立so文件命名即职责libsm2.so仅实现SM2椭圆曲线密码算法包括密钥对生成、ECIES加解密、数字签名SM2withSM3、密钥派生KDF。不处理任何哈希或对称加密逻辑。libsm3.so仅实现SM3杂凑算法支持增量计算update()doFinal()、HMAC-SM3密钥长度任意内部自动补位、SM3-256输出固定32字节。不依赖OpenSSL的EVP_MD完全自主实现压缩函数与消息扩展。libsm4.so仅实现SM4分组密码算法支持ECB/CBC/CTR/GCM四种模式其中CBC模式自动管理IV随机生成附带返回GCM模式支持AAD附加认证数据。所有模式均通过AES-NI指令集模拟优化ARMv8-A的crypto扩展已启用。这三个so物理隔离是因为Android系统对so加载有强校验机制若合并为单个so在某些定制ROM如某国产厂商深度定制系统上会触发SELinux策略拦截报错dlopen failed: library libsm.so not found。分开后可按需加载例如仅做签名验签的模块只引入libsm2.soAPK体积减少32%。2.3 Java层门面设计CryptoProvider接口如何统一三类算法Java层核心是com.example.sm.CryptoProvider抽象类它定义了所有国密操作的统一入口public abstract class CryptoProvider { // SM2相关 public abstract SM2KeyPair generateSM2KeyPair(); public abstract byte[] sm2Encrypt(byte[] plaintext, byte[] publicKey); public abstract byte[] sm2Decrypt(byte[] ciphertext, byte[] privateKey); public abstract byte[] sm2Sign(byte[] data, byte[] privateKey); public abstract boolean sm2Verify(byte[] data, byte[] signature, byte[] publicKey); // SM3相关 public abstract byte[] sm3Hash(byte[] data); public abstract byte[] hmacSm3(byte[] data, byte[] key); // SM4相关 public abstract SM4Result sm4Encrypt(byte[] plaintext, byte[] key, SM4Mode mode); public abstract byte[] sm4Decrypt(byte[] ciphertext, byte[] key, SM4Mode mode, byte[] iv); }实际使用时开发者只需继承该类并实现loadLibrary()加载对应so后续所有调用都通过CryptoProvider.getInstance()获取单例。这种设计屏蔽了JNI调用细节也便于单元测试——你可以写一个MockCryptoProvider返回预设结果无需启动Android环境。注意SM4Result是一个不可变容器类包含ciphertext、ivCBC/CTR/GCM模式下、authTagGCM模式下避免业务层自己拼接IV导致的安全隐患。3. 编译与集成从源码到APK三步完成Android工程接入3.1 NDK编译环境准备NDK版本、CMakeLists.txt关键配置项项目使用CMake构建要求NDK版本 ≥ r21e因用到optional和std::string_view。CMakeLists.txt中必须声明以下关键配置# 必须开启C17支持 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 启用ARMv8-A crypto扩展加速SM3/SM4 if(ANDROID_ABI STREQUAL arm64-v8a) target_compile_options(sm2 PRIVATE -marcharmv8-acrypto) target_compile_options(sm3 PRIVATE -marcharmv8-acrypto) target_compile_options(sm4 PRIVATE -marcharmv8-acrypto) endif() # 禁用RTTI和异常减小so体积Android平台不推荐异常传递 set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions) # 导出符号控制只导出JNI_OnLoad和Java_com_example_sm_*函数 set(CMAKE_SHARED_LINKER_FLAGS ${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,ALL)编译命令如下以arm64-v8a为例cd jni/ $ANDROID_NDK_HOME/ndk-build \ APP_ABIarm64-v8a \ APP_PLATFORMandroid-21 \ NDK_PROJECT_PATH. \ APP_BUILD_SCRIPTAndroid.mk \ -j4编译产物位于libs/arm64-v8a/下共三个so文件总大小约1.2MB未压缩。3.2 Android工程集成gradle配置、so加载时机与ProGuard规则在Android工程中将编译好的libs/文件夹整体复制到app/src/main/jniLibs/目录下注意不是libs/是jniLibs/。build.gradle中需添加android { defaultConfig { // 显式指定ABI过滤避免打包无用架构 ndk { abiFilters arm64-v8a, armeabi-v7a } } // 启用R8全量混淆国密算法名不能被混淆 buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } }proguard-rules.pro中必须保留以下规则# 保留JNI方法签名 -keepclasseswithmembernames class * { native methods; } # 保留CryptoProvider及其子类 -keep class com.example.sm.** { *; } # 保留SM2/SM3/SM4相关数据结构避免字段重命名导致JNI参数错位 -keep class com.example.sm.**$* { *; }so加载时机很重要不能在Application.onCreate()中立即System.loadLibrary()因为此时ClassLoader可能未就绪。正确做法是在首次调用CryptoProvider.getInstance()时懒加载public class SM2CryptoProvider extends CryptoProvider { static { // 不在此处load } Override protected void loadLibrary() { try { System.loadLibrary(sm2); System.loadLibrary(sm3); System.loadLibrary(sm4); } catch (UnsatisfiedLinkError e) { throw new RuntimeException(Failed to load SM libraries, e); } } }3.3 首次运行验证如何确认so加载成功且算法可用集成完成后写一个最简验证Activitypublic class SMTestActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sm_test); // 1. 获取实例触发so加载 CryptoProvider provider CryptoProvider.getInstance(); // 2. 生成SM2密钥对验证sm2.so SM2KeyPair keyPair provider.generateSM2KeyPair(); Log.d(SMTest, SM2 key pair generated: keyPair.getPublicKey().length); // 3. SM3哈希验证sm3.so byte[] hash provider.sm3Hash(hello world.getBytes(UTF_8)); Log.d(SMTest, SM3 hash: Hex.toHexString(hash)); // 4. SM4加密验证sm4.so SM4Result result provider.sm4Encrypt(secret.getBytes(UTF_8), keyPair.getPrivateKey(), SM4Mode.CBC); Log.d(SMTest, SM4 encrypted len: result.getCiphertext().length); } }若Logcat中出现三行D/SMTest日志且无UnsatisfiedLinkError或NoSuchMethodError说明集成成功。特别注意若在Android 12设备上报java.lang.UnsatisfiedLinkError: dlopen failed: library libsm2.so not found大概率是jniLibs/目录结构错误应为jniLibs/arm64-v8a/libsm2.so而非jniLibs/libsm2.so。4. 核心算法实现解析SM2签名、SM3-HMAC、SM4-CBC三段关键代码逐行讲透4.1 SM2签名实现为什么用ECDSA变体而非原始SM2标准SM2标准GM/T 0003-2012规定签名过程必须包含Z值计算基于用户ID和公钥的SM3哈希但很多旧版国密库直接套用ECDSA流程跳过Z值导致验签失败。本项目严格实现Z值计算并在JNI层完成全部步骤// jni/sm2/sign.cpp extern C JNIEXPORT jbyteArray JNICALL Java_com_example_sm_SM2CryptoProvider_sm2Sign(JNIEnv *env, jobject thiz, jbyteArray data, jbyteArray privKey) { // 1. 从jbyteArray提取原始数据 jbyte *dataBytes env-GetByteArrayElements(data, nullptr); jsize dataLen env-GetArrayLength(data); jbyte *keyBytes env-GetByteArrayElements(privKey, nullptr); // SM2私钥是32字节整数直接转为BN BIGNUM *d BN_bin2bn((const unsigned char*)keyBytes, 32, nullptr); // 2. 计算Z值SM3(ENTL || ID || a || b || Gx || Gy || Px || Py) // ENTLL 0x0080 (128bit)ID 1234567812345678国密默认 unsigned char z[32]; compute_z_value(z, d); // 内部调用sm3_hash // 3. 对(Z || data)做SM3哈希得到e unsigned char e[32]; unsigned char z_data[64]; memcpy(z_data, z, 32); memcpy(z_data 32, dataBytes, dataLen 32 ? 32 : dataLen); sm3_hash(z_data, dataLen 32, e); // 4. ECDSA签名kG (x1,y1), r (x1 e) mod n, s k^(-1)(r*d e) mod n EC_GROUP *group EC_GROUP_new_by_curve_name(NID_sm2p256v1); EC_KEY *key EC_KEY_new(); EC_KEY_set_group(key, group); EC_KEY_set_private_key(key, d); ECDSA_SIG *sig ECDSA_do_sign(e, 32, key); // ... 序列化r,s为ASN.1格式返回 }关键点compute_z_value()必须用项目自带的sm3_hash()不能调用OpenSSL的EVP_sm3()否则Z值不一致ECDSA_do_sign()传入的是e32字节不是原始data这是SM2与ECDSA的本质区别返回的签名是DER编码的SEQUENCE{r INTEGER, s INTEGER}Java层用ASN1Sequence.getInstance()解析。4.2 SM3-HMAC实现如何用SM3构造HMAC而不依赖外部库HMAC-SM3不是简单地SM3(key||data)而是遵循RFC 2104标准H(K ⊕ opad, H(K ⊕ ipad, data))。项目在sm3/hmac.cpp中手写实现// jni/sm3/hmac.cpp void hmac_sm3(const unsigned char *key, size_t key_len, const unsigned char *data, size_t data_len, unsigned char *out) { unsigned char k_ipad[64] {0}; // inner padding unsigned char k_opad[64] {0}; // outer padding // 1. 若key_len 64先SM3(key)得32字节再填充 unsigned char k0[32]; if (key_len 64) { sm3_hash(key, key_len, k0); key k0; key_len 32; } // 2. 填充k_ipad/k_opadkey左对齐右侧补0 memcpy(k_ipad, key, key_len); memcpy(k_opad, key, key_len); // 3. 异或ipad/opad常量0x36/0x5c for (int i 0; i 64; i) { k_ipad[i] ^ 0x36; k_opad[i] ^ 0x5c; } // 4. 计算 inner SM3(k_ipad || data) unsigned char inner[32]; unsigned char inner_input[64 1024]; // 支持最大1024字节data memcpy(inner_input, k_ipad, 64); memcpy(inner_input 64, data, data_len); sm3_hash(inner_input, 64 data_len, inner); // 5. 计算 outer SM3(k_opad || inner) unsigned char outer_input[64 32]; memcpy(outer_input, k_opad, 64); memcpy(outer_input 64, inner, 32); sm3_hash(outer_input, 64 32, out); }这段代码的关键在于完全避开OpenSSL的HMAC_CTX用纯SM3逻辑实现确保在FIPS模式或禁用OpenSSL的环境中仍可用。4.3 SM4-CBC加密IV自动管理与PKCS#7填充的边界处理SM4-CBC要求明文长度为16字节整数倍项目在Java层完成PKCS#7填充JNI层只做纯块加密// java/com/example/sm/SM4CryptoProvider.java public SM4Result sm4Encrypt(byte[] plaintext, byte[] key, SM4Mode mode) { // 1. 生成随机IV仅CBC/CTR/GCM需要 byte[] iv new byte[16]; if (mode.requiresIV()) { new SecureRandom().nextBytes(iv); } // 2. PKCS#7填充CBC模式 byte[] padded plaintext; if (mode SM4Mode.CBC) { int padLen 16 - (plaintext.length % 16); padded new byte[plaintext.length padLen]; System.arraycopy(plaintext, 0, padded, 0, plaintext.length); Arrays.fill(padded, plaintext.length, padded.length, (byte) padLen); } // 3. 调用JNI byte[] ciphertext nativeSm4Encrypt(padded, key, mode.ordinal(), iv); return new SM4Result(ciphertext, iv, null); }JNI层nativeSm4Encrypt()只接收已填充的padded数组不做任何填充判断——这避免了JNI与Java层填充逻辑不一致导致的解密失败。血泪经验曾有团队在JNI层重复填充导致解密后末尾多出16字节乱码排查三天才发现是填充逻辑重叠。5. 常见问题排查5个真实踩坑记录覆盖从编译到运行全链路5.1 现象UnsatisfiedLinkError: dlopen failed: cannot locate symbol sm3_hash原因libsm3.so未正确加载或libsm2.so/libsm4.so在libsm3.so之前加载导致符号解析失败Android动态链接器按加载顺序解析依赖。解决在loadLibrary()中严格按sm3→sm2→sm4顺序加载或在Android.mk中为libsm2.so添加-lsm3链接依赖。5.2 现象SM2签名在Android 12设备上返回空数组Logcat无报错原因Android 12强制启用scudo堆分配器而项目中部分SM2密钥派生代码使用了malloc未对齐内存触发scudo静默截断。解决在sm2/key_derive.cpp中将malloc(32)替换为aligned_alloc(32, 32)并在CMakeLists.txt中添加-D_GLIBCXX_USE_CXX11_ABI0。5.3 现象SM4-CBC解密后明文末尾出现乱码如hello\x05\x05\x05\x05\x05原因业务方误将SM4Result.iv当作密文一部分参与解密或Java层未正确执行PKCS#7去填充。解决检查解密代码是否调用sm4Decrypt(ciphertext, key, mode, iv)时传入了正确的iv必须是加密时返回的result.getIv()并在解密后手动去除填充int padLen plaintext[plaintext.length - 1] 0xFF; return Arrays.copyOf(plaintext, plaintext.length - padLen);5.4 现象ProGuard混淆后SM2验签始终返回false原因SM2KeyPair类被混淆导致JNI层通过GetObjectClass()获取的jclass与Java层不一致GetMethodID()返回NULL。解决在proguard-rules.pro中添加-keep class com.example.sm.SM2KeyPair { *; } -keep class com.example.sm.SM4Result { *; }5.5 现象armeabi-v7a设备上SM3哈希结果与arm64-v8a不一致原因SM3压缩函数中使用了未初始化的栈变量unsigned char t[64]在32位ARM上该数组未被清零残留垃圾值影响哈希。解决将unsigned char t[64]改为unsigned char t[64] {0}或用memset(t, 0, sizeof(t))显式初始化。6. 二次开发实战如何安全地扩展SM4-GCM模式并注入自定义密钥派生函数6.1 SM4-GCM模式扩展补齐认证加密的最后一块拼图项目默认支持SM4-GCM但Java层未暴露AADAdditional Authenticated Data参数。要支持「加密同时认证请求头」这类场景需扩展sm4Encrypt方法// 新增方法签名 public SM4Result sm4EncryptGCM(byte[] plaintext, byte[] key, byte[] iv, byte[] aad) { // iv必须为12字节GCM标准 if (iv.length ! 12) { throw new IllegalArgumentException(GCM IV must be 12 bytes); } byte[] ciphertext nativeSm4EncryptGCM(plaintext, key, iv, aad); // GCM模式下最后16字节是authTag byte[] authTag Arrays.copyOfRange(ciphertext, ciphertext.length - 16, ciphertext.length); byte[] actualCiphertext Arrays.copyOf(ciphertext, ciphertext.length - 16); return new SM4Result(actualCiphertext, iv, authTag); }JNI层nativeSm4EncryptGCM()需调用sm4_gcm_encrypt()函数该函数已在sm4/gcm.cpp中实现但默认未导出。修改sm4/jni_interface.cpp添加extern C JNIEXPORT jbyteArray JNICALL Java_com_example_sm_SM4CryptoProvider_nativeSm4EncryptGCM( JNIEnv *env, jobject thiz, jbyteArray data, jbyteArray key, jbyteArray iv, jbyteArray aad) { // ... 提取参数 unsigned char *ciphertext (unsigned char*) malloc(data_len 16); // 16 for authTag sm4_gcm_encrypt(plaintext, data_len, key_bytes, iv_bytes, aad_bytes, aad_len, ciphertext, auth_tag_len); // 将ciphertextauthTag打包返回 }注意GCM模式下authTag必须与密文一起传输且验签时需用相同aad重新计算否则sm4_gcm_decrypt()会返回-1认证失败。6.2 自定义密钥派生替换SM2密钥交换中的KDF函数标准SM2密钥交换使用SM3作为KDF但某些政企系统要求使用PBKDF2-SM3。项目预留了KDF扩展点在sm2/key_derive.h中定义虚函数class KeyDerivationFunction { public: virtual ~KeyDerivationFunction() default; virtual void deriveKey(const unsigned char *z, size_t z_len, const unsigned char *shared, size_t shared_len, unsigned char *out, size_t out_len) 0; };实现PBKDF2SM3KDF类重写deriveKey()内部调用pbkdf2_sm3()函数已存在于sm3/pbkdf2.cpp。然后在SM2CryptoProvider中注入public class CustomSM2Provider extends SM2CryptoProvider { Override protected KeyDerivationFunction createKDF() { return new PBKDF2SM3KDF(10000); // 迭代10000次 } }这样所有sm2Encrypt()/sm2Decrypt()调用都会走新的KDF逻辑无需修改JNI层。6.3 安全加固实践从密钥生命周期管理到防侧信道攻击在真实项目中我遇到过因密钥复用导致的中间人攻击。从那以后我每次做国密集成都强制走一遍密钥轮换流程密钥生成阶段用SecureRandom生成SM2密钥对后立即调用Arrays.fill()清空私钥字节数组密钥存储阶段绝不存私钥到SharedPreferences改用Android Keystore的KeyGenParameterSpec.Builder().setDigests(DIGEST_NONE)生成硬件绑定密钥加解密阶段SM4密钥必须从Keystore动态获取而非硬编码在so中防侧信道在sm4/encrypt.cpp中所有分支判断如if (mode CBC)替换为查表法避免时序差异泄露模式信息。这些改动都在项目security/目录下提供了参考实现。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑