前言
指纹解锁、面部支付、硬件密钥——这些功能表面上看起来简单(调个系统 API 弹个框),但背后涉及的安全模型很值得深挖。很多 App 的"指纹锁"其实是假锁:用户指纹通过后,App 拿一个固定的 AES key 解密数据。这个 key 写在代码里或 SharedPreferences 里,逆向就能拿到。
这一篇讲 BiometricPrompt 的正确用法,以及怎么让它跟 Android Keystore 配合做真正的"硬件级"数据保护。
一、BiometricPrompt 基本用法
class AuthManager(private val activity: FragmentActivity) {
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo
fun setup() {
val executor = ContextCompat.getMainExecutor(activity)
biometricPrompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
// 认证成功
// result.cryptoObject 可以拿到解密用的 Cipher
onAuthSuccess(result.cryptoObject)
}
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
// 认证出错或取消
// errorCode: ERROR_USER_CANCELED, ERROR_LOCKOUT 等
onAuthError(errorCode, errString)
}
override fun onAuthenticationFailed() {
// 指纹不匹配(非错误,只是没认出来)
// 会自动重试,不用手动处理
}
}
)
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("验证身份")
.setSubtitle("使用指纹或面部解锁")
.setNegativeButtonText("取消")
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG
// 可选: BIOMETRIC_WEAK, DEVICE_CREDENTIAL
)
.build()
}
fun authenticate() {
// 检查设备是否支持生物认证
val biometricManager = BiometricManager.from(activity)
val canAuth = biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG
)
when (canAuth) {
BiometricManager.BIOMETRIC_SUCCESS -> {
biometricPrompt.authenticate(promptInfo)
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
// 设备没有生物识别硬件
fallbackToPin()
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// 没有录入指纹/面部,引导去设置
promptEnrollBiometric()
}
BiometricManager.BIOMETRIC_ERROR_LOCKOUT -> {
// 尝试次数过多,锁定中(通常 30 秒)
showToast("请稍后再试")
}
}
}
}
二、Authenticators 等级
// 组合:允许强生物认证或设备 PIN
setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
// 注意:跟 DEVICE_CREDENTIAL 组合时不能用 setNegativeButtonText
// 系统会自动显示"使用 PIN"按钮
三、假锁 vs 真锁
假锁(常见错误)
// 坏例子:指纹通过后用硬编码 key 解密
val AES_KEY = "0123456789abcdef" // 逆向就能拿到
fun onAuthSuccess() {
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
cipher.init(Cipher.DECRYPT_MODE,
SecretKeySpec(AES_KEY.toByteArray(), "AES"))
val decrypted = cipher.doFinal(encryptedData)
// 这个 AES_KEY 被反编译就暴露了
}
真锁(Keystore + BiometricPrompt)
class SecureDataManager(context: Context) {
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
private val context = context
// 1. 生成跟生物认证绑定的 key
fun createBiometricKey() {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
"biometric_key_1",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// 关键:绑定生物认证
.setUserAuthenticationRequired(true)
// 关键:只接受强认证
.setUserAuthenticationParameters(
BiometricPrompt.cryptoObjectAuthenticationType
)
.setInvalidatedByBiometricEnrollment(true)
// 如果用户重新录入指纹,key 自动失效
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
}
// 2. 用这个 key 创建 Cipher,传给 BiometricPrompt
fun encryptData(data: ByteArray) {
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
val key = keyStore.getKey("biometric_key_1", null) as SecretKey
cipher.init(Cipher.ENCRYPT_MODE, key)
val iv = cipher.iv // 保存 IV
val encrypted = cipher.doFinal(data)
// 保存 encrypted + iv
}
// 3. 解密时,Cipher 要传给 BiometricPrompt
fun decryptData(encryptedData: ByteArray, iv: ByteArray) {
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
val key = keyStore.getKey("biometric_key_1", null) as SecretKey
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
// 关键:把 Cipher 传给 BiometricPrompt
// 只有指纹通过后,系统才会解锁 key,Cipher 才能用
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
biometricPrompt.authenticate(promptInfo, cryptoObject)
// 在 onAuthenticationSucceeded 回调里:
// val cipher = result.cryptoObject.cipher
// val decrypted = cipher.doFinal(encryptedData)
}
}
四、安全模型
+-------------------+ +------------------+
| BiometricPrompt | 认证通过 | Android Keystore |
| (UI + 认证) | -----> | (key 解锁) |
+-------------------+ +------------------+
|
v
+------------------+
| Cipher 初始化 |
| (key 可用) |
+------------------+
|
v
+------------------+
| 加解密数据 |
+------------------+
关键点:
Key 绑定生物认证:
setUserAuthenticationRequired(true)让 key 只在指纹通过后可用Key 存在硬件里:AndroidKeyStore 的 key 不可导出,在 TEE/StrongBox 硬件里
Cipher 绑定 CryptoObject:把 Cipher 传给 BiometricPrompt,只有认证成功后系统才解锁 key
指纹重新录入失效:
setInvalidatedByBiometricEnrollment(true)确保密钥跟指纹绑定
逆向者拿到你的 APK:
能看到
BiometricPrompt的调用代码能看到
AndroidKeyStore的 key alias但拿不到 key 本身(在硬件里)
没有指纹就无法解锁 key
重新编译 APK 也无法使用(签名变了,Keystore 不认)
五、CryptoObject 的版本兼容
// Android 10+ 支持每次认证有效期
// BiometricPrompt + CryptoObject 在 Android 9 以下有限制
// 需要用 BiometricPrompt 兼容库
fun authenticateWithCrypto(cipher: Cipher) {
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+:CryptoObject 跟认证结果绑定
biometricPrompt.authenticate(promptInfo, cryptoObject)
} else {
// Android 10 以下:需要用 setConfirmationRequired(false)
// 且 CryptoObject 可能需要额外处理
val promptInfoCompat = BiometricPrompt.PromptInfo.Builder()
.setTitle("验证身份")
.setNegativeButtonText("取消")
.setConfirmationRequired(false)
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG
)
.build()
biometricPrompt.authenticate(promptInfoCompat, cryptoObject)
}
}
六、实战:支付前验证
class PaymentActivity : FragmentActivity() {
private lateinit var biometricPrompt: BiometricPrompt
fun initiatePayment(amount: Long, payee: String) {
// 从 Keystore 取支付密钥
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val key = keyStore.getKey("payment_key", null) as? SecretKey ?: run {
createPaymentKey() // 首次创建
return initiatePayment(amount, payee)
}
// 创建签名
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(key)
val dataToSign = "$amount:$payee:${System.currentTimeMillis()}".toByteArray()
signature.update(dataToSign)
// 用 BiometricPrompt 解锁签名
val cryptoObject = BiometricPrompt.CryptoObject(signature)
biometricPrompt.authenticate(
promptInfo,
cryptoObject
)
// onAuthenticationSucceeded:
// val sig = result.cryptoObject.signature
// val signed = sig.sign()
// 发给后端验证
}
private fun createPaymentKey() {
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
"payment_key",
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyPairGenerator.initialize(spec)
keyPairGenerator.generateKeyPair()
}
}
七、错误处理对照
小结
BiometricPrompt 真正的安全保障来自跟 AndroidKeyStore 的配合。setUserAuthenticationRequired(true) + CryptoObject 是标准模式,让密钥只在认证后可用,密钥本身在硬件里不可导出。
记住一个检验标准:如果你的 AES key 写在代码里或者 SharedPreferences 里,那不管弹了什么好看的指纹 UI,都是假锁。
下篇讲多窗口与桌面模式,Android 在折叠屏和桌面形态上的适配工程。