7、CryptoObject与加密:生成SecretKey、创建Cipher对象、将CryptoObject绑定到BiometricPrompt
各位同学,咱们今天聊点硬核的——CryptoObject。
说白了,它就是一把锁。一把只有合法生物特征(比如你的脸)才能打开的密码锁。没有它,你的App也能用指纹或人脸,但说白了就是个“皮肤识别”,跟安全两个字不沾边。我个人习惯是,只要涉及用户敏感数据(支付、登录Token、私钥),必须上CryptoObject。为什么?因为系统会帮你保证:只有本次生物认证通过后,Cipher才能正常工作。否则,就算黑客绕过了BiometricPrompt,也拿不到解密后的数据。
7.1 为什么需要CryptoObject?
你想想看,如果只是调用authenticate(),那跟“刷脸开门”没区别——门开了,但谁都能进。CryptoObject相当于在门后面加了一把保险柜。只有你的脸匹配,保险柜才会打开。
我在项目中遇到过这样一个场景:用户用指纹支付,但App被Hook了,指纹验证结果被篡改。嗯,那一次之后,我所有支付相关流程都强制绑定了CryptoObject。因为Cipher对象只在认证成功后才会解锁,这是由TEE(可信执行环境)保证的,App层根本没法伪造。
7.2 生成SecretKey
要创建Cipher,首先得有一把密钥。Android里生成对称密钥,我推荐用KeyGenerator配合KeyGenParameterSpec。注意,一定要指定setUserAuthenticationRequired(true)。否则密钥跟生物识别就没关系了。
// 生成一个只属于生物认证的AES密钥
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
"my_biometric_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true) // 关键!必须用户认证后才能用
.setInvalidatedByBiometricEnrollment(true) // 新增指纹/人脸后旧密钥失效
.build();
keyGenerator.init(spec);
SecretKey secretKey = keyGenerator.generateKey();
setInvalidatedByBiometricEnrollment(true) 这个参数很重要。如果用户新增了一个指纹或人脸,之前的密钥会自动失效。这能防止“旧指纹还能解密”的安全漏洞。我曾经因为没设这个,被安全审计怼了一顿……
7.3 创建Cipher对象
有了密钥,接下来就是创建Cipher。这里有个坑:Cipher的初始化必须在BiometricPrompt调用之前完成。因为Cipher初始化时会去KeyStore里拿密钥,如果密钥要求用户认证,此时会抛出一个KeyStoreException。别慌,这是正常的——它会在BiometricPrompt认证成功后自动解锁。
Cipher cipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
try {
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
} catch (KeyStoreException e) {
// 这里会抛异常,因为密钥需要用户认证
// 但别担心,BiometricPrompt会处理
}
嗯,这里要注意:cipher.init() 可能会抛 KeyStoreException,但不要捕获后什么都不做。正确的做法是:先尝试初始化,如果抛异常,就说明需要生物认证,然后正常启动BiometricPrompt。系统会在认证成功后,自动让Cipher变得可用。
7.4 将CryptoObject绑定到BiometricPrompt
这一步最简单,但也是最容易出错的地方。直接上代码:
BiometricPrompt biometricPrompt = new BiometricPrompt(
this,
executor,
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(
BiometricPrompt.AuthenticationResult result) {
// 认证成功!从这里获取Cipher
CryptoObject cryptoObject = result.getCryptoObject();
Cipher cipher = cryptoObject.getCipher();
// 现在可以用cipher加密/解密数据了
}
});
// 关键:把CryptoObject传进去
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("人脸验证")
.setSubtitle("请面向手机")
.setNegativeButtonText("取消")
.build();
biometricPrompt.authenticate(promptInfo,
new BiometricPrompt.CryptoObject(cipher)); // 绑定CryptoObject
onAuthenticationSucceeded里重新创建了一个Cipher,而不是用result.getCryptoObject()拿到的那个。结果呢?解密出来的数据全是乱码。记住:一定要用回调里返回的CryptoObject,因为那个Cipher才是经过TEE认证解锁的。
7.5 完整流程示意图
下面这张图,是我自己画的一个流程图,帮你理清整个调用链路:
7.6 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| onAuthenticationSucceeded里拿到的Cipher为null | 没有正确传入CryptoObject | 检查authenticate()是否传了CryptoObject |
| 解密数据乱码 | 使用了不同的Cipher实例 | 必须用result.getCryptoObject().getCipher() |
| KeyStoreException: Key user not authenticated | 密钥要求认证,但Cipher初始化时还没认证 | 这是正常现象,忽略即可,BiometricPrompt会处理 |
| 新增指纹后旧密钥失效 | setInvalidatedByBiometricEnrollment(true) | 这是安全设计,需要重新生成密钥 |
onAuthenticationSucceeded回调里,我会先判断result.getCryptoObject()是否为空。不为空才进行后续加密操作。如果为空,说明系统版本太低或者CryptoObject没传进去,这时候我会降级到不使用加密的普通生物认证(但会提示用户安全等级降低)。
好了,关于CryptoObject的绑定,核心就是这三步:生成密钥 → 创建Cipher → 绑定到BiometricPrompt。每一步都有坑,但只要你按照上面的流程走,基本不会出问题。我在好几个项目里都是这么用的,稳得很。
公众号:蓝海资料掘金营,微信deep3321