Keystore 基本操作:生成密钥、导入密钥、获取 KeyStore 实例

说实话,很多开发天天跟 Keystore 打交道,但真正理解这三个基本操作的人并不多。我见过不少项目,一上来就照着网上的代码抄,结果运行一段时间就莫名其妙报错,最后发现是 KeyStore 初始化姿势不对。

这一节,我们就从最基础的动作开始,把获取实例、生成密钥、导入密钥这三个核心操作彻底讲清楚。你想想看,这三个操作要是没弄明白,后面异步化改造根本无从谈起。

获取 KeyStore 实例:第一步不能错

获取 KeyStore 实例,说白了就是告诉系统你要用哪种类型的密钥仓库。Android 上最常见的类型是 BKS(Bouncy Castle KeyStore)和 AndroidKeyStore。从 Android 4.3 开始,AndroidKeyStore 被官方推荐,因为它把密钥存储在了硬件隔离区域(TEE),安全性更高。

代码其实很简单:

KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null); // AndroidKeyStore 不需要输入流,传入 null 即可

嗯,这里要注意一个坑:load(null) 是 AndroidKeyStore 特有的用法。如果你用的是传统 BKS 类型,则必须传入一个包含密钥库文件的输入流。我曾经见过一个项目,用了 BKS 类型却传 null,结果运行直接抛 KeyStoreException,日志里就一行 "input stream is null",排查了大半天。

⚠️ 警告:AndroidKeyStore 在 API 28 以上才支持对称密钥(AES)的生成。如果你的 minSdk 低于 28,生成对称密钥时请改用 BKS 或使用 KeyGenParameterSpec.Builder.setIsStrongBoxBacked(true) 做兼容。

我个人习惯在项目里定义一个工具类,统一管理 KeyStore 初始化:

public static KeyStore getAndroidKeyStore() throws Exception {
    KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
    ks.load(null);
    return ks;
}

这样所有模块都调用同一个方法,万一以后要切换 KeyStore 类型,改一处就行。

生成密钥:对称与非对称,两条路

密钥生成分两种场景:一种是生成 RSA/EC 非对称密钥对,另一种是生成 AES 对称密钥。AndroidKeyStore 对这两种的支持方式不同,别搞混了。

生成非对称密钥对(RSA)

我记得第一次做指纹支付时,需要用 RSA 在 TEE 里生成密钥对,当时照着文档抄了半天,结果一直报错。后来发现是 KeyPairGenerator 的初始化参数写错了。

KeyPairGenerator kpg = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");

KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
        "my_key_alias",
        KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
        .setDigests(KeyProperties.DIGEST_SHA256)
        .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
        .setKeySize(2048)
        .build();

kpg.initialize(spec);
KeyPair keyPair = kpg.generateKeyPair();

这里有一个关键点:别名(alias)必须是唯一的。如果你用同一个别名重复生成,AndroidKeyStore 会直接覆盖已有密钥,而且不会给你任何警告。我在线上遇到过这种事故——用户设备上的旧密钥被新密钥覆盖,导致之前加密的数据全部无法解密。那叫一个惨啊。

💡 小提示:生成之前,先用 keyStore.containsAlias("my_key_alias") 检查是否已存在。如果已存在,要么复用,要么先删除再生成。

生成对称密钥(AES)

对称密钥在 AndroidKeyStore 里的生成方式稍有不同,需要用 KeyGenerator:

KeyGenerator kg = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");

KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
        "aes_key_alias",
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .setKeySize(256)
        .build();

kg.init(spec);
SecretKey key = kg.generateKey();

注意:AES 在 AndroidKeyStore 里只支持 GCM 模式 + NoPadding。如果你偏要用 CBC + PKCS7Padding,系统会直接抛异常。说白了,这是 Google 为了安全性强制限制的,别想着钻空子。

导入密钥:把外部密钥搬进 KeyStore

有些场景下,密钥不是在 Android 设备上生成的,而是从服务器下发的。这时就需要把已有的密钥导入到 KeyStore 里。对于 AndroidKeyStore,导入操作其实很受限——它不支持直接导入外部生成的私钥或对称密钥(因为要保证硬件隔离,外部导入的密钥无法确保安全)。

但如果你用的是传统 BKS KeyStore,那就可以像操作普通文件一样导入:

// 假设你已经有一个 PKCS12 文件
KeyStore ks = KeyStore.getInstance("BKS");
ks.load(context.getResources().openRawResource(R.raw.keystore), "password".toCharArray());

// 遍历原有条目,或者直接添加新条目
PrivateKey privateKey = ...;
Certificate[] chain = ...;
ks.setKeyEntry("imported_key", privateKey, "keyPassword".toCharArray(), chain);

// 保存回文件
FileOutputStream fos = new FileOutputStream("keystore.bks");
ks.store(fos, "password".toCharArray());

这里有一个我踩过的坑:BKS 的存储密码和密钥密码可以不同。但很多人在 setKeyEntry 时传了和 store 一样的密码,结果后面用的时候又传了空密码,导致 KeyStoreException: No such key。其实不是没有 key,而是密码错了。

📌 关键结论:如果你使用 AndroidKeyStore,导入密钥的唯一方式是在生成时通过 setIsStrongBoxBacked(true) 让系统自己生成,外部导入这条路基本走不通。要导入外部密钥,请改用 BKS 或 PKCS12 类型的 KeyStore。

三种操作的关系图谱

为了让你对这三个操作有一个整体的认识,我画了一张流程图,展示它们之间的逻辑关系:

KeyStore 基本操作流程 获取KeyStore实例 load(null) 初始化 AndroidKeyStore: 传 null BKS/PKCS12: 传文件流 支持外部导入? AndroidKeyStore: ❌ BKS: ✅ 生成密钥(本地) 导入密钥(外部) store() 持久化 无论生成还是导入,最终都要经过 store() 才能写入持久化存储

操作对比速查表

操作 AndroidKeyStore BKS/PKCS12 常用API类
获取实例 KeyStore.getInstance("AndroidKeyStore") KeyStore.getInstance("BKS") KeyStore
生成非对称 ❌ 不支持直接生成对称密钥 ✅ 通过 KeyPairGenerator KeyPairGenerator
生成对称 ✅ 通过 KeyGenerator(仅GCM) ✅ 通用 KeyGenerator
导入外部密钥 ❌ 不支持 ✅ setKeyEntry() KeyStore.setKeyEntry()
持久化 自动(store() 不是必须) 需显式调用 store() KeyStore.store()

几个你必须知道的细节

  • load() 要尽早调用:获取实例后如果忘记调用 load(null),后续任何操作都会抛异常。我建议把 load() 放到 Application.onCreate 里,一启动就执行。
  • 别名大小写敏感:"MyKey" 和 "mykey" 是两个不同的条目。我曾经因为大小写没对齐,在调试时浪费了半小时。
  • store() 耗时不可忽略:尤其对于 BKS,写入文件可能涉及几十毫秒甚至上百毫秒的 IO。这正是后面我们要讲异步化的起点——如果你的生成/导入操作放在 UI 线程上,那 ANR 就离你不远了。
  • AndroidKeyStore 的条目数量有限:我记得在早期某款三星手机上,最多只能存储 64 个条目,超了会直接抛异常。上限跟硬件有关,生产环境别忘了做异常捕获。
🎯 最佳实践回顾:获取实例 → load() → 检查别名是否存在 → 按需生成或复用 → 如果是 BKS 一定要调 store()。

好了,这三个基本操作到这里就讲完了。别小看它们,我见过太多线上事故都是因为这些基础操作没写对。你先把代码跑通,记住了里面的坑,后面讲异步改造时你才能从容应对。