3. Keystore 操作核心 API:拿、加载、存、读
做 Android 数据保护,绕不开 KeyStore。说白了,它就是系统给你的一套保险柜——专门放密钥的。我遇到过很多 ANR 问题,追到最后发现是 KeyStore 操作本身耗时太长,甚至卡在主线程上。所以这一章我带你把这个 API 的四个核心动作掰开揉碎:获取实例、加载、存储、读取。
3.1 获取实例:KeyStore.getInstance()
拿到 KeyStore 对象,就像去银行申请一个保险箱。
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
参数是类型。最常见的就两种:
| 类型 | 说明 | 我遇到的场景 |
|---|---|---|
| AndroidKeyStore | Android 系统本地的硬件/软件安全环境,从 4.0 开始支持 | 大部分生产项目都用它,密钥直接绑定设备,无法导出 |
| PKCS12 | 标准的 .p12 / .pfx 文件,可以导出到其他设备 | 只在迁移或备份时用,我亲自踩过密码泄漏的坑 |
| JKS / BKS | Java 或 BouncyCastle 的密钥库 | 兼容旧系统,但 Android 上不推荐,容易 ANR |
我个人习惯用 AndroidKeyStore,因为它把密钥生命周期交给系统管理,应用本身拿不到明文,安全性高很多。除非你有跨平台导出需求,否则别选 PKCS12——密码管理稍不注意就变成明文硬编码。
3.2 load():开门,但别在主线程上开
拿到钥匙柜之后要开锁:
keyStore.load(null); // AndroidKeyStore 不需要密码,传 null
如果你用 PKCS12,那得传密码:
keyStore.load(new FileInputStream("keystore.p12"), "myPassword".toCharArray());
但注意,load() 是一个 可能阻塞的操作。尤其在 AndroidKeyStore 首次创建时,系统得与 TEE(可信执行环境)通信,有时会卡几十毫秒甚至几百毫秒。我曾经在启动页的 onCreate() 里直接调 load(),结果满屏幕白了好几秒,差点被测试打死。
避坑指南:千万不要在主线程调用 load()。我后来改成在 Application.onCreate() 里用 AsyncTask 或 Coroutine 做预加载,才彻底告别这个 ANR。
另外一个坑:load(null) 之后你不能立刻知道密钥库是否初始化成功——它不会抛异常,除非你后面操作时报“KeyStore not initialized”。所以我在项目里加了一段校验:
try {
keyStore.load(null);
// 马上试一下 containsAlias,触发内部初始化
keyStore.containsAlias("test");
} catch (KeyStoreException e) {
// 初始化失败,重试或降级
}
3.3 存储密钥:塞进保险柜
存密钥用 setEntry()。我最常用的是对称密钥(AES),场景如加密本地数据库。
KeyGenerator keyGen = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGen.init(
new KeyGenParameterSpec.Builder("my_secret_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build());
SecretKey secretKey = keyGen.generateKey();
// 注意:这时密钥已经自动存进 KeyStore 了
// 因为是通过 KeyStore 生成的,系统会自动绑定
如果你是外部导入的密钥,可以用 SecretKeyEntry:
SecretKeyEntry entry = new SecretKeyEntry(myExternalKey);
keyStore.setEntry("imported_key", entry, null); // 第三个参数是 ProtectionParameter,AndroidKeyStore 传 null
存的时候系统会检查别名是否已存在——如果重复,会直接覆盖。我曾经因为忘记检查,把之前生成的一把密钥覆盖了,结果那个用户的所有加密数据都解不开了……必须手动清理数据。所以我现在的习惯:存之前先 containsAlias() 做判断,或者用带前缀的命名规则,避免冲突。
3.4 读取密钥:取出来用
读密钥用 getEntry():
KeyStore.Entry entry = keyStore.getEntry("my_secret_key", null);
if (entry instanceof KeyStore.SecretKeyEntry) {
SecretKey secretKey = ((KeyStore.SecretKeyEntry) entry).getSecretKey();
// 开始加密/解密
}
注意:getEntry() 返回的是 KeyStore.Entry 基类,需要强转。这个转换我经常看到新人忘记判空导致 ClassCastException。你想想看,万一别名不存在,getEntry() 返回 null,强转直接就崩溃。所以我个人习惯先 containsAlias() 再取:
if (keyStore.containsAlias("my_secret_key")) {
SecretKey key = (SecretKey) keyStore.getKey("my_secret_key", null);
// ... 处理
}
如果密钥是 受硬件保护的(AndroidKeyStore 默认行为),那么 getEntry() 返回的你只能拿到一个 引用,而不是明文密钥。这一点常常让新人不适应——明明取出来了,但 key.getEncoded() 返回 null。嗯,这就是安全设计:密钥明文永远不会进入应用进程。
关键点:AndroidKeyStore 中,getKey() 返回的 SecretKey 实现是 KeyStoreKey,它的 getEncoded() 恒为 null。别尝试用它的字节数组去做别的密钥派生——老老实实用它提供的加密/解密方法。
3.5 别名管理与删除
除了存和读,日常还要维护别名列表。我常用:
aliases():遍历所有存储的密钥别名size():查看当前密钥数量deleteEntry():删除某个密钥(小心!删除后对应加密数据永久失效)
举个例子,用户注销账号时,我一般会清空 KeyStore 里所有关联密钥:
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (alias.startsWith("user_" + userId)) {
keyStore.deleteEntry(alias);
}
}
这里我踩过另一个坑:aliases() 返回的枚举遍历过程中如果删除当前元素,某些实现会抛 ConcurrentModificationException。正确的做法先收集到列表,再遍历删除。
3.6 避坑总结
- 主线程:
getInstance()和load()可能涉及 IO 或 RPC 到 TEE,一定要异步。我曾经在ContentProvider.onCreate()里同步加载,直接导致系统服务 ANR。 - 密码泄露:PKCS12 的密码不要硬编码,从
KeyStore.SecretKeyEntry的ProtectionParameter里传递。 - 密钥持久性:AndroidKeyStore 的密钥随应用卸载而删除,备份前要提前导出。
- 硬件支持:低端机可能不支持硬件密钥,用
KeyGenParameterSpec.Builder.setIsStrongBoxBacked(false)回退到软件实现。
掌握了这些,你就能安全地在 KeyStore 里存取密钥,避开 90% 的 ANR 和崩溃。嗯,接下来要做的就是在你的实际项目里跑一跑这些代码,踩踩我刚才说过的坑——只有自己摔过,才记得牢。