如何在Android NDK C++中实现AES-256-CBC加解密、解决OpenSSL链接问题并保护加密密钥
解决Android NDK中AES-256-CBC加解密及OpenSSL集成、密钥保护问题
一、修复OpenSSL集成错误(找不到AES_cbc_encrypt)
你当前的CMakeLists.txt没有链接OpenSSL库,导致编译器无法找到AES相关函数。以下是正确的配置步骤:
1. 目录结构准备
把你的libcrypto.a和libssl.a按CPU架构放在src/main/jniLibs目录下,同时准备好OpenSSL头文件:
src/ └── main/ ├── jniLibs/ │ ├── arm64-v8a/ │ │ ├── libcrypto.a │ │ └── libssl.a │ └── armeabi-v7a/ │ ├── libcrypto.a │ └── libssl.a └── cpp/ ├── include/ │ └── openssl/ # 存放openssl/aes.h、openssl/crypto.h等头文件 ├── CMakeLists.txt └── native-lib.cpp
2. 修改CMakeLists.txt
更新配置,添加头文件路径并链接OpenSSL静态库:
cmake_minimum_required(VERSION 3.10.2) project("myapplication") # 添加OpenSSL头文件目录 include_directories(${CMAKE_SOURCE_DIR}/include) add_library( native-lib SHARED native-lib.cpp ) # 导入OpenSSL静态库 add_library(openssl-crypto STATIC IMPORTED) set_target_properties(openssl-crypto PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libcrypto.a ) add_library(openssl-ssl STATIC IMPORTED) set_target_properties(openssl-ssl PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libssl.a ) find_library( log-lib log ) target_link_libraries( native-lib openssl-crypto openssl-ssl ${log-lib} )
二、AES-256-CBC加解密实现(C++)
下面是完整的加解密代码,包含PKCS#7填充(AES要求输入长度为16字节的倍数),并支持Base64编码(方便Java层处理二进制数据):
#include <jni.h> #include <string> #include <openssl/aes.h> #include <openssl/bio.h> #include <openssl/evp.h> #include <cstring> #include <unistd.h> // PKCS#7填充函数 void pkcs7_pad(unsigned char* data, size_t data_len, size_t block_size) { size_t pad_len = block_size - (data_len % block_size); for (size_t i = 0; i < pad_len; ++i) { data[data_len + i] = static_cast<unsigned char>(pad_len); } } // PKCS#7去填充函数 size_t pkcs7_unpad(unsigned char* data, size_t data_len) { if (data_len == 0) return 0; unsigned char pad_len = data[data_len - 1]; if (pad_len > AES_BLOCK_SIZE) return data_len; for (size_t i = 0; i < pad_len; ++i) { if (data[data_len - 1 - i] != pad_len) { return data_len; // 填充无效,返回原长度 } } return data_len - pad_len; } // AES-256-CBC加密 extern "C" JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_aes256CbcEncrypt( JNIEnv* env, jobject /* this */, jstring plaintext_jstr, jstring key_jstr, jstring iv_jstr) { const char* plaintext = env->GetStringUTFChars(plaintext_jstr, nullptr); const char* key = env->GetStringUTFChars(key_jstr, nullptr); const char* iv = env->GetStringUTFChars(iv_jstr, nullptr); // 校验密钥长度(AES-256需要32字节密钥) if (strlen(key) != 32) { env->ReleaseStringUTFChars(plaintext_jstr, plaintext); env->ReleaseStringUTFChars(key_jstr, key); env->ReleaseStringUTFChars(iv_jstr, iv); return env->NewStringUTF("Error: Key must be 32 bytes (256 bits)"); } // 初始化加密上下文 AES_KEY aes_key; if (AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(key), 256, &aes_key) != 0) { env->ReleaseStringUTFChars(plaintext_jstr, plaintext); env->ReleaseStringUTFChars(key_jstr, key); env->ReleaseStringUTFChars(iv_jstr, iv); return env->NewStringUTF("Error: Failed to set encryption key"); } size_t plaintext_len = strlen(plaintext); size_t padded_len = plaintext_len + (AES_BLOCK_SIZE - (plaintext_len % AES_BLOCK_SIZE)); unsigned char* padded_plaintext = new unsigned char[padded_len]; memcpy(padded_plaintext, plaintext, plaintext_len); pkcs7_pad(padded_plaintext, plaintext_len, AES_BLOCK_SIZE); // 执行加密 unsigned char* ciphertext = new unsigned char[padded_len]; unsigned char iv_copy[AES_BLOCK_SIZE]; memcpy(iv_copy, iv, AES_BLOCK_SIZE); // CBC模式会修改IV,使用副本避免影响原数据 AES_cbc_encrypt(padded_plaintext, ciphertext, padded_len, &aes_key, iv_copy, AES_ENCRYPT); // 二进制密文转Base64 BIO* b64 = BIO_new(BIO_f_base64()); BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); BIO* bio = BIO_new(BIO_s_mem()); bio = BIO_push(b64, bio); BIO_write(bio, ciphertext, padded_len); BIO_flush(bio); char* base64_buf; long base64_len = BIO_get_mem_data(bio, &base64_buf); std::string base64_str(base64_buf, base64_len); BIO_free_all(bio); // 清理资源并清零敏感数据 delete[] padded_plaintext; delete[] ciphertext; memset(const_cast<char*>(key), 0, strlen(key)); env->ReleaseStringUTFChars(plaintext_jstr, plaintext); env->ReleaseStringUTFChars(key_jstr, key); env->ReleaseStringUTFChars(iv_jstr, iv); return env->NewStringUTF(base64_str.c_str()); } // AES-256-CBC解密 extern "C" JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_aes256CbcDecrypt( JNIEnv* env, jobject /* this */, jstring ciphertext_base64_jstr, jstring key_jstr, jstring iv_jstr) { const char* ciphertext_base64 = env->GetStringUTFChars(ciphertext_base64_jstr, nullptr); const char* key = env->GetStringUTFChars(key_jstr, nullptr); const char* iv = env->GetStringUTFChars(iv_jstr, nullptr); if (strlen(key) != 32) { env->ReleaseStringUTFChars(ciphertext_base64_jstr, ciphertext_base64); env->ReleaseStringUTFChars(key_jstr, key); env->ReleaseStringUTFChars(iv_jstr, iv); return env->NewStringUTF("Error: Key must be 32 bytes (256 bits)"); } // Base64解码为二进制密文 BIO* b64 = BIO_new(BIO_f_base64()); BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); BIO* bio = BIO_new_mem_buf(ciphertext_base64, strlen(ciphertext_base64)); bio = BIO_push(b64, bio); size_t max_cipher_len = strlen(ciphertext_base64) * 3 / 4 + 1; unsigned char* ciphertext = new unsigned char[max_cipher_len]; int ciphertext_len = BIO_read(bio, ciphertext, max_cipher_len); BIO_free_all(bio); // 初始化解密上下文 AES_KEY aes_key; if (AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(key), 256, &aes_key) != 0) { delete[] ciphertext; env->ReleaseStringUTFChars(ciphertext_base64_jstr, ciphertext_base64); env->ReleaseStringUTFChars(key_jstr, key); env->ReleaseStringUTFChars(iv_jstr, iv); return env->NewStringUTF("Error: Failed to set decryption key"); } // 执行解密 unsigned char* plaintext = new unsigned char[ciphertext_len]; unsigned char iv_copy[AES_BLOCK_SIZE]; memcpy(iv_copy, iv, AES_BLOCK_SIZE); AES_cbc_encrypt(ciphertext, plaintext, ciphertext_len, &aes_key, iv_copy, AES_DECRYPT); // 去除PKCS#7填充 size_t unpadded_len = pkcs7_unpad(plaintext, ciphertext_len); plaintext[unpadded_len] = '\0'; // 添加字符串结束符 std::string plaintext_str(reinterpret_cast<char*>(plaintext), unpadded_len); // 清理资源并清零敏感数据 delete[] ciphertext; delete[] plaintext; memset(const_cast<char*>(key), 0, strlen(key)); env->ReleaseStringUTFChars(ciphertext_base64_jstr, ciphertext_base64); env->ReleaseStringUTFChars(key_jstr, key); env->ReleaseStringUTFChars(iv_jstr, iv); return env->NewStringUTF(plaintext_str.c_str()); }
三、密钥保护与反逆向措施
针对APK易反编译的问题,从多个层面加固你的加解密流程:
1. 避免硬编码密钥
绝对不要将明文密钥直接写在代码中,推荐使用Android Keystore:
- 在Java层调用KeyStore生成不可导出的AES-256密钥
- JNI通过Java接口获取密钥引用,而非明文密钥
2. 密钥分片与动态拼接
如果必须使用固定密钥,将其拆分为多个片段:
- 比如把32字节密钥拆成4个8字节片段,分别存放在JNI常量、Java资源、assets目录中
- 运行时在JNI内动态拼接得到完整密钥
3. 代码混淆与优化
- JNI层:在
build.gradle开启编译优化,隐藏未导出函数:android { defaultConfig { ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' cppFlags "-O3 -fvisibility=hidden" } } } - Java层:开启ProGuard/R8混淆,模糊JNI调用逻辑
4. 反调试与反模拟器检查
在JNI中添加检测逻辑,阻止调试或模拟器运行:
bool isDebuggerAttached() { FILE* fp = fopen("/proc/self/status", "r"); if (!fp) return false; char buf[256]; while (fgets(buf, sizeof(buf), fp)) { if (strstr(buf, "TracerPid:") && strstr(buf, "0") == nullptr) { fclose(fp); return true; } } fclose(fp); return false; } bool isEmulator() { return access("/dev/socket/qemud", F_OK) == 0 || access("/dev/qemu_pipe", F_OK) == 0; }
在加解密函数开头调用这些检测,若触发则直接返回错误。
5. 内存敏感数据清零
使用完密钥、明文等敏感数据后,立即用memset清零,避免残留内存被dump。
最后提醒
- 测试不同CPU架构,确保OpenSSL库与架构匹配
- 完善异常处理,比如密钥长度错误、密文损坏、Base64解码失败等场景
- 遵循Android权限规范,不申请不必要的权限
内容的提问来源于stack exchange,提问作者Krishna Acharya




