10. Keystore访问控制:锁屏状态检测、用户认证绑定、超时策略、生物识别集成

各位同学,今天我们来聊聊Keystore里一个非常关键的话题——访问控制。说白了,就是「谁、在什么时候、能用这把钥匙」。我见过太多App,密钥是存进去了,但谁都能调出来用,那跟把密码贴在屏幕上有什么区别?

Android Keystore的访问控制,核心就四个维度:锁屏状态用户认证绑定超时策略生物识别集成。咱们一个一个拆开讲。

10.1 锁屏状态检测:密钥的「门禁」

先问一个问题:你的密钥,在手机锁屏后还能用吗?

默认情况下,Keystore生成的密钥在设备解锁后是可以用的。但有些场景——比如支付密钥、医疗数据密钥——你肯定希望它更严格:只有屏幕亮着、用户正在操作时才能用

Android提供了 KeyGenParameterSpec.Builder 里的 setUnlockedDeviceRequired(boolean) 方法。设为 true 后,只要设备锁屏,所有使用该密钥的操作都会抛出 KeyStoreException

核心逻辑:锁屏状态检测,本质上是硬件级别的门控。密钥材料在TEE(可信执行环境)里,锁屏后TEE直接拒绝任何签名/解密请求,App层根本拿不到密钥内容。

我在项目中遇到过一个问题:某个金融App要求「锁屏后立即失效」,但测试发现,从锁屏到TEE真正切断访问,有大约200ms的窗口期。嗯,这个细节后来我们通过加一个「锁屏广播监听 + 内存标记」来兜底了。

10.2 用户认证绑定:密钥只认「你」

这是Keystore访问控制里最常用的功能。你可以让一个密钥跟用户的生物特征或锁屏密码绑定——每次使用前,必须重新认证

实现方式是通过 setUserAuthenticationRequired(true)。一旦设置,密钥就像一把「指纹锁」,每次开锁都要用户按一下指纹,或者输一次锁屏密码。

KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
        "my_secure_key",
        KeyProperties.PURPOSE_SIGN)
    .setUserAuthenticationRequired(true)
    .setUserAuthenticationValidityDurationSeconds(0)  // 每次都要认证
    .build();

注意这个 setUserAuthenticationValidityDurationSeconds(0)——设为0,表示每次使用都要认证。如果设成比如30,那30秒内可以免认证使用。这个我们下一节细说。

我曾经踩过的坑setUserAuthenticationRequired(true) 的密钥,在用户没有设置锁屏密码的设备上,生成时会直接抛异常。所以生成密钥前,一定要先检查 KeyguardManager.isDeviceSecure()。否则用户手机没设密码,你的App直接崩溃,体验极差。

10.3 超时策略:认证一次,管多久?

刚才提到了 setUserAuthenticationValidityDurationSeconds,这个参数决定了「用户认证一次后,密钥可以免认证使用多长时间」。

我个人的习惯是:

  • 支付类密钥:设0,每次都要认证。安全第一,用户体验可以优化(比如用指纹代替密码)。
  • 数据加密密钥:设30-60秒。用户打开App后,认证一次,短时间内多次加解密不用反复按指纹。
  • 后台同步密钥:不要绑定用户认证。因为后台服务没法弹指纹对话框。
场景 推荐超时时间 原因
支付/签名 0秒 每次操作都需要用户明确授权
本地数据加解密 30-60秒 平衡安全与体验
后台服务 不绑定认证 无法弹窗,必须用其他方案

这里有个容易被忽略的点:超时是从用户认证成功那一刻开始算的。比如你设了30秒,用户按了指纹,30秒内密钥随便用。30秒一过,下次使用就会触发 KeyStoreException,你需要重新调用 BiometricPromptKeyguardManager 认证。

10.4 生物识别集成:指纹、人脸、虹膜

Android 9之后,Google统一了生物识别API——BiometricPrompt。Keystore跟它配合得天衣无缝。

流程其实很简单:

  1. 生成密钥时,设置 setUserAuthenticationRequired(true)
  2. 需要使用密钥时,先弹出 BiometricPrompt 让用户认证。
  3. 认证成功后,BiometricPrompt 会返回一个 CryptoObject,里面封装了 SignatureCipher 对象。
  4. 用这个 CryptoObject 去操作密钥,Keystore就知道「用户已经认证过了」,放行。
// 生成密钥时
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(...)
    .setUserAuthenticationRequired(true)
    .build();

// 使用时
BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor,
    new BiometricPrompt.AuthenticationCallback() {
        @Override
        public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
            // 拿到 CryptoObject,里面封装了签名/加密对象
            Signature signature = result.getCryptoObject().getSignature();
            // 用 signature 去签名数据
        }
    });

// 弹出认证
biometricPrompt.authenticate(
    new BiometricPrompt.CryptoObject(signature),
    new BiometricPrompt.PromptInfo.Builder()
        .setTitle("请验证指纹")
        .setNegativeButtonText("取消")
        .build()
);

小技巧BiometricPrompt 支持多种生物识别方式——指纹、人脸、虹膜。你不需要关心具体是哪种,系统会自动选择用户已注册的最佳方式。但要注意,如果用户只注册了人脸,而你的App只支持指纹,那 BiometricPrompt 会直接报错。所以建议不要限制 setAllowedAuthenticators,让系统自己判断。

10.5 知识体系总览

下面这张图,是我梳理的Keystore访问控制整体结构。你可以看到,所有访问控制最终都落到TEE里,App层只是「传话的」。

Keystore 访问控制体系 App 层:BiometricPrompt / KeyguardManager Keystore Service(系统服务) TEE(可信执行环境) 密钥材料永不离开此区域 访问控制策略 • 锁屏状态检测 • 用户认证绑定 • 超时策略 生物识别集成 • 指纹 • 人脸 • 虹膜

你看,整个体系分三层:App层只负责「弹窗」和「传参」,真正做决策的是TEE。你想想看,如果密钥材料在App层就能被读取,那所有访问控制都是摆设。Android Keystore的设计哲学就是:把安全决策下沉到硬件

10.6 避坑指南

最后,分享几个我实际踩过的坑:

  • 密钥被永久锁定:如果 setUserAuthenticationRequired(true)setInvalidatedByBiometricEnrollment(true)(默认就是true),那么用户新增或删除一个指纹/人脸时,所有绑定的密钥会立即永久失效。这是为了防止「旧指纹还能用」的安全漏洞。但很多开发者不知道,上线后用户一改指纹,数据全解密失败。所以,重要数据一定要有备份密钥或恢复机制
  • 超时时间别设太长:我曾经见过有人设24小时,说是「用户体验好」。结果手机丢了,别人在24小时内可以随便用密钥。安全产品,别贪图方便。
  • 模拟器上别测生物识别:模拟器不支持真正的TEE,很多Keystore行为跟真机不一样。我建议所有访问控制相关的测试,都在真机上跑。

好了,这一章的内容就到这里。访问控制是Keystore安全性的核心,理解了它,你就能设计出既安全又好用的密钥管理方案。


公众号:蓝海资料掘金营,微信deep3321