Android 加密 API 概述:常用加密类与方法
做 Android 加密,绕不开 Java Cryptography Architecture,也就是 JCA。我刚开始接触这块的时候,以为只是调几个 API 那么简单……后来踩过坑才明白,选对类、选对模式,直接决定了你的应用是安全稳定,还是 ANR 频发。
JCA 与 Android 加密体系
JCA 是一套标准加密框架,Android 从 1.0 就开始支持。它提供了统一的接口,底层却可以替换不同的加密 Provider。说白了,你写代码时只关心“用什么算法”,而不需要关心“这个算法是哪个厂商实现的”。
我个人习惯优先用 Android 原生提供的 AndroidKeyStore Provider,因为它能把密钥藏在硬件安全模块里,应用进程被 dump 也拿不到密钥材料。
| 核心类 | 用途 | 常见问题 |
|---|---|---|
Cipher |
执行加密/解密操作 | 初始化耗时,阻塞 UI |
KeyGenerator |
生成对称密钥 | 随机数源可能变慢 |
KeyStore |
存储和管理密钥 | 首次访问需解锁,ANR 常见 |
SecretKeySpec |
从字节数组构建密钥 | 密钥硬编码风险 |
IvParameterSpec |
提供初始化向量 | IV 重复导致安全漏洞 |
常用加密算法与模式选择
Android 加密 API 支持很多算法,但我项目中真正推荐的只有两套:AES/GCM/NoPadding 和 RSA/ECB/OAEPWithSHA-256AndMGF1Padding。为什么?GCM 模式自带认证,能防止密文被篡改;OAEP 填充能抵抗选择密文攻击。
加密操作与 ANR 的关系
你想想看,加密是 CPU 密集型计算。AES-256 加密 1MB 数据,在低端设备上可能要 30~50ms;如果数据过大或者密钥生成过于复杂,阻塞主线程超过 5 秒就会触发 ANR。我遇到过最夸张的一次,是同事直接在 Compose 的 LaunchedEffect 里调 Cipher.init() 加密用户输入的密码——结果每次输入都卡顿,还时不时 ANR。
withContext(Dispatchers.IO) 或者 rememberCoroutineScope 配合 async 都可以。代码示例:安全加密一段文本
suspend fun encryptData(context: Context, plainText: String): ByteArray {
return withContext(Dispatchers.IO) {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
// 如果密钥不存在则生成
if (!keyStore.containsAlias("my_aes_key")) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(
"my_aes_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
)
keyGenerator.generateKey()
}
val secretKey = (keyStore.getEntry("my_aes_key", null) as KeyStore.SecretKeyEntry).secretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
// 获取 IV,解密时需要用
val iv = cipher.iv
val encrypted = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
// 将 IV 拼接在密文前面一起返回
ByteArray(iv.size + encrypted.size).apply {
System.arraycopy(iv, 0, this, 0, iv.size)
System.arraycopy(encrypted, 0, this, iv.size, encrypted.size)
}
}
}
注意这里用了 withContext(Dispatchers.IO) 把整个加密逻辑切到 IO 线程池。Cipher 初始化时如果密钥在硬件安全模块中,还可能需要用户授权,那调用本身就是异步的——但即便如此,也不要在主线程等。
密钥存储与 AndroidKeyStore
AndroidKeyStore 从 4.3 开始引入,到了 6.0 以后支持硬件-backed 的密钥。把密钥存到 KeyStore 里,其他应用无法读取,root 设备也可能读不到(取决于安全芯片实现)。这是保护密钥最可靠的方式。
我曾经遇到过一个坑:在 Android 5.0 设备上使用 KeyStore 的 getEntry() 可能会阻塞主线程几百毫秒。原因是在低版本上 KeyStore 没有做异步优化。解决办法也很简单——把 KeyStore 的 load() 和 getEntry() 都放在后台线程。
最佳实践总结:
- 使用
AndroidKeyStore存储密钥,不要硬编码或存 SharedPreferences - 对称加密首选 AES/GCM/NoPadding,非对称加密首选 RSA/OAEP
Cipher.init()和doFinal()必须在 IO/CPU 密集型线程执行- 每次加密生成新的随机 IV,并将 IV 拼接在密文前
- 密钥别名不要用可预测的字符串,最好加版本号和应用标识
常见陷阱与避坑指南
- Cipher 实例重复利用:不要每次都
Cipher.getInstance(),这会触发 Provider 扫描,耗时不稳定。创建一次实例后可以缓存,但注意每次加密前要init()重新设置模式。 - 密钥过期:用
KeyGenParameterSpec.Builder.setKeyValidityStart/End可以限制密钥有效期,防止密钥泄露后长期可用。但要注意时间戳要与设备本地时间对齐,用户调时间可能导致密钥立即失效。 - 加密结果太大导致 OOM:我测试过用
Cipher.doFinal()加密 10MB 数据,堆内存瞬间飙升十几 MB。可以用Cipher.update()分批处理,或者直接限制单次加密数据量(比如 1MB 以内)。 - IV 重复:GCM 模式下,IV 重复会直接导致密钥泄露。每次加密务必生成新的随机 IV,不要复用。我见过有人把 IV 写死成 12 个零字节……那加密等于形同虚设。
ViewModel 的 viewModelScope 里管理加密协程。这样即使屏幕旋转,加密任务也不会被取消,还能配合 snapshotFlow 把加密结果安全地发回 UI。