6、CryptoObject 加密认证:生成CryptoObject、使用KeyGenerator创建密钥、Cipher初始化、加密模式下的指纹认证
好,咱们继续往下走。前面几章我们把指纹认证的基本流程跑通了,但说实话,那只是「能用」的阶段。真正的生产环境里,你不可能让指纹认证裸奔——万一被中间人攻击怎么办?万一有人伪造认证结果怎么办?
这时候,CryptoObject 就派上用场了。它本质上是一个加密外壳,把指纹认证和加密操作绑定在一起。说白了,就是让系统帮你确认:「这次指纹验证通过后,我保证这个加密操作是合法的,没有被篡改过。」
我个人习惯是,只要涉及支付、登录、敏感数据解密,一律上 CryptoObject。别嫌麻烦,安全这东西,出事就是大事。
6.1 什么是 CryptoObject?为什么需要它?
先看官方定义:FingerprintManager.CryptoObject 是一个包装类,它可以包装 Cipher、Signature 或 Mac 对象。当指纹认证成功后,系统会使用这个包装对象执行后续的加密操作。
你想想看,如果没有 CryptoObject,你的 App 怎么知道指纹验证的结果是真实的?虽然回调里会告诉你成功或失败,但万一有人 hook 了你的回调函数呢?
有了 CryptoObject,系统会在 TEE(可信执行环境)里完成认证和加密的绑定。指纹匹配成功的那一刻,密钥才会被释放出来。这个流程是硬件级别的,App 层根本干预不了。
核心价值:CryptoObject 保证了「指纹验证通过」和「加密操作执行」这两个事件是原子性的,不可分割。
6.2 生成 CryptoObject 的完整流程
要生成一个 CryptoObject,你需要走四步:
- 创建密钥——用 KeyGenerator 生成一个对称密钥,存入 Android KeyStore
- 初始化 Cipher——用刚才的密钥初始化 Cipher,指定加密模式
- 包装成 CryptoObject——把 Cipher 塞进 CryptoObject 里
- 发起指纹认证——把 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 知识结构图
下面这张图把整个流程串起来了,你可以对照着看:
6.7 避坑指南
最后,我把自己踩过的坑整理一下,你遇到类似问题可以直接翻到这里:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| authenticate 崩溃 | 在子线程调用 | 确保在主线程调用 authenticate |
| Cipher 初始化失败 | 密钥被删除或未创建 | 检查 KeyStore 中是否存在密钥,不存在则重新创建 |
| 指纹认证成功但 Cipher 不能用 | setUserAuthenticationRequired 未设置 | 创建密钥时加上这个参数 |
| 新增指纹后旧密钥还能用 | setInvalidatedByBiometricEnrollment 未设置 | 创建密钥时加上这个参数 |
| Android 6.0 以下崩溃 | FingerprintManager 只在 API 23+ 可用 | 运行时检查版本,低版本走密码验证 |
我曾经在一个支付项目里,因为忘了设 setInvalidatedByBiometricEnrollment,导致用户换手机后旧指纹还能支付。虽然金额不大,但被安全团队通报了。从那以后,我每次创建密钥都会把这两个参数写死,形成肌肉记忆。
好了,CryptoObject 的内容就这些。你只要把密钥创建、Cipher 初始化、包装、认证这四个步骤走通,指纹加密认证就算入门了。下一章我们会聊更进阶的话题——如何在 BiometricPrompt 中使用 CryptoObject,以及如何处理多指纹场景。