6、CryptoObject 加密认证:生成CryptoObject、使用KeyGenerator创建密钥、Cipher初始化、加密模式下的指纹认证

好,咱们继续往下走。前面几章我们把指纹认证的基本流程跑通了,但说实话,那只是「能用」的阶段。真正的生产环境里,你不可能让指纹认证裸奔——万一被中间人攻击怎么办?万一有人伪造认证结果怎么办?

这时候,CryptoObject 就派上用场了。它本质上是一个加密外壳,把指纹认证和加密操作绑定在一起。说白了,就是让系统帮你确认:「这次指纹验证通过后,我保证这个加密操作是合法的,没有被篡改过。」

我个人习惯是,只要涉及支付、登录、敏感数据解密,一律上 CryptoObject。别嫌麻烦,安全这东西,出事就是大事。

6.1 什么是 CryptoObject?为什么需要它?

先看官方定义:FingerprintManager.CryptoObject 是一个包装类,它可以包装 CipherSignatureMac 对象。当指纹认证成功后,系统会使用这个包装对象执行后续的加密操作。

你想想看,如果没有 CryptoObject,你的 App 怎么知道指纹验证的结果是真实的?虽然回调里会告诉你成功或失败,但万一有人 hook 了你的回调函数呢?

有了 CryptoObject,系统会在 TEE(可信执行环境)里完成认证和加密的绑定。指纹匹配成功的那一刻,密钥才会被释放出来。这个流程是硬件级别的,App 层根本干预不了。

核心价值:CryptoObject 保证了「指纹验证通过」和「加密操作执行」这两个事件是原子性的,不可分割。

6.2 生成 CryptoObject 的完整流程

要生成一个 CryptoObject,你需要走四步:

  1. 创建密钥——用 KeyGenerator 生成一个对称密钥,存入 Android KeyStore
  2. 初始化 Cipher——用刚才的密钥初始化 Cipher,指定加密模式
  3. 包装成 CryptoObject——把 Cipher 塞进 CryptoObject 里
  4. 发起指纹认证——把 CryptoObject 传给 FingerprintManager

嗯,这里要注意:密钥必须设置指纹认证的访问限制,否则 CryptoObject 就形同虚设了。

6.3 使用 KeyGenerator 创建密钥

Android 的密钥管理推荐用 KeyStore 系统。它把密钥存在一个安全容器里,App 只能通过 KeyStore API 使用密钥,拿不到密钥的明文。

创建密钥的代码大概长这样:

private static final String KEY_NAME = "my_fingerprint_key";
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";

private void createKey() {
    try {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
        keyStore.load(null);

        // 如果密钥已存在,先删除再重建
        if (keyStore.containsAlias(KEY_NAME)) {
            keyStore.deleteEntry(KEY_NAME);
        }

        KeyGenerator keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);

        KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
                KEY_NAME,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                .setUserAuthenticationRequired(true)  // 关键!必须指纹认证才能用
                .setInvalidatedByBiometricEnrollment(true); // 新增指纹时失效

        keyGenerator.init(builder.build());
        keyGenerator.generateKey();

    } catch (Exception e) {
        Log.e(TAG, "创建密钥失败", e);
    }
}

这里我重点说两个参数:

  • setUserAuthenticationRequired(true)——这个必须设成 true,否则密钥不需要指纹就能用,那 CryptoObject 就没意义了。
  • setInvalidatedByBiometricEnrollment(true)——用户新增指纹后,旧密钥自动失效。这是为了防止旧指纹还能继续用。我在项目中遇到过,用户换手机后旧指纹还能解锁 App,就是因为没设这个参数。

6.4 Cipher 初始化

密钥创建好了,接下来要用它初始化 Cipher。Cipher 是 Java 加密框架的核心类,负责实际的加解密操作。

private Cipher createCipher() {
    try {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
        keyStore.load(null);
        Key key = keyStore.getKey(KEY_NAME, null);

        Cipher cipher = Cipher.getInstance(
                KeyProperties.KEY_ALGORITHM_AES + "/"
                        + KeyProperties.BLOCK_MODE_CBC + "/"
                        + KeyProperties.ENCRYPTION_PADDING_PKCS7);

        // 加密模式初始化
        cipher.init(Cipher.ENCRYPT_MODE, key);

        return cipher;
    } catch (Exception e) {
        Log.e(TAG, "Cipher 初始化失败", e);
        return null;
    }
}

注意看,这里用的是 Cipher.ENCRYPT_MODE。为什么是加密模式?因为我们要用指纹认证来「授权」一次加密操作。解密模式也是可以的,但场景不同——加密模式常用于「生成一个加密结果,证明指纹验证通过」,解密模式常用于「用指纹解锁后读取敏感数据」。

小技巧:如果 Cipher 初始化时抛出了 UserNotAuthenticatedException,说明密钥需要指纹认证才能使用。这是正常的,说明你的安全设置生效了。

6.5 加密模式下的指纹认证

现在,我们把 Cipher 包装成 CryptoObject,然后发起指纹认证:

private void startFingerprintAuthWithCrypto() {
    FingerprintManager fingerprintManager = 
            (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE);

    Cipher cipher = createCipher();
    if (cipher == null) {
        // 处理初始化失败的情况
        return;
    }

    FingerprintManager.CryptoObject cryptoObject = 
            new FingerprintManager.CryptoObject(cipher);

    FingerprintManager.AuthenticationCallback callback = 
            new FingerprintManager.AuthenticationCallback() {
        @Override
        public void onAuthenticationSucceeded(
                FingerprintManager.AuthenticationResult result) {
            // 指纹认证成功!
            // 此时 CryptoObject 里的 Cipher 已经可以正常使用了
            Cipher authenticatedCipher = result.getCryptoObject().getCipher();
            // 用 authenticatedCipher 加密数据...
            Log.d(TAG, "指纹认证成功,Cipher 已就绪");
        }

        @Override
        public void onAuthenticationFailed() {
            Log.d(TAG, "指纹认证失败");
        }

        @Override
        public void onAuthenticationError(int errorCode, CharSequence errString) {
            Log.e(TAG, "指纹认证错误: " + errString);
        }
    };

    // 没有异常处理的话,这里可能会崩溃
    fingerprintManager.authenticate(cryptoObject, null, 0, callback, null);
}

这里有个坑,我曾经踩过:authenticate 方法必须在主线程调用,而且 callback 的回调也在主线程。如果你在子线程里初始化 Cipher,然后直接调 authenticate,可能会收到 IllegalStateException

警告:如果 Cipher 初始化时用了 setUserAuthenticationValidityDurationSeconds() 设置了有效期,那么在这段时间内,即使不按指纹,Cipher 也能用。这违背了「每次认证都要按指纹」的初衷。我个人建议不要设这个值,或者设成 0。

6.6 知识结构图

下面这张图把整个流程串起来了,你可以对照着看:

CryptoObject 加密认证流程 1. 创建密钥 KeyGenerator + KeyStore 2. 初始化 Cipher Cipher.ENCRYPT_MODE 3. 包装 CryptoObject new CryptoObject(cipher) 4. 发起指纹认证 authenticate(cryptoObject) 5. TEE 验证指纹 硬件级匹配 6. 释放密钥 Cipher 可正常使用 7. 加密数据 doFinal(data) 关键点 • setUserAuthenticationRequired(true) • 密钥在 KeyStore 中 • 指纹匹配成功才释放密钥 • 新增指纹后密钥失效

6.7 避坑指南

最后,我把自己踩过的坑整理一下,你遇到类似问题可以直接翻到这里:

问题 原因 解决方案
authenticate 崩溃 在子线程调用 确保在主线程调用 authenticate
Cipher 初始化失败 密钥被删除或未创建 检查 KeyStore 中是否存在密钥,不存在则重新创建
指纹认证成功但 Cipher 不能用 setUserAuthenticationRequired 未设置 创建密钥时加上这个参数
新增指纹后旧密钥还能用 setInvalidatedByBiometricEnrollment 未设置 创建密钥时加上这个参数
Android 6.0 以下崩溃 FingerprintManager 只在 API 23+ 可用 运行时检查版本,低版本走密码验证

我曾经在一个支付项目里,因为忘了设 setInvalidatedByBiometricEnrollment,导致用户换手机后旧指纹还能支付。虽然金额不大,但被安全团队通报了。从那以后,我每次创建密钥都会把这两个参数写死,形成肌肉记忆。

好了,CryptoObject 的内容就这些。你只要把密钥创建、Cipher 初始化、包装、认证这四个步骤走通,指纹加密认证就算入门了。下一章我们会聊更进阶的话题——如何在 BiometricPrompt 中使用 CryptoObject,以及如何处理多指纹场景。


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