一、APK 安全威胁全景
1.1 APK 反编译技术链
任何一个 APK 本质上是一个 ZIP 压缩包,包含了编译后的字节码、资源文件和签名信息。Android 逆向工程师通过以下工具链可以轻易还原 APK 的内部逻辑:
APK 文件 │ ├─ unzip 解压 │ ├─ classes.dex → dex2jar → .jar → JD-GUI / Jadx → Java 源码 │ ├─ res/ → aapt dump → 资源文件 │ ├─ AndroidManifest.xml → AXMLPrinter2 → 可读 XML │ ├─ lib/ → IDA Pro / Ghidra → ARM 汇编 → C 伪代码 │ └─ META-INF/ → 签名信息 │ └─ apktool 反编译 └─ smali 代码 → 修改 → 重新打包 → 重签名
|
常见的逆向工具:
- Jadx:DEX → Java 源码,反编译质量最高
- apktool:DEX → Smali,支持重新打包
- IDA Pro / Ghidra:Native .so 逆向
- Frida / Xposed:动态运行时 Hook
- Charles / mitmproxy:HTTPS 中间人攻击
1.2 APK 文件结构
app-release.apk ├── AndroidManifest.xml (二进制 XML,声明组件和权限) ├── classes.dex (主 DEX 文件,Java/Kotlin 字节码) ├── classes2.dex (MultiDex 的额外 DEX 文件) ├── res/ (编译后的资源) │ ├── layout/ │ ├── drawable/ │ ├── values/ │ └── ... ├── resources.arsc (编译后的资源索引表) ├── lib/ (Native .so 库) │ ├── arm64-v8a/ │ │ ├── libnative.so │ │ └── libcrypto.so │ ├── armeabi-v7a/ │ └── x86_64/ ├── assets/ (原始 assets 文件) ├── META-INF/ (签名信息) │ ├── CERT.RSA │ ├── CERT.SF │ └── MANIFEST.MF └── kotlin/ (Kotlin 元数据,如果有)
|
1.3 加固的基本原理
加固的通用思路:
- 整体加固:将原 APK 的 DEX 加密后存储在 assets 中。运行时壳程序解密 DEX,通过
DexClassLoader 加载
- DEX 加固:对 DEX 中的方法体加密,运行时通过 JIT Hook 在方法执行前解密
- SO 加固:对 Native .so 的关键节(.text, .rodata)加密,运行时解密
- 资源加密:对 assets/res 中的敏感文件(脚本、配置、模型文件)加密
原始 APK → 加密关键文件 → 附加壳 DEX/SO → 重新签名 → 加固后的 APK
运行时: 加固 APK → 壳 Application.attachBaseContext() 启动 → 解密原 DEX → DexClassLoader 加载原 DEX → 反射调用原 Application → 正常业务逻辑
|
二、AES 算法深度解析
2.1 AES 算法原理
AES(Advanced Encryption Standard)是一种对称分组密码,由 Joan Daemen 和 Vincent Rijmen 设计。它将明文分为 128 位(16 字节)的块,通过多轮变换加密。
关键参数:
| 参数 |
AES-128 |
AES-192 |
AES-256 |
| 密钥长度 |
128 位(16 字节) |
192 位(24 字节) |
256 位(32 字节) |
| 轮数 |
10 |
12 |
14 |
| 扩展密钥大小 |
176 字节 |
208 字节 |
240 字节 |
单轮变换包含四个步骤:
- SubBytes:通过 S-Box 进行字节替换(非线性变换,提供混淆)
- ShiftRows:行移位(提供扩散)
- MixColumns:列混淆(提供扩散,最后一轮省略)
- AddRoundKey:与轮密钥异或
Android 默认的 AES-128 安全强度:穷举攻击需要 2^128 次操作,即使用全世界所有计算资源,也需要数十亿年。
2.2 ECB 模式 —— 最基础也最危险
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encrypted = cipher.doFinal(plaintext);
|
ECB 的问题:
- 相同的明文块产生相同的密文块
- 无法隐藏数据模式(企鹅 ECB 加密后轮廓仍然可见)
- 攻击者可以重排密文块来操控解密结果
绝不使用 ECB 模式进行文件加密。
2.3 CBC 模式
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(generateRandomBytes(16)); cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); byte[] encrypted = cipher.doFinal(plaintext);
|
CBC 的特点:
- 每个密文块依赖前一个密文块(链式依赖)
- 需要初始向量 IV(Initialization Vector)
- IV 必须随机且不可预测(不能用计数器)
- 不能并行加密(因为块间有依赖),但可以并行解密
- 不提供完整性保护,需要配合 HMAC
2.4 CTR 模式 —— 流密码模式
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); IvParameterSpec iv = new IvParameterSpec(counter); cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); byte[] encrypted = cipher.doFinal(plaintext);
|
CTR 的特点:
- 将分组密码转换为流密码
- 不需要填充(NoPadding)
- 可以并行加密
- 计数器决不能重复使用同一密钥(否则安全性崩溃)
- 同样需要 HMAC 来保证完整性
2.5 GCM 模式 —— Android 文件加密首选
GCM = Galois/Counter Mode,结合了 CTR 模式的加密和 GMAC 的认证:
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonce = new byte[12]; SecureRandom.getInstanceStrong().nextBytes(nonce); GCMParameterSpec spec = new GCMParameterSpec(128, nonce); cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
byte[] ciphertext = cipher.doFinal(plaintext);
|
GCM 的优势:
- 认证加密(AEAD):同时提供机密性和完整性
- 任何密文篡改都会在解密时被检测到(抛出 AEADBadTagException)
- 不需要单独的 HMAC
- 可以并行加密
- 支持 Additional Authenticated Data(AAD),绑定附加信息
GCM 的严格规则:
- 同一密钥下,nonce(IV)绝对不能重复使用
- 推荐 nonce 长度为 12 字节(96 位),性能最优
- 最多加密 2^32 个块(约 64GB)
三、密钥派生
3.1 为什么需要密钥派生
用户输入的密码通常长度不够、熵不足、不符合 AES 密钥的格式要求。密钥派生函数(KDF)将任意长度的密码转换为固定长度的密钥。
3.2 PBKDF2 with HMAC-SHA256
public static SecretKey deriveKey(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { int iterations = 10000; int keyLength = 256;
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec( password.toCharArray(), salt, iterations, keyLength ); SecretKey tmp = factory.generateSecret(spec); return new SecretKeySpec(tmp.getEncoded(), "AES"); }
public static byte[] generateSalt() { byte[] salt = new byte[16]; SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(salt); return salt; }
|
PBKDF2 的核心计算:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
U_1 = PRF(Password, Salt || INT_32_BE(1)) U_2 = PRF(Password, U_1) ... U_c = PRF(Password, U_{c-1}) T_1 = U_1 XOR U_2 XOR ... XOR U_c // 重复直到生成足够长的密钥 DK = T_1 || T_2 || ... || T_l
|
3.3 Argon2 —— 现代替代方案
Argon2 是 2015 年密码哈希竞赛(PHC)的获胜者,被广泛认为是当前最安全的 KDF:
fun deriveKeyWithArgon2(password: String, salt: ByteArray): SecretKey { val argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id) val iterations = 3 val memory = 65536 val parallelism = 4 val hashLength = 32
val hash = argon2.hash(iterations, memory, parallelism, password.toCharArray(), StandardCharsets.UTF_8, salt) return SecretKeySpec(hash, 0, 32, "AES") }
|
Argon2 的三个变体:
- Argon2d:最大化 GPU 抵抗能力(依赖数据访问模式)
- Argon2i:最大化侧信道攻击抵抗(数据访问模式与密码无关)
- Argon2id:前一半 Argon2i,后一半 Argon2d,综合安全性最好
3.4 PBKDF2 vs Argon2 对比
| 特性 |
PBKDF2 |
Argon2 |
| 年份 |
2000 (RFC 2898) |
2015 (RFC 9106) |
| GPU 抵抗 |
弱(可用 GPU 并行加速) |
强(内存硬性要求) |
| 内存开销 |
极低 |
可配置(MB 级) |
| ASIC 抵抗 |
弱 |
强 |
| Android API |
内置(无需额外依赖) |
需要第三方库 |
| NIST 推荐 |
继续推荐 |
尚未纳入 FIPS |
| 适用场景 |
兼容性优先 |
安全性优先 |
四、Android Keystore —— 硬件级密钥保护
4.1 Android Keystore 架构
Android Keystore 提供了硬件支持的密钥存储和安全操作。在支持的设备上(如使用 TEE 或 SE),密钥永远不会离开安全硬件。
应用层 │ ├─ java.security.KeyStore (JCA) │ └─ "AndroidKeyStore" provider │ ├─ KeyStore Service (keystore2 in Android 12+) │ └─ /dev/binder 或 HIDL │ ├─ Keymaster HAL (Hardware Abstraction Layer) │ ├─ Software implementation (Keymaster 4.0+ 纯软件实现) │ ├─ TEE implementation (Trusty TEE / Qualcomm QTEE) │ └─ StrongBox (独立安全芯片, Keymaster 4.0+ 硬件实现) │ └─ 硬件 ├─ ARM TrustZone (TEE) ├─ Secure Element (SE) └─ StrongBox Keymaster (独立安全芯片)
|
AOSP 源码参考:
system/security/keystore2/ — Android 12+ Keystore 服务
system/keymaster/ — Keymaster HAL 实现
frameworks/base/core/java/android/security/keystore/ — Android Keystore 的 Java API
4.2 生成 AES 密钥(硬件支持)
@RequiresApi(api = Build.VERSION_CODES.M) public static SecretKey generateAesKey(String alias) throws Exception { KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder( alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT ) .setKeySize(256) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setRandomizedEncryptionRequired(true) .setUserAuthenticationRequired(false) .build();
KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); keyGenerator.init(spec); return keyGenerator.generateKey(); }
|
4.3 使用 Keystore 密钥进行 RSA 包装加密
通常的做法是用 Keystore 中的 RSA 公钥(或 EC 公钥)加密 AES 密钥,只有拥有对应私钥的 Keystore 才能解密:
@RequiresApi(api = Build.VERSION_CODES.M) public static void generateRsaKeyPair(String alias) throws Exception { KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder( alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT ) .setKeySize(2048) .setDigests(KeyProperties.DIGEST_SHA256) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) .build();
KeyPairGenerator generator = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); generator.initialize(spec); generator.generateKeyPair(); }
public static byte[] wrapAesKey(String rsaAlias, SecretKey aesKey) throws Exception { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); PublicKey publicKey = keyStore.getCertificate(rsaAlias).getPublicKey();
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); cipher.init(Cipher.WRAP_MODE, publicKey); return cipher.wrap(aesKey); }
public static SecretKey unwrapAesKey(String rsaAlias, byte[] wrappedKey) throws Exception { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); PrivateKey privateKey = (PrivateKey) keyStore.getKey(rsaAlias, null);
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); cipher.init(Cipher.UNWRAP_MODE, privateKey); return (SecretKey) cipher.unwrap(wrappedKey, "AES", Cipher.SECRET_KEY); }
|
4.4 AES-GCM 加密/解密
public static byte[] encrypt(SecretKey key, byte[] plaintext) throws Exception { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonce = new byte[12]; SecureRandom.getInstanceStrong().nextBytes(nonce); GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext);
byte[] result = new byte[nonce.length + ciphertext.length]; System.arraycopy(nonce, 0, result, 0, nonce.length); System.arraycopy(ciphertext, 0, result, nonce.length, ciphertext.length); return result; }
public static byte[] decrypt(SecretKey key, byte[] encryptedData) throws Exception { byte[] nonce = Arrays.copyOfRange(encryptedData, 0, 12); byte[] ciphertext = Arrays.copyOfRange(encryptedData, 12, encryptedData.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec spec = new GCMParameterSpec(128, nonce); cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(ciphertext); }
|
五、完整文件加密实现
5.1 文件加密管线
原文件 (plaintext file) │ ├─ 生成随机 AES-256 密钥 (Data Encryption Key, DEK) │ ├─ 用 DEK + GCM 加密文件内容 │ └─ 输出: nonce(12B) + ciphertext + tag(16B) │ ├─ 用 Keystore RSA 公钥加密 DEK │ └─ 输出: wrappedKey (由 RSA-2048 OAEP 包装) │ └─ 打包: [wrappedKey length (4B)] [wrappedKey] [nonce (12B)] [ciphertext + tag] └─ 写入 .enc 文件
|
5.2 Encryptor 完整实现
public class FileEncryptor { private static final int GCM_NONCE_LENGTH = 12; private static final int GCM_TAG_LENGTH = 16; private static final int AES_KEY_SIZE = 256; private static final int BUFFER_SIZE = 8192;
private final String keystoreAlias;
public FileEncryptor(String keystoreAlias) { this.keystoreAlias = keystoreAlias; }
public void encrypt(File inputFile, File outputFile) throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(AES_KEY_SIZE); SecretKey dek = keyGenerator.generateKey();
byte[] nonce = new byte[GCM_NONCE_LENGTH]; SecureRandom.getInstanceStrong().nextBytes(nonce);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); cipher.init(Cipher.ENCRYPT_MODE, dek, gcmSpec);
byte[] wrappedKey = wrapDataEncryptionKey(dek);
try (FileOutputStream fos = new FileOutputStream(outputFile); DataOutputStream dos = new DataOutputStream(fos)) {
dos.writeInt(wrappedKey.length); dos.write(wrappedKey);
dos.write(nonce);
try (FileInputStream fis = new FileInputStream(inputFile); CipherOutputStream cos = new CipherOutputStream(fos, cipher)) { byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { cos.write(buffer, 0, bytesRead); } } } }
private byte[] wrapDataEncryptionKey(SecretKey dek) throws Exception { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); PublicKey publicKey = keyStore.getCertificate(keystoreAlias).getPublicKey();
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); rsaCipher.init(Cipher.WRAP_MODE, publicKey); return rsaCipher.wrap(dek); } }
|
5.3 Decryptor 完整实现
public class FileDecryptor { private static final int GCM_NONCE_LENGTH = 12; private static final int GCM_TAG_LENGTH = 16; private static final int BUFFER_SIZE = 8192;
private final String keystoreAlias;
public FileDecryptor(String keystoreAlias) { this.keystoreAlias = keystoreAlias; }
public void decrypt(File encryptedFile, File outputFile) throws Exception { try (FileInputStream fis = new FileInputStream(encryptedFile); DataInputStream dis = new DataInputStream(fis)) {
int wrappedKeyLength = dis.readInt(); byte[] wrappedKey = new byte[wrappedKeyLength]; dis.readFully(wrappedKey);
byte[] nonce = new byte[GCM_NONCE_LENGTH]; dis.readFully(nonce);
SecretKey dek = unwrapDataEncryptionKey(wrappedKey);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); cipher.init(Cipher.DECRYPT_MODE, dek, gcmSpec);
try (FileOutputStream fos = new FileOutputStream(outputFile); CipherInputStream cis = new CipherInputStream(fis, cipher)) { byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead; while ((bytesRead = cis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } } }
private SecretKey unwrapDataEncryptionKey(byte[] wrappedKey) throws Exception { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); PrivateKey privateKey = (PrivateKey) keyStore.getKey(keystoreAlias, null);
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); rsaCipher.init(Cipher.UNWRAP_MODE, privateKey); return (SecretKey) rsaCipher.unwrap(wrappedKey, "AES", Cipher.SECRET_KEY); } }
|
六、大文件分块加密
对于 1GB+ 的大文件,一次性加载到内存不可行。需要分块加密:
public class ChunkedFileEncryptor { private static final int CHUNK_SIZE = 4096; private static final int GCM_NONCE_LENGTH = 12; private static final int GCM_TAG_LENGTH = 16;
public void encryptLargeFile(File inputFile, File outputFile, SecretKey key) throws Exception { try (FileInputStream fis = new FileInputStream(inputFile); FileOutputStream fos = new FileOutputStream(outputFile); DataOutputStream dos = new DataOutputStream(fos)) {
dos.writeInt(CHUNK_SIZE);
byte[] buffer = new byte[CHUNK_SIZE]; long sequenceNumber = 0; int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) { byte[] nonce = new byte[GCM_NONCE_LENGTH]; SecureRandom.getInstanceStrong().nextBytes(nonce); ByteBuffer.wrap(nonce, 8, 4).putInt((int) sequenceNumber);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] aad = ByteBuffer.allocate(8).putLong(sequenceNumber).array(); cipher.updateAAD(aad);
byte[] ciphertext = cipher.doFinal(buffer, 0, bytesRead);
dos.write(nonce); dos.writeInt(ciphertext.length); dos.write(ciphertext);
sequenceNumber++; } } }
public void decryptLargeFile(File encryptedFile, File outputFile, SecretKey key) throws Exception { try (FileInputStream fis = new FileInputStream(encryptedFile); DataInputStream dis = new DataInputStream(fis); FileOutputStream fos = new FileOutputStream(outputFile)) {
int chunkSize = dis.readInt(); long sequenceNumber = 0;
while (dis.available() > 0) { byte[] nonce = new byte[GCM_NONCE_LENGTH]; dis.readFully(nonce);
int ciphertextLength = dis.readInt(); byte[] ciphertext = new byte[ciphertextLength]; dis.readFully(ciphertext);
int storedSeqNum = ByteBuffer.wrap(nonce, 8, 4).getInt(); if (storedSeqNum != sequenceNumber) { throw new SecurityException( "Chunk sequence mismatch: expected " + sequenceNumber + ", got " + storedSeqNum); }
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] aad = ByteBuffer.allocate(8).putLong(sequenceNumber).array(); cipher.updateAAD(aad);
byte[] plaintext = cipher.doFinal(ciphertext); fos.write(plaintext);
sequenceNumber++; } } } }
|
分块加密的安全关键点:
- 每个 chunk 必须使用唯一的 nonce(通过
随机前缀 + 递增序列号 保证)
- 使用序列号作为 AAD,防止攻击者重排 chunk 顺序
- 每个 chunk 有独立的认证标签,任何篡改都会立即可检测
七、性能考量
7.1 硬件 AES 加速
现代 ARM 处理器支持 AES 硬件指令集(ARMv8-A Cryptographic Extension):
ARMv8-A 的 AES 指令: - AESE: AES 加密轮 - AESD: AES 解密轮 - AESMC: AES 列混淆 - AESIMC: AES 逆列混淆
这些指令在 1-3 个时钟周期内完成一轮 AES 变换, 软件实现需要 10-30 个周期。
|
检查硬件加速是否可用:
public static boolean isAesHardwareAccelerated() { try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); Provider provider = cipher.getProvider(); Log.d("AES", "Provider: " + provider.getName()); return true; } catch (Exception e) { return false; } }
|
7.2 性能基准测试
public class AesBenchmark { private static final int TEST_SIZE = 1024 * 1024; private static final int WARMUP_ITERATIONS = 10; private static final int TEST_ITERATIONS = 50;
public static void main(String[] args) throws Exception { SecretKey key = generateKey(); byte[] plaintext = new byte[TEST_SIZE]; new SecureRandom().nextBytes(plaintext);
for (int i = 0; i < WARMUP_ITERATIONS; i++) { encryptGcm(key, plaintext); }
long totalNanos = 0; for (int i = 0; i < TEST_ITERATIONS; i++) { long start = System.nanoTime(); byte[] encrypted = encryptGcm(key, plaintext); byte[] decrypted = decryptGcm(key, encrypted); long end = System.nanoTime(); totalNanos += (end - start);
if (!Arrays.equals(plaintext, decrypted)) { throw new RuntimeException("Encryption/decryption mismatch!"); } }
double avgMs = totalNanos / 1_000_000.0 / TEST_ITERATIONS; double throughputMBps = (TEST_SIZE / (1024.0 * 1024.0)) / (avgMs / 1000.0);
System.out.printf("AES-256-GCM: %.2f ms per 1MB (%.2f MB/s)%n", avgMs, throughputMBps); } }
|
典型性能数据(骁龙 865 / OnePlus 8):
| 操作 |
软件实现 (Java) |
硬件加速 (AES-NI/ARMv8) |
| AES-256-GCM 加密 1MB |
~8 ms |
~1.5 ms |
| AES-256-GCM 解密 1MB |
~9 ms |
~1.8 ms |
| RSA-2048 OAEP 包装 |
~0.3 ms |
~0.3 ms(RSA 无硬件加速) |
| PBKDF2-SHA256 10000 迭代 |
~120 ms |
~120 ms(SHA 有硬件加速) |
八、安全最佳实践清单
8.1 必须遵守
- 永远使用 GCM 模式:
AES/GCM/NoPadding
- nonce 决不重用:使用
SecureRandom.getInstanceStrong() 生成 12 字节 nonce
- 密钥不硬编码:使用 Android Keystore 存储密钥
- Salt 全局唯一:每个文件使用不同的 salt
- 验证认证标签:GCM 自动验证,不要捕获
AEADBadTagException 后使用错误数据
- 使用 HTTPS:即使内容加密,传输也应加密(纵深防御)
- 安全删除:加密完成后使用
File.delete() 并覆盖原文件
8.2 应该避免
- 不要使用 ECB:
AES/ECB/* 任何模式都不应使用
- 不要使用静态 IV:每个加密操作使用新的随机 nonce/IV
- 不要自己设计密码学协议:使用 NIST 标准化的模式和算法
- 不要在 Java 层持有密钥明文过久:用完后立即用
Arrays.fill(keyBytes, (byte) 0) 覆盖
- 不要信任用户提供的密码:始终使用 PBKDF2/Argon2 派生密钥
- 不要遗留调试日志:生产版本移除所有密钥、明文相关的日志
8.3 纵深防御
应用层加密 (AES-GCM) + Android Keystore (硬件保护密钥) + 代码混淆 (ProGuard/R8) + 完整性校验 (签名验证) + 运行时保护 (检测 Frida/Xposed) = 多层安全防护
|
九、总结
AES 文件加密在 Android 安全中的位置是数据层防护的核心。总结关键决策:
- 算法选择:AES-256-GCM,提供认证加密
- 密钥管理:Android Keystore(TEE/SE 硬件保护) + RSA 包装
- 大文件处理:分块加密,每块独立 nonce + AAD 序列号绑定
- 密钥派生:PBKDF2(内置兼容)或 Argon2(更高安全)
加密不是银弹:它保护数据在存储和传输中的机密性,但不能防止运行时的内存 dump、动态 Hook 和 Root 环境下的攻击。良好的安全架构需要纵深防御——多层防护叠加,任何一个被突破后仍有其他层保护。
参考资源