目录
  1. 1. 一、为什么把加密放在 Native 层?
  2. 2. 二、AES 加密算法实现
    1. 2.1. 2.1 使用 OpenSSL 的 AES-256-CBC
    2. 2.2. 2.2 AES 模式选择
    3. 2.3. 2.3 Android BoringSSL
  3. 3. 三、密钥管理
    1. 3.1. 3.1 密钥派生(避免硬编码)
    2. 3.2. 3.2 HKDF(标准密钥派生)
    3. 3.3. 3.3 Android Keystore 集成
    4. 3.4. 3.4 双层密钥架构
  4. 4. 四、文件拆分混淆
    1. 4.1. 4.1 文件混淆的附加策略
    2. 4.2. 4.2 分片重组的完整流程
  5. 5. 五、流式加密:处理大文件
  6. 6. 六、完整性验证(HMAC)
  7. 7. 七、Native 代码混淆
    1. 7.1. 7.1 OLLVM 混淆
    2. 7.2. 7.2 反调试
    3. 7.3. 7.3 字符串加密
  8. 8. 八、Java vs Native 加密性能对比
  9. 9. 九、面试常问题目
NDK文件拆分加密处理

一、为什么把加密放在 Native 层?

在 Android 中,Java/Kotlin 代码对逆向工程相对透明。即使使用了 ProGuard/R8 混淆,关键逻辑仍可通过 smali/baksmali、JADX、JEB 等工具还原。将加密逻辑迁移到 Native 层(C/C++ via NDK)可以显著提高逆向门槛:

  1. 机器码阅读难度:ARM/ARM64 汇编远比 smali 字节码难以阅读和还原。
  2. 工具链支持弱:IDA Pro / Ghidra 可以反编译 native 代码,但还原出的 C 代码质量远不如 Java 反编译工具。
  3. 混淆加固空间大:OLLVM(Obfuscator-LLVM)可以对 native 代码施加控制流平坦化、指令替换、虚假控制流等混淆。
  4. 关键常量和密钥隐藏:密钥可以分散在代码段中,通过运算动态拼装,避免明文字符串扫描。

但需要清醒认识:native 层的保护并非绝对安全。足够有决心的攻击者仍然可以使用 IDA Pro + Frida 动态调试来分析和 Hook native 函数。安全的目标是提高攻击成本,使攻击的经济投入超过收益

二、AES 加密算法实现

AES(Advanced Encryption Standard)是对称加密的事实标准,Android NDK 中通常使用 OpenSSL 或 mbed TLS 库。

2.1 使用 OpenSSL 的 AES-256-CBC

#include <openssl/aes.h>
#include <openssl/rand.h>

// 生成随机 IV(初始化向量)
int generate_iv(unsigned char *iv, int iv_len) {
return RAND_bytes(iv, iv_len); // 硬件随机数
}

// AES-256-CBC 加密
int aes_encrypt(const unsigned char *plaintext, int plaintext_len,
const unsigned char *key, // 32 字节(256-bit)
const unsigned char *iv, // 16 字节
unsigned char *ciphertext) {
AES_KEY aes_key;
if (AES_set_encrypt_key(key, 256, &aes_key) != 0) {
return -1; // key 长度错误
}

// PKCS7 填充:使数据长度为 AES_BLOCK_SIZE 的整数倍
int padded_len = ((plaintext_len / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE;
unsigned char *padded = (unsigned char *)malloc(padded_len);
memcpy(padded, plaintext, plaintext_len);
int pad_value = padded_len - plaintext_len;
memset(padded + plaintext_len, pad_value, pad_value);

// 加密
AES_cbc_encrypt(padded, ciphertext, padded_len, &aes_key, iv, AES_ENCRYPT);

free(padded);
return padded_len; // 返回加密后的长度(= padded_len)
}

// AES-256-CBC 解密
int aes_decrypt(const unsigned char *ciphertext, int ciphertext_len,
const unsigned char *key,
const unsigned char *iv,
unsigned char *plaintext) {
AES_KEY aes_key;
AES_set_decrypt_key(key, 256, &aes_key);

AES_cbc_encrypt(ciphertext, plaintext, ciphertext_len, &aes_key, iv, AES_DECRYPT);

// 移除 PKCS7 填充
int pad_value = plaintext[ciphertext_len - 1];
if (pad_value > 0 && pad_value <= AES_BLOCK_SIZE) {
return ciphertext_len - pad_value;
}
return ciphertext_len; // 填充错误,返回原始长度
}

2.2 AES 模式选择

模式 描述 优点 缺点
CBC 密文分组链接,前一个密文块与当前明文块 XOR 再加密 安全性好,同一明文不同密文(靠 IV) 不可并行加密(解密可并行)
CTR 计数器模式,加密一个递增计数器,结果与明文 XOR 可并行,不需要填充,随机访问 计数器不能重用(同 key + IV 安全灾难)
GCM Galois/Counter Mode,CTR + 认证 同时提供加密和完整性认证(AEAD) 实现稍复杂
ECB 电子密码本,每个块独立加密 简单 不安全!相同明文块产生相同密文块

推荐使用 AES-GCM,因为它提供了认证加密(AEAD, Authenticated Encryption with Associated Data),在解密后能验证数据未被篡改,省去了单独的 MAC 校验:

#include <openssl/evp.h>  // OpenSSL EVP 高级接口

// AES-256-GCM 加密(带认证)
int aes_gcm_encrypt(const unsigned char *plaintext, int plaintext_len,
const unsigned char *key,
const unsigned char *iv, int iv_len,
unsigned char *ciphertext,
unsigned char *tag) { // 16 字节认证标签
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL);
EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv);

int len;
EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len);
int ciphertext_len = len;

EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);
ciphertext_len += len;

// 获取认证标签
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag);

EVP_CIPHER_CTX_free(ctx);
return ciphertext_len;
}

// AES-256-GCM 解密(带认证验证)
int aes_gcm_decrypt(const unsigned char *ciphertext, int ciphertext_len,
const unsigned char *key,
const unsigned char *iv, int iv_len,
const unsigned char *tag,
unsigned char *plaintext) {
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL);
EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv);

int len;
EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len);
int plaintext_len = len;

// 设置认证标签
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void *)tag);

int ret = EVP_DecryptFinal_ex(ctx, plaintext + len, &len);
EVP_CIPHER_CTX_free(ctx);

if (ret > 0) {
plaintext_len += len;
return plaintext_len; // 验证通过
}
return -1; // 认证失败——数据被篡改!
}

OpenSSL 在 Android NDK 中的集成:Android 系统自带精简版 OpenSSL(libcrypto.so),但版本和 API 暴露不全。推荐使用 NDK 静态链接自己编译的 OpenSSL 或使用 mbed TLS。

2.3 Android BoringSSL

Android 从 6.0 开始使用 BoringSSL(Google 的 OpenSSL fork)替代 OpenSSL。NDK 开发中,如果使用预置的 libcrypto.so,实际上是 BoringSSL。部分 OpenSSL 的 API 在 BoringSSL 中不可用或被修改。

# 使用 Android 内置的 libcrypto(BoringSSL)
find_library(crypto-lib crypto)

target_link_libraries(nativecrypto ${crypto-lib})

三、密钥管理

密钥管理是加密系统中最脆弱的一环。如果密钥硬编码在代码中,逆向工程师用 strings 命令即可发现。

3.1 密钥派生(避免硬编码)

#include <sys/system_properties.h>  // Android system properties

void derive_master_key(unsigned char *key_out) {
// 多因子组成主密钥
char android_id[65];
char build_fingerprint[256];
char app_signature[129];

// 因子 1: Android ID(设备绑定)
__system_property_get("ro.boot.serialno", android_id);

// 因子 2: 系统构建指纹(防止跨 ROM 迁移)
__system_property_get("ro.build.fingerprint", build_fingerprint);

// 因子 3: 应用签名(通过 JNI 从 Java 层获取)
// ... get_app_signature(app_signature) ...

// 派生:HKDF (HMAC-based Key Derivation Function)
unsigned char salt[] = "com.example.app.crypto.salt";

// 使用 HMAC-SHA256 派生
unsigned char combined[1024];
snprintf((char *)combined, sizeof(combined),
"%s|%s|%s", android_id, build_fingerprint, app_signature);

HMAC(EVP_sha256(), salt, sizeof(salt) - 1,
combined, strlen((char *)combined),
key_out, NULL); // key_out 长度 = 32 (SHA256)
}

注意:更换设备或系统升级后,上述因子会变化,导致无法解密旧数据。需要设计密钥迁移/备份机制(如用户登录后从服务端获取恢复密钥)。

3.2 HKDF(标准密钥派生)

#include <openssl/kdf.h>

// 使用 HKDF (RFC 5869) 从主密钥派生数据加密密钥
int derive_key_with_hkdf(const unsigned char *master_key, size_t master_key_len,
const unsigned char *info, size_t info_len,
unsigned char *out_key, size_t out_key_len) {
EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(pctx);
EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha256());
EVP_PKEY_CTX_set1_hkdf_salt(pctx, "app_salt", 8);
EVP_PKEY_CTX_set1_hkdf_key(pctx, master_key, master_key_len);
EVP_PKEY_CTX_add1_hkdf_info(pctx, info, info_len);

size_t out_len = out_key_len;
int ret = EVP_PKEY_derive(pctx, out_key, &out_len);
EVP_PKEY_CTX_free(pctx);
return ret;
}

// 为文件分片派生独立密钥
void derive_chunk_key(const unsigned char *master_key,
uint32_t chunk_index,
unsigned char chunk_key[32]) {
derive_key_with_hkdf(master_key, 32,
(const unsigned char *)&chunk_index, sizeof(chunk_index),
chunk_key, 32);
}

3.3 Android Keystore 集成

Android Keystore System(Keymaster HAL)提供了硬件支持的密钥存储,密钥在 TEE(Trusted Execution Environment)中生成和操作,私钥永远不会离开安全硬件:

// Java 层使用 Android Keystore 生成 AES 密钥
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(new KeyGenParameterSpec.Builder(
"my_file_encryption_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build());
SecretKey key = keyGenerator.generateKey();

// 加密时使用 key(实际加密操作在 TEE 中完成)
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] ciphertext = cipher.doFinal(plaintext);
byte[] iv = cipher.getIV();

Keystore 的密钥无法被导出(即使用 root 也无法获取),提供了最高的安全等级。但需要注意的是,Keystore 操作比纯软件 AES 慢(需要 IPC 到 Keymaster),不适合加密大量文件。更好的方案是:用 Keystore 保护的密钥加密数据加密密钥(DEK, Data Encryption Key),用 DEK 对文件进行批量加密。

3.4 双层密钥架构

根密钥(KEK - Key Encryption Key)
├── 存储在 Android Keystore 中(硬件保护)
└── 用于加密/解密:DEK

数据加密密钥(DEK - Data Encryption Key)
├── 存储在文件头部(被 KEK 加密后存储)
└── 用于加密/解密:实际文件数据

文件结构:
┌──────────────────────────────────────┐
│ Encrypted DEK (被 KEK 加密,256 byte) │
├──────────────────────────────────────┤
│ IV (12 byte for GCM) │
├──────────────────────────────────────┤
│ Ciphertext (AES-256-GCM 加密的文件体) │
├──────────────────────────────────────┤
│ GCM Tag (16 byte) │
└──────────────────────────────────────┘

这种设计的优势:

  • 数据密钥(DEK)可以定期轮换而不需要重新加密根密钥
  • 根密钥被硬件保护,无法提取
  • 每个文件可以有单独的 DEK

四、文件拆分混淆

除了加密内容,还可以在文件层面进行拆分——将一个文件切成多段,每段独立加密,且保存到不同位置。这增加了逆向分析的难度。

// 文件拆分加密
typedef struct {
uint32_t index; // 分片序号
uint32_t total_chunks; // 总分片数
uint32_t data_len; // 本分片数据长度
unsigned char iv[16]; // 本分片的 IV
// 后面跟着 data_len 字节的密文
} FileChunk;

int split_and_encrypt_file(const char *input_path,
const char *output_dir,
const unsigned char *master_key) {
// 1. 读取整个文件
FILE *fp = fopen(input_path, "rb");
fseek(fp, 0, SEEK_END);
size_t file_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
unsigned char *data = malloc(file_size);
fread(data, 1, file_size, fp);
fclose(fp);

// 2. 分片(每片 64KB)
const size_t CHUNK_SIZE = 64 * 1024;
uint32_t total_chunks = (file_size + CHUNK_SIZE - 1) / CHUNK_SIZE;

for (uint32_t i = 0; i < total_chunks; i++) {
// 派生每片的密钥(基于主密钥 + 序号)
unsigned char chunk_key[32];
derive_chunk_key(master_key, i, chunk_key);

// 生成随机 IV
unsigned char iv[16];
generate_iv(iv, 16);

// 每片的明文
size_t offset = i * CHUNK_SIZE;
size_t chunk_len = (i == total_chunks - 1)
? file_size - offset
: CHUNK_SIZE;

// 加密
unsigned char *ciphertext = malloc(chunk_len + 16); // +16 用于 padding
int cipher_len = aes_gcm_encrypt(
data + offset, chunk_len, chunk_key, iv, 16,
ciphertext, NULL);

// 写入分片文件(文件名为随机字符串,打乱顺序)
char chunk_path[512];
snprintf(chunk_path, sizeof(chunk_path),
"%s/chunk_%08x.dat", output_dir, rand() ^ i);

FILE *out = fopen(chunk_path, "wb");
FileChunk header = {i, total_chunks, cipher_len};
memcpy(header.iv, iv, 16);
fwrite(&header, sizeof(header), 1, out);
fwrite(ciphertext, 1, cipher_len, out);
fclose(out);

free(ciphertext);
}

free(data);
return 0;
}

4.1 文件混淆的附加策略

  • 分片存储在不同目录(如 /data/data/<pkg>/files//sdcard/Android/data/<pkg>/)。
  • 分片的文件名用随机哈希,顺序信息仅存储在加密的索引文件中(索引文件本身也加密)。
  • 将少量关键分片(如文件头)与普通分片混合,增加重组难度。
  • 混合真实分片和诱饵分片(填充随机数据的假文件)。
  • 分片数量随机化:不要固定分片大小,在合理范围内随机变化,使得攻击者无法从分片数量推断原始文件大小。
  • 存储路径随机化:将分片路径哈希到不同目录,增加静态分析查找难度。

4.2 分片重组的完整流程

int merge_and_decrypt_chunks(const char *input_dir,
uint32_t total_chunks,
const unsigned char *master_key,
const char *output_path) {
FILE *out = fopen(output_path, "wb");
if (!out) return -1;

// 尝试多种可能的文件名模式
for (uint32_t i = 0; i < total_chunks; i++) {
char chunk_path[512];
// 可能需要遍历目录查找正确的分片
// 根据索引文件的元数据定位分片
snprintf(chunk_path, sizeof(chunk_path),
"%s/chunk_%08x.dat", input_dir, rand_seed ^ i);

FILE *in = fopen(chunk_path, "rb");
if (!in) continue;

// 读取分片头
FileChunk header;
fread(&header, sizeof(header), 1, in);

// 读取密文
unsigned char *ciphertext = malloc(header.data_len);
fread(ciphertext, 1, header.data_len, in);
fclose(in);

// 派生分片密钥
unsigned char chunk_key[32];
derive_chunk_key(master_key, header.index, chunk_key);

// 解密
unsigned char *plaintext = malloc(header.data_len);
int plain_len = aes_gcm_decrypt(ciphertext, header.data_len,
chunk_key, header.iv, 16, NULL, plaintext);

if (plain_len < 0) {
// 认证失败——数据可能被篡改
free(ciphertext);
free(plaintext);
fclose(out);
return -1;
}

// 写入输出文件
fwrite(plaintext, 1, plain_len, out);

free(ciphertext);
free(plaintext);
}

fclose(out);
return 0;
}

五、流式加密:处理大文件

对于 GB 级别的大文件(如视频、游戏资源),一次性加载到内存是不现实的。需要流式加密:

// 流式 AES-256-CTR 加密(CTR 模式支持随机访问,无需填充)
typedef struct {
EVP_CIPHER_CTX *ctx;
unsigned char iv[16];
unsigned char key[32];
} StreamEncryptor;

int stream_encrypt_init(StreamEncryptor *se, const unsigned char *key,
const unsigned char *iv) {
se->ctx = EVP_CIPHER_CTX_new();
memcpy(se->key, key, 32);
memcpy(se->iv, iv, 16);
EVP_EncryptInit_ex(se->ctx, EVP_aes_256_ctr(), NULL, key, iv);
return 0;
}

int stream_encrypt_chunk(StreamEncryptor *se,
const unsigned char *plaintext, int plaintext_len,
unsigned char *ciphertext) {
int len;
EVP_EncryptUpdate(se->ctx, ciphertext, &len, plaintext, plaintext_len);
return len;
}

void stream_encrypt_cleanup(StreamEncryptor *se) {
EVP_CIPHER_CTX_free(se->ctx);
}

// 使用示例:加密大文件
void encrypt_large_file(const char *input_path, const char *output_path,
const unsigned char *key, const unsigned char *iv) {
FILE *in = fopen(input_path, "rb");
FILE *out = fopen(output_path, "wb");

StreamEncryptor se;
stream_encrypt_init(&se, key, iv);

unsigned char buffer[64 * 1024]; // 64KB 缓冲区
unsigned char encrypted[64 * 1024 + 16]; // +16 为 AES 块对齐留余量
size_t bytes_read;

while ((bytes_read = fread(buffer, 1, sizeof(buffer), in)) > 0) {
int encrypted_len = stream_encrypt_chunk(&se, buffer, bytes_read,
encrypted);
fwrite(encrypted, 1, encrypted_len, out);
}

stream_encrypt_cleanup(&se);
fclose(in);
fclose(out);
}

六、完整性验证(HMAC)

除了 GCM 的认证标签外,对于整个文件的完整性验证也是必要的:

#include <openssl/hmac.h>

// 计算文件的 HMAC-SHA256
int compute_file_hmac(const char *file_path,
const unsigned char *hmac_key, size_t hmac_key_len,
unsigned char *hmac_out) {
HMAC_CTX *ctx = HMAC_CTX_new();
HMAC_Init_ex(ctx, hmac_key, hmac_key_len, EVP_sha256(), NULL);

FILE *fp = fopen(file_path, "rb");
unsigned char buffer[8192];
size_t bytes_read;

while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
HMAC_Update(ctx, buffer, bytes_read);
}
fclose(fp);

unsigned int len;
HMAC_Final(ctx, hmac_out, &len);
HMAC_CTX_free(ctx);
return 0;
}

// 验证文件完整性
int verify_file_integrity(const char *file_path,
const unsigned char *expected_hmac,
const unsigned char *hmac_key, size_t hmac_key_len) {
unsigned char computed_hmac[EVP_MAX_MD_SIZE];
compute_file_hmac(file_path, hmac_key, hmac_key_len, computed_hmac);
return CRYPTO_memcmp(computed_hmac, expected_hmac, 32) == 0;
// 注意:使用 CRYPTO_memcmp(常量时间比较)而非 memcmp
// 防止时序攻击(timing attack)
}

七、Native 代码混淆

加密实现本身需要保护。以下是对 native 代码的硬化措施:

7.1 OLLVM 混淆

# 使用 OLLVM 编译 native 代码
-DCMAKE_CXX_FLAGS="-mllvm -fla -mllvm -sub -mllvm -bcf"

-fla: 控制流平坦化 (Control Flow Flattening)
-sub: 指令替换 (Instruction Substitution)
-bcf: 虚假控制流 (Bogus Control Flow)

输入代码:

int check_license(const char *key) {
if (strlen(key) != 16) return 0;
int hash = 0;
for (int i = 0; i < 16; i++) hash ^= key[i] << (i % 4) * 8;
return hash == 0x12345678;
}

OLLVM 混淆后:条件判断被替换为状态机,算术运算被替换为语义等价但更复杂的序列,插入了不会执行的虚假分支。这使静态分析变得极其困难。

7.2 反调试

#include <sys/ptrace.h>
#include <unistd.h>

// 检测调试器附加
static void anti_debug() {
// 方法 1: ptrace 自我跟踪(一个进程只能被一个调试器跟踪)
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
// 已经被调试器 attach,采取保护措施
__android_log_print(ANDROID_LOG_WARN, "AntiDebug", "Debugger detected!");
// 可以退出进程、清除密钥、返回假数据等
_exit(1);
}

// 方法 2: 检查 /proc/self/status 中的 TracerPid
FILE *fp = fopen("/proc/self/status", "r");
if (fp) {
char line[256];
while (fgets(line, sizeof(line), fp)) {
if (strncmp(line, "TracerPid:", 10) == 0) {
int tracer_pid = atoi(line + 10);
if (tracer_pid != 0) {
fclose(fp);
_exit(1);
}
break;
}
}
fclose(fp);
}

// 方法 3: 时间检测——如果单步执行(调试器),指令执行时间会异常长
// 在关键代码段前后测量时间,如果超过阈值则判定有调试器
}

7.3 字符串加密

不要在 native 代码中硬编码明文字符串:

// BAD: 字符串在 .rodata 段中明文可见
const char *API_KEY = "sk-xxxx-secret-key-12345";

// GOOD: 编译时加密,运行时解密
// 使用宏或脚本在构建时将字符串加密
// 或者将字符串拆分为多个部分动态拼接
char api_key[32];
void init_api_key() {
const char part1[] = {0x73, 0x6b, 0x2d, 0x78, 0x78, 0x78, 0x78, 0x2d, 0};
const char part2[] = {0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2d, 0x6b, 0};
// XOR 解码
for (int i = 0; i < 8; i++) api_key[i] = part1[i] ^ 0x12;
for (int i = 0; i < 9; i++) api_key[i + 8] = part2[i] ^ 0x34;
api_key[24] = '\0';
}

// 更好:使用编译期字符串加密(如用构建脚本在编译前处理)
// 或使用 LLVM 插件实现 compile-time string obfuscation

八、Java vs Native 加密性能对比

// 实际测试:加密 10MB 文件
// 测试环境:Snapdragon 865, Android 11

// Java (javax.crypto.Cipher, AES-256-CBC): ~120ms
// Native C (OpenSSL AES-256-CBC): ~25ms
// Native C (mbed TLS AES-256-CBC): ~35ms
// Native C (OpenSSL AES-256-GCM): ~30ms (含认证)

C 层加密速度约为 Java 层的 4-5 倍。差异来自:

  1. JNI 调用开销被分摊(大块数据一次性传递)。
  2. OpenSSL 使用了 ARM NEON 指令集加速(AESPMULL 指令)。
  3. Java Cipher 每次 update/doFinal 都需要 JNI 调用到 BouncyCastle/OpenSSL。

对于 1MB 以下的小数据,Java 层加密足够(差异在毫秒级)。对于大文件(视频、游戏资源、数据库备份),Native 加密的性能优势显著。

九、面试常问题目

Q1: 为什么推荐 AES-GCM 而不是 AES-CBC + HMAC?

AES-GCM 同时提供加密和认证(AEAD),在算法层面统一了加密和完整性验证,减少了开发者自己实现 Encrypt-then-MAC 可能出错的风险。CBC + HMAC 需要分别管理加密密钥和 MAC 密钥,且如果顺序错了(如 MAC-then-Encrypt)可能引入安全漏洞(如 POODLE 攻击)。GCM 使用 CTR 模式加密,可并行处理,且不需要填充(避免了 Padding Oracle 攻击)。不过 GCM 对 Nonce 重用的容忍度为零——同一密钥同一个 IV 用两次就彻底破坏安全性。

Q2: 为什么密钥要动态派生而不是硬编码?怎么从代码中提取密钥?

硬编码密钥可以通过 strings 命令或 IDA 的字符串窗口直接发现。动态派生将密钥的计算分散到多个参数(设备 ID + 签名 + 系统指纹 + 算法),即使攻击者拿到派生函数的代码,也需要获取所有输入参数才能复现密钥。推荐的方案是:根密钥存储在 Android Keystore 中(硬件保护),数据密钥通过 HKDF 从根密钥派生,文件使用数据密钥加密。这样即使 native 代码被完全逆向,也无法获取根密钥。

Q3: NDK 加密相比 Java 加密的主要优势是什么?

(1) 逆向难度:Java 代码反编译后逻辑几乎完全可读,native 代码需要 ARM 汇编分析和反编译,复杂度和成本高一个数量级。(2) 性能:使用 OpenSSL + NEON 指令集,比 Java Cipher 快 4-5 倍。(3) 密钥保护:native 层的字符串和常量不像 Java 那样容易被 jadx 等工具直接提取。(4) 混淆空间:OLLVM 等工具可以对 native 代码施加控制流混淆,Java 层的混淆(ProGuard/R8)仅限于名称混淆和简单的控制流。

Q4: 文件拆分后如何提高重组的难度?

(1) 分片信息(顺序、长度、密钥)存储在加密的索引文件中,与分片分离。(2) 分片使用随机文件名(SHA256 的前 8 字节),存储在多个不同路径。(3) 每个分片使用不同的加密密钥(从主密钥 + 序号派生)和独立 IV。(4) 混合真实分片和诱饵分片(假分片,包含随机数据)。(5) 关键分片(如文件头)进行额外加密或存储在异常位置。(6) 分片大小随机化,使攻击者无法从分片数量推断原始文件大小。

Q5: 如何处理 Android Keystore 中密钥的备份和迁移问题?

Android Keystore 的密钥与设备绑定(Keymaster HAL 生成的密钥存储在 TEE 中)。用户更换设备后,Keystore 中的密钥不可用,导致无法解密旧数据。解决方案:(1) 双层密钥架构——Keystore 中保护 KEK,用服务端备份的 KEK 恢复 DEK;(2) 用户在服务端账号体系中备份 KEK(用账号密码/恢复码加密);(3) 提供数据重新加密功能(用户登录后,在新设备上从服务端恢复 KEK,重新加密本地 DEK,存入新设备的 Keystore)。


参考源码路径:

  • OpenSSL EVP:https://github.com/openssl/openssl/tree/master/crypto/evp
  • mbed TLS:https://github.com/Mbed-TLS/mbedtls
  • Android Keystore:frameworks/base/keystore/
  • Keymaster HAL:hardware/interfaces/keymaster/
  • Android libcrypto:external/boringssl/ (Android 的 OpenSSL fork)
  • OLLVM:https://github.com/obfuscator-llvm/obfuscator
  • AES-GCM RFC 5116:https://tools.ietf.org/html/rfc5116
  • NIST SP 800-38D (GCM 标准):https://csrc.nist.gov/publications/detail/sp/800-38d/final
  • HKDF RFC 5869:https://tools.ietf.org/html/rfc5869
打赏
  • 微信
  • 支付宝

评论