简介:本资源是一套面向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.0~14、支持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)手写核心算法逻辑,并用C++17特性做了关键优化。JNI层严格遵循Android ABI规范,为armeabi-v7a、arm64-v8a、x86_64分别编译so文件,避免运行时加载失败;Java层不暴露任何底层指针或上下文对象,所有方法均为静态工具类调用,彻底规避内存泄漏风险。这种设计牺牲了一定的扩展灵活性(比如不支持自定义SM4 S盒),但换来的是极简集成路径和可预测的性能表现——实测在骁龙660设备上,SM2签名耗时稳定在12~15ms,SM4-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.so),APK体积减少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、iv(CBC/CTR/GCM模式下)、authTag(GCM模式下),避免业务层自己拼接IV导致的安全隐患。
3. 编译与集成:从源码到APK,三步完成Android工程接入
3.1 NDK编译环境准备:NDK版本、CMakeLists.txt关键配置项
项目使用CMake构建,要求NDK版本 ≥ r21e(因用到<optional>和std::string_view)。CMakeLists.txt中必须声明以下关键配置:
# 必须开启C++17支持 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 -march=armv8-a+crypto) target_compile_options(sm3 PRIVATE -march=armv8-a+crypto) target_compile_options(sm4 PRIVATE -march=armv8-a+crypto) 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_ABI=arm64-v8a \ APP_PLATFORM=android-21 \ NDK_PROJECT_PATH=. \ APP_BUILD_SCRIPT=Android.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加载成功且算法可用?
集成完成后,写一个最简验证Activity:
public 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()传入的是e(32字节),不是原始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_opad:key左对齐,右侧补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_ABI=0。
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层未暴露AAD(Additional 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); // 将ciphertext+authTag打包返回 }注意: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/目录下提供了参考实现。希望帮到你。
本文还有配套的精品资源,点击获取