7、CryptoObject与加密:什么是CryptoObject、生成SecretKey、创建Cipher、与BiometricPrompt结合
好,咱们今天聊点硬核的。前面几章我们把BiometricPrompt的调用、回调都捋了一遍,但说实话,那只是「能用」的阶段。真正到了生产环境,你不可能让用户随便刷个脸、扫个虹膜就完事了——你得确保这次认证是安全的,认证通过后拿到的数据是加密的。
这就引出了我们今天的主角:CryptoObject。
7.1 什么是CryptoObject?
说白了,CryptoObject就是一个包装类。它把Java加密体系里的Cipher、Signature、Mac这些对象包了一层,然后交给BiometricPrompt去用。
为什么要包一层?因为Android的生物识别API在设计时考虑了一个关键问题:如何防止中间人攻击?你想想看,如果我只是调了个authenticate(),然后回调里告诉你「认证成功」,那这个结果能被信任吗?万一有人hook了你的回调呢?
嗯,CryptoObject的作用就在这里——它把加密操作和生物识别绑定在一起。只有用户当场通过了指纹/虹膜/人脸验证,CryptoObject里包裹的那个Cipher才能正常工作。换句话说,密钥的使用权,被生物特征锁住了。
核心理解:CryptoObject = 生物识别 + 加密操作的「联合认证」。缺一个都不行。
我在项目中遇到过一种情况:某个金融App要求用户每次转账前必须刷脸,刷脸通过后自动对交易数据签名。如果不使用CryptoObject,你得先刷脸,拿到一个「通过」标记,然后再去签名——这中间就有被篡改的风险。而CryptoObject把这两步合并成一步原子操作,安全等级直接拉满。
7.2 生成SecretKey
要用CryptoObject,首先你得有一把密钥。对于对称加密,我们通常用SecretKey。生成方式有很多,我个人习惯用KeyGenerator配合KeyStore,这样密钥可以安全地存储在硬件隔离区。
来看代码:
// 初始化KeyStore
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
// 生成密钥的规范
String keyName = "my_biometric_key";
KeyGenParameterSpec keyGenSpec = new KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true) // 关键:必须用户认证才能用
.setInvalidatedByBiometricEnrollment(true)
.build();
// 生成密钥
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(keyGenSpec);
SecretKey secretKey = keyGenerator.generateKey();
注意看setUserAuthenticationRequired(true)这一行。这就是我说的「密钥被生物特征锁住」的关键。没有这行,密钥随时能用,那CryptoObject就失去意义了。
我曾经踩过一个坑:第一次写的时候忘了加setInvalidatedByBiometricEnrollment(true)。结果用户新增了一个指纹后,旧密钥居然还能用。这其实是个安全隐患——理论上,用户新增生物特征后,之前的密钥应该失效,防止有人用旧特征冒充。加上这个标记后,系统会自动处理失效逻辑。
7.3 创建Cipher
密钥有了,接下来就是创建Cipher。Cipher是Java加密体系里的核心操作类,负责真正的加密/解密。
Cipher cipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
// 初始化Cipher为加密模式
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
// 加密一些数据
byte[] plaintext = "这是需要保护的数据".getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = cipher.doFinal(plaintext);
这里有个细节:cipher.init()这一步,如果密钥设置了setUserAuthenticationRequired(true),那么此时调用init()并不会报错。真正报错是在你试图用这个Cipher去加密/解密的时候——如果用户没有通过生物识别,系统会抛出一个KeyStoreException,提示「密钥需要用户认证」。
为什么会这样?因为Android的设计哲学是「延迟检查」。它不在初始化时拦截你,而是在实际使用密钥时,才去检查当前是否有有效的生物识别认证结果。这样更灵活,也避免了不必要的阻塞。
7.4 与BiometricPrompt结合
好,现在密钥和Cipher都准备好了,怎么把它们塞进BiometricPrompt里?很简单:
// 创建CryptoObject
BiometricPrompt.CryptoObject cryptoObject =
new BiometricPrompt.CryptoObject(cipher);
// 发起认证
BiometricPrompt biometricPrompt = new BiometricPrompt(
activity,
executor,
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(
BiometricPrompt.AuthenticationResult result) {
// 认证成功!此时可以从result中取出CryptoObject
BiometricPrompt.CryptoObject crypto = result.getCryptoObject();
Cipher successCipher = crypto.getCipher();
// 用这个Cipher去解密之前加密的数据
try {
byte[] decrypted = successCipher.doFinal(ciphertext);
String original = new String(decrypted, StandardCharsets.UTF_8);
// 拿到原始数据,继续业务逻辑
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
// 认证失败
}
@Override
public void onAuthenticationFailed() {
// 生物特征不匹配
}
});
// 开始认证,传入CryptoObject
biometricPrompt.authenticate(cryptoObject, cancelSignal);
注意看onAuthenticationSucceeded回调里,我通过result.getCryptoObject()取回了Cipher。这个Cipher和传入的那个是同一个对象,但此时它已经被「解锁」了——因为用户刚刚通过了生物识别验证。然后我就可以放心地用这个Cipher去解密数据了。
一个小技巧:如果你只是想做「纯认证」,不需要加解密数据,可以不传CryptoObject,直接调biometricPrompt.authenticate(cancelSignal)。但这样你就失去了「认证结果不可篡改」的保证。我个人建议,只要涉及敏感操作,一律带上CryptoObject。
7.5 整体流程梳理
说了这么多,咱们用一张图把整个流程串起来:
这张图把整个流程分成了5步:生成密钥 → 创建Cipher → 包装成CryptoObject → 发起认证 → 认证成功后取出Cipher使用。核心就是中间那个「锁定」和「解锁」的机制。
7.6 避坑指南
最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路:
- 密钥别名不要写死:我曾经把密钥别名写死在代码里,结果不同模块共用同一个密钥,导致一个模块的认证策略影响了另一个模块。建议每个业务场景用独立的密钥别名。
- Cipher的IV向量要保存:如果你用CBC模式,加密时生成的IV向量必须保存下来(比如用SharedPreferences存Base64),否则解密时拿不到IV,数据就废了。
- setInvalidatedByBiometricEnrollment(true)一定要加:这个我之前说过了,不加的话,用户新增指纹后旧密钥依然有效,存在安全隐患。
- 不要在onAuthenticationSucceeded里做耗时操作:这个回调是在主线程跑的,如果你在里面做大量解密运算,会卡UI。建议把解密操作丢到子线程去。
- Android 9以下不支持:CryptoObject在Android 9(API 28)以下也能用,但有些厂商的定制ROM可能有兼容性问题。我建议在低版本上做降级处理,比如回退到密码认证。
个人习惯:我每次写完CryptoObject相关的代码,都会做一次「负面测试」——故意不通过生物识别,直接调Cipher.doFinal(),看看会不会抛异常。如果没抛,说明密钥配置有问题,赶紧回去检查setUserAuthenticationRequired。
好了,关于CryptoObject和加密的内容就聊到这里。记住一句话:生物识别只是手段,数据安全才是目的。CryptoObject就是连接这两者的桥梁,用好它,你的App安全等级会上一个台阶。
公众号:蓝海资料掘金营,微信deep3321