目录
  1. 1. 一、APK 安全威胁全景
    1. 1.1. 1.1 APK 反编译技术链
    2. 1.2. 1.2 APK 文件结构
    3. 1.3. 1.3 加固的基本原理
  2. 2. 二、AES 算法深度解析
    1. 2.1. 2.1 AES 算法原理
    2. 2.2. 2.2 ECB 模式 —— 最基础也最危险
    3. 2.3. 2.3 CBC 模式
    4. 2.4. 2.4 CTR 模式 —— 流密码模式
    5. 2.5. 2.5 GCM 模式 —— Android 文件加密首选
  3. 3. 三、密钥派生
    1. 3.1. 3.1 为什么需要密钥派生
    2. 3.2. 3.2 PBKDF2 with HMAC-SHA256
    3. 3.3. 3.3 Argon2 —— 现代替代方案
    4. 3.4. 3.4 PBKDF2 vs Argon2 对比
  4. 4. 四、Android Keystore —— 硬件级密钥保护
    1. 4.1. 4.1 Android Keystore 架构
    2. 4.2. 4.2 生成 AES 密钥(硬件支持)
    3. 4.3. 4.3 使用 Keystore 密钥进行 RSA 包装加密
    4. 4.4. 4.4 AES-GCM 加密/解密
  5. 5. 五、完整文件加密实现
    1. 5.1. 5.1 文件加密管线
    2. 5.2. 5.2 Encryptor 完整实现
    3. 5.3. 5.3 Decryptor 完整实现
  6. 6. 六、大文件分块加密
  7. 7. 七、性能考量
    1. 7.1. 7.1 硬件 AES 加速
    2. 7.2. 7.2 性能基准测试
  8. 8. 八、安全最佳实践清单
    1. 8.1. 8.1 必须遵守
    2. 8.2. 8.2 应该避免
    3. 8.3. 8.3 纵深防御
  9. 9. 九、总结
  10. 10. 参考资源
【实战系列】IO之AES加密加固

一、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 加固的基本原理

加固的通用思路:

  1. 整体加固:将原 APK 的 DEX 加密后存储在 assets 中。运行时壳程序解密 DEX,通过 DexClassLoader 加载
  2. DEX 加固:对 DEX 中的方法体加密,运行时通过 JIT Hook 在方法执行前解密
  3. SO 加固:对 Native .so 的关键节(.text, .rodata)加密,运行时解密
  4. 资源加密:对 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 字节

单轮变换包含四个步骤

  1. SubBytes:通过 S-Box 进行字节替换(非线性变换,提供混淆)
  2. ShiftRows:行移位(提供扩散)
  3. MixColumns:列混淆(提供扩散,最后一轮省略)
  4. 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");
// IV 必须是不可预测的随机数
IvParameterSpec iv = new IvParameterSpec(generateRandomBytes(16));
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
byte[] encrypted = cipher.doFinal(plaintext);
// 需要保存 IV,解密时使用

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");
// IV 在 GCM 中称为 nonce,推荐 12 字节
byte[] nonce = new byte[12];
SecureRandom.getInstanceStrong().nextBytes(nonce);
GCMParameterSpec spec = new GCMParameterSpec(128, nonce); // 128-bit 认证标签
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);

byte[] ciphertext = cipher.doFinal(plaintext);
// ciphertext 的最后 16 字节是认证标签(Authentication Tag)

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 {
// 迭代次数:10000+ 是 NIST 推荐的最低值(2017)
// 对于移动设备,建议 10000 ~ 100000
int iterations = 10000;
int keyLength = 256; // AES-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");
}

// Salt 生成
public static byte[] generateSalt() {
byte[] salt = new byte[16]; // 128-bit salt
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:

// 使用 Argon2 JVM 库
// implementation("de.mkammerer:argon2-jvm:2.11")
fun deriveKeyWithArgon2(password: String, salt: ByteArray): SecretKey {
val argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id)

// Argon2id 参数
val iterations = 3 // 时间成本(t)
val memory = 65536 // 内存成本,KB(m = 64MB)
val parallelism = 4 // 并行度(p)
val hashLength = 32 // 输出长度(AES-256)

val hash = argon2.hash(iterations, memory, parallelism,
password.toCharArray(), StandardCharsets.UTF_8, salt)

// 只取需要的 32 字节作为 AES 密钥
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) // 要求每次加密使用随机 IV
.setUserAuthenticationRequired(false) // 不需要用户认证(自动解密)
// 如果要绑定用户生物认证:
// .setUserAuthenticationRequired(true)
// .setUserAuthenticationValidityDurationSeconds(60) // 认证后 60 秒内可用
.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 才能解密:

// 生成 RSA 密钥对(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();
}

// 用 RSA 公钥包装 AES 密钥
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);
}

// 用 RSA 私钥解包装 AES 密钥
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");

// 生成随机 nonce(12 字节)
byte[] nonce = new byte[12];
SecureRandom.getInstanceStrong().nextBytes(nonce);
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);

cipher.init(Cipher.ENCRYPT_MODE, key, spec);

// 可选:添加 AAD(Additional Authenticated Data)
// cipher.updateAAD(aad);

byte[] ciphertext = cipher.doFinal(plaintext);

// 返回值:nonce (12B) + ciphertext + tag (16B)
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 {
// 分离 nonce 和密文
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);

// 如果有 AAD,需要同样的 AAD
// cipher.updateAAD(aad);

return cipher.doFinal(ciphertext);
// 如果认证标签校验失败,抛出 AEADBadTagException
}

五、完整文件加密实现

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; // 96 bits
private static final int GCM_TAG_LENGTH = 16; // 128 bits
private static final int AES_KEY_SIZE = 256;
private static final int BUFFER_SIZE = 8192; // 8KB 缓冲区

private final String keystoreAlias;

public FileEncryptor(String keystoreAlias) {
this.keystoreAlias = keystoreAlias;
}

public void encrypt(File inputFile, File outputFile) throws Exception {
// 1. 生成随机 AES 密钥(DEK)
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(AES_KEY_SIZE);
SecretKey dek = keyGenerator.generateKey();

// 2. 生成随机 nonce
byte[] nonce = new byte[GCM_NONCE_LENGTH];
SecureRandom.getInstanceStrong().nextBytes(nonce);

// 3. 用 AES-GCM 加密文件内容
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce);
cipher.init(Cipher.ENCRYPT_MODE, dek, gcmSpec);

// 4. 用 Keystore RSA 公钥包装 DEK
byte[] wrappedKey = wrapDataEncryptionKey(dek);

// 5. 写入输出文件
try (FileOutputStream fos = new FileOutputStream(outputFile);
DataOutputStream dos = new DataOutputStream(fos)) {

// 写入 wrapped key 的长度和内容
dos.writeInt(wrappedKey.length);
dos.write(wrappedKey);

// 写入 nonce
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);
}
}
// CipherOutputStream.close() 会写入最后的认证标签
}
}

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)) {

// 1. 读取 wrapped key
int wrappedKeyLength = dis.readInt();
byte[] wrappedKey = new byte[wrappedKeyLength];
dis.readFully(wrappedKey);

// 2. 读取 nonce
byte[] nonce = new byte[GCM_NONCE_LENGTH];
dis.readFully(nonce);

// 3. 用 Keystore RSA 私钥解包装 DEK
SecretKey dek = unwrapDataEncryptionKey(wrappedKey);

// 4. 准备解密
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce);
cipher.init(Cipher.DECRYPT_MODE, dek, gcmSpec);

// 5. 写入解密后的文件
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; // 4KB per chunk
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)) {

// 写入文件头:Chunk Size
dos.writeInt(CHUNK_SIZE);

byte[] buffer = new byte[CHUNK_SIZE];
long sequenceNumber = 0;
int bytesRead;

while ((bytesRead = fis.read(buffer)) != -1) {
// 每个 chunk 使用唯一的 nonce
// 方式:nonce(8B 随机) + sequenceNumber(4B 大端)
byte[] nonce = new byte[GCM_NONCE_LENGTH];
SecureRandom.getInstanceStrong().nextBytes(nonce);
// 将序列号写入 nonce 的最后 4 字节,保证全局唯一
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);

// 绑定 chunk 序列号作为 AAD,防止 chunk 重排攻击
byte[] aad = ByteBuffer.allocate(8).putLong(sequenceNumber).array();
cipher.updateAAD(aad);

byte[] ciphertext = cipher.doFinal(buffer, 0, bytesRead);

// 写入 chunk: [nonce(12B)] [ciphertext+tag]
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) {
// 读取 nonce
byte[] nonce = new byte[GCM_NONCE_LENGTH];
dis.readFully(nonce);

// 读取 ciphertext
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);

// 同样的 AAD
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 个周期。

检查硬件加速是否可用:

// Android 中,Java Cipher 自动使用硬件加速(如果可用)
// 可以通过以下方式检测:
public static boolean isAesHardwareAccelerated() {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// Provider 信息
Provider provider = cipher.getProvider();
Log.d("AES", "Provider: " + provider.getName());
// Android 9+ 通常使用 "AndroidKeyStoreBCWorkaround"
// 最终会委托给 BoringSSL(Google 维护的 OpenSSL fork)
// BoringSSL 会自动检测并使用 ARMv8 AES 指令
return true; // 大多数现代设备都支持
} catch (Exception e) {
return false;
}
}

7.2 性能基准测试

public class AesBenchmark {
private static final int TEST_SIZE = 1024 * 1024; // 1MB
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);

// 预热 JIT
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 必须遵守

  1. 永远使用 GCM 模式AES/GCM/NoPadding
  2. nonce 决不重用:使用 SecureRandom.getInstanceStrong() 生成 12 字节 nonce
  3. 密钥不硬编码:使用 Android Keystore 存储密钥
  4. Salt 全局唯一:每个文件使用不同的 salt
  5. 验证认证标签:GCM 自动验证,不要捕获 AEADBadTagException 后使用错误数据
  6. 使用 HTTPS:即使内容加密,传输也应加密(纵深防御)
  7. 安全删除:加密完成后使用 File.delete() 并覆盖原文件

8.2 应该避免

  1. 不要使用 ECBAES/ECB/* 任何模式都不应使用
  2. 不要使用静态 IV:每个加密操作使用新的随机 nonce/IV
  3. 不要自己设计密码学协议:使用 NIST 标准化的模式和算法
  4. 不要在 Java 层持有密钥明文过久:用完后立即用 Arrays.fill(keyBytes, (byte) 0) 覆盖
  5. 不要信任用户提供的密码:始终使用 PBKDF2/Argon2 派生密钥
  6. 不要遗留调试日志:生产版本移除所有密钥、明文相关的日志

8.3 纵深防御

应用层加密 (AES-GCM)
+
Android Keystore (硬件保护密钥)
+
代码混淆 (ProGuard/R8)
+
完整性校验 (签名验证)
+
运行时保护 (检测 Frida/Xposed)
=
多层安全防护

九、总结

AES 文件加密在 Android 安全中的位置是数据层防护的核心。总结关键决策:

  1. 算法选择:AES-256-GCM,提供认证加密
  2. 密钥管理:Android Keystore(TEE/SE 硬件保护) + RSA 包装
  3. 大文件处理:分块加密,每块独立 nonce + AAD 序列号绑定
  4. 密钥派生:PBKDF2(内置兼容)或 Argon2(更高安全)

加密不是银弹:它保护数据在存储和传输中的机密性,但不能防止运行时的内存 dump、动态 Hook 和 Root 环境下的攻击。良好的安全架构需要纵深防御——多层防护叠加,任何一个被突破后仍有其他层保护。


参考资源

打赏
  • 微信
  • 支付宝

评论