You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Android NDK C++中实现AES-256-CBC加解密、解决OpenSSL链接问题并保护加密密钥

解决Android NDK中AES-256-CBC加解密及OpenSSL集成、密钥保护问题

一、修复OpenSSL集成错误(找不到AES_cbc_encrypt

你当前的CMakeLists.txt没有链接OpenSSL库,导致编译器无法找到AES相关函数。以下是正确的配置步骤:

1. 目录结构准备

把你的libcrypto.alibssl.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

火山引擎 最新活动