3. Keystore 操作核心 API:拿、加载、存、读

做 Android 数据保护,绕不开 KeyStore。说白了,它就是系统给你的一套保险柜——专门放密钥的。我遇到过很多 ANR 问题,追到最后发现是 KeyStore 操作本身耗时太长,甚至卡在主线程上。所以这一章我带你把这个 API 的四个核心动作掰开揉碎:获取实例、加载、存储、读取。

3.1 获取实例:KeyStore.getInstance()

拿到 KeyStore 对象,就像去银行申请一个保险箱。

KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");

参数是类型。最常见的就两种:

类型说明我遇到的场景
AndroidKeyStoreAndroid 系统本地的硬件/软件安全环境,从 4.0 开始支持大部分生产项目都用它,密钥直接绑定设备,无法导出
PKCS12标准的 .p12 / .pfx 文件,可以导出到其他设备只在迁移或备份时用,我亲自踩过密码泄漏的坑
JKS / BKSJava 或 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。正确的做法先收集到列表,再遍历删除。

KeyStore 操作核心流程 开始 KeyStore.getInstance() 选择类型 AndroidKeyStore 无密码 PKCS12 / 传统 需要密码参数 load(null) 或 load(input, pwd) ⚠️ 异步执行 setEntry() / generateKey() 存密钥并绑定别名 getEntry() / getKey() 强转 + 判空 常见异常 • KeyStoreException(未初始化) • ClassCastException(类型强转) • android.security.KeyStoreException(硬件失败) 命名规范示例 "user_${userId}_${keyType}" 避免覆盖和冲突

3.6 避坑总结

  • 主线程:getInstance() 和 load() 可能涉及 IO 或 RPC 到 TEE,一定要异步。我曾经在 ContentProvider.onCreate() 里同步加载,直接导致系统服务 ANR。
  • 密码泄露:PKCS12 的密码不要硬编码,从 KeyStore.SecretKeyEntry 的 ProtectionParameter 里传递。
  • 密钥持久性:AndroidKeyStore 的密钥随应用卸载而删除,备份前要提前导出。
  • 硬件支持:低端机可能不支持硬件密钥,用 KeyGenParameterSpec.Builder.setIsStrongBoxBacked(false) 回退到软件实现。

掌握了这些,你就能安全地在 KeyStore 里存取密钥,避开 90% 的 ANR 和崩溃。嗯,接下来要做的就是在你的实际项目里跑一跑这些代码,踩踩我刚才说过的坑——只有自己摔过,才记得牢。