4. Android KeyStore 系统:密钥到底该放哪?

聊到密码SDK,逃不开的一个话题就是密钥存储。Android从4.0(API 14)就引入了KeyStore,但说实话,早期版本就是个软件实现,跟随便写个文件存一下差别不大。真正让KeyStore变“硬”的是Android 6.0以后引入的硬件支持。我自己做第一个金融SDK时,踩过不少KeyStore的坑,今天把核心内容一次讲透。

4.1 KeyStore 提供程序:Android自己的密钥保险柜

Android KeyStore不是一个新的KeyStore类型,它其实是一个Provider——AndroidKeyStoreProvider。你平时用Java标准库的KeyStore.getInstance("AndroidKeyStore"),拿到的就是这个Provider的实例。

它的特点很直接:

  • 密钥不可导出:一旦存入,应用层拿不到密钥的明文。
  • 绑定应用身份:每个应用的KeyStore空间隔离,别的应用读不了。
  • 支持加密算法:AES, RSA, EC等,且硬件支持时使用硬件加密引擎。
小提示:我记得早期项目里有人直接用KeyStore.getInstance("JKS")存密钥,结果被逆向工具直接拖走。用AndroidKeyStore,至少密钥不会明文出现在APK或data分区里。

4.2 密钥生成与存储:不是new一个对象那么简单

密钥生成要用KeyPairGenerator或KeyGenerator,但需要指定KeyStore作为数据存储目标。这里我习惯用KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"),第二个参数告诉系统用KeyStore的Provider。

看一个实际的RSA密钥对生成例子:

KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
        "my_rsa_key_pair",
        KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
        .setKeyValidityForOriginationEnd(
                Calendar.getInstance().getTimeInMillis() + 1000L * 60 * 60 * 24 * 365);
kpg.initialize(builder.build());
KeyPair keyPair = kpg.generateKeyPair();

这里有个关键点:KeyGenParameterSpec配置了用途、摘要、有效期等。为什么要设有效期?我在一个项目中见过,密钥永不过期,结果用户换了SIM卡、设备重刷后,旧密钥还能解密,这不安全。

注意:密钥别存太久。建议至少设置setKeyValidityForOriginationEnd,甚至配合生物识别使用setUserAuthenticationRequired(true)。否则,密钥被恢复出厂设置后还能用,就等于白给了攻击者一个后门。

存储之后,读取也用KeyStore实例:

KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
PrivateKey privateKey = (PrivateKey) ks.getKey("my_rsa_key_pair", null);
PublicKey publicKey = ks.getCertificate("my_rsa_key_pair").getPublicKey();

你看,根本不需要密码或KeyStore口令——因为KeyStore绑定的是应用UID,系统自动管理。我当初写第一个版本时,还多传了个null的password进去,其实没用。

4.3 硬件支持:TEE、StrongBox,以及芯片级的保障

说到硬件支持,这是Android KeyStore真正的王牌。从Android 6.0起,系统可以利用TEE(可信执行环境)来做密钥操作;到了Android 9,又引入了StrongBox KeyStore——一个独立的、有自己CPU和RAM的安全芯片。

不同硬件安全级别对比
安全级别 说明 性能 典型平台
软件实现(无硬件) 密钥在Android系统内存中操作,容易被HOOK或读取 快 模拟器、低端老设备
TEE(如ARM TrustZone) 密钥操作在独立安全OS中,主系统没法直接访问 中 大多数中高端Android手机
StrongBox(独立安全芯片) 有独立的CPU、RAM、RNG,抗物理攻击更强 慢(约软件方案的1/10) Pixel系列、部分高端旗舰

你可能会问:“那我怎么知道设备支持什么级别?” 可以用KeyStore.getInstance().isHardwareBacked(),但这个API已废弃。更靠谱的做法是通过Context.getSystemService(KeyStoreManager.class)的isHardwareBacked()方法,或者检查设备是否声明了android.hardware.strongbox_keystore功能。

我曾经在一款国产机上发现,明明SOC是支持TEE的,但厂商没给KeyStore驱动打补丁,结果密钥操作回退到软件实现。这事很难从代码层面判断,建议SDK在初始化时做一个sign + verify循环基准测试,如果耗时异常低(比如低于10ms),可能没走硬件。

4.4 从生成到使用的完整流程

Android KeyStore 密钥生命周期 应用初始化KeyStore实例 生成密钥对(KeyPair) 检查硬件支持级别 StrongBox安全芯片 TEE可信执行环境 软件降级实现 密钥存入KeyStore sign / encrypt 操作 密钥销毁/过期 硬件支持可能被厂商阉割 建议运行时检测回退策略

这张图把密钥从生成到销毁走了一遍。重点看“检查硬件支持级别”这一步——很多SDK开发者忽略了,直接假设设备一定支持StrongBox。结果在千元机上密钥操作奇慢(因为StrongBox响应太慢),或者根本不工作。我建议你先尝试创建带setIsStrongBoxBacked(true)的密钥,如果抛出StrongBoxUnavailableException,再降级为TEE,最后才是纯软件实现。

4.5 开发者必须避开的三个坑

  1. 密钥别名冲突:同一个KeyStore不许重复生成同名密钥,否则抛异常。我习惯在生成前先ks.containsAlias("key_name")检查一下,如果已存在就直接使用。
  2. KeyGenParameterSpec的setEncryptionPaddings:RSA加密如果不设setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP),默认可能是PKCS1Padding,这在某些硬件(尤其是Smartcard)上不支持。曾经有一次因为这个导致合作银行的测试机验签失败,查了两天才发现是填充模式不匹配。
  3. 用户认证绑定:如果你用了setUserAuthenticationRequired(true),密钥在用户锁定屏幕后会被清空,不过只有下次解锁才能再次使用。注意:如果你同时设置了setUserAuthenticationValidityDurationSeconds(-1),那就只验证一次,直到设备重启。这在密码SDK中用于“记住登录状态”很常见,但要配合KeyguardManager的API使用。
个人经验:我见过最坑的是某厂商的TEE实现有bug,RSA签名每次返回的结果都不一样(实际上签名是随机的)。后来我去查了厂商的安全补丁列表,发现需要至少2018年5月后的安全补丁才能修复。所以SDK里最好加一个签名自校验的测试——生成密钥后立刻签一个固定消息,验签成功再对外暴露接口。

4.6 硬件支持判定代码片段

最后给你一段实用的判断代码,直接Copy到你的初始化逻辑里:

private boolean isStrongBoxSupported(Context context) {
    PackageManager pm = context.getPackageManager();
    if (pm.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
        return true;
    }
    // 回退检测:尝试创建StrongBox密钥
    try {
        KeyGenerator keyGen = KeyGenerator.getInstance("AES", "AndroidKeyStore");
        KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
                "_test_strongbox_key",
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .setIsStrongBoxBacked(true)
                .build();
        keyGen.init(spec);
        keyGen.generateKey();
        KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
        ks.load(null);
        ks.deleteEntry("_test_strongbox_key");
        return true;
    } catch (StrongBoxUnavailableException | ProviderException | NoSuchAlgorithmException |
             NoSuchProviderException | InvalidAlgorithmParameterException | 
             KeyStoreException | CertificateException | IOException e) {
        return false;
    }
}

这段代码我放进了SDK的onCreate里,只跑一次,同时把结果缓存到一个SharedPreference中。注意:setIsStrongBoxBacked只在API 28+有效,低版本直接跳过StrongBox检测。

嗯,Android KeyStore的内容就这么多。其实记住两点就行:密钥别明文存、硬件降级要有日志。你想想看,如果某天用户反馈“我的密钥被黑了”,至少你能从日志里知道当时是用StrongBox还是软件实现,定位问题就快得多。