Administrator
发布于 2026-09-22 / 5,563 阅读
0
0

Android 生物认证:BiometricPrompt 与本地数据保护

前言

指纹解锁、面部支付、硬件密钥——这些功能表面上看起来简单(调个系统 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 等级

等级

常量

说明

安全性

强认证

BIOMETRIC_STRONG

指纹/3D 面部

活体检测,不可伪造

弱认证

BIOMETRIC_WEAK

2D 面部识别

可能被照片骗过

设备凭证

DEVICE_CREDENTIAL

PIN/密码/图案

知识型认证

// 组合:允许强生物认证或设备 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
                              +------------------+
                              | 加解密数据        |
                              +------------------+

关键点:

  1. Key 绑定生物认证:setUserAuthenticationRequired(true) 让 key 只在指纹通过后可用

  2. Key 存在硬件里:AndroidKeyStore 的 key 不可导出,在 TEE/StrongBox 硬件里

  3. Cipher 绑定 CryptoObject:把 Cipher 传给 BiometricPrompt,只有认证成功后系统才解锁 key

  4. 指纹重新录入失效: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()
    }
}

七、错误处理对照

错误码

含义

处理建议

ERROR_USER_CANCELED

用户主动取消

不提示,静默关闭

ERROR_NEGATIVE_BUTTON

用户点"取消"

不提示

ERROR_LOCKOUT

尝试 5 次失败,锁 30 秒

提示"稍后再试"

ERROR_LOCKOUT_PERMANENT

锁太多次,需解锁屏幕

引导锁屏解锁

ERROR_HW_NOT_PRESENT

无硬件

fallback 到 PIN

ERROR_NO_BIOMETRICS

没录入指纹

引导去设置

ERROR_NO_DEVICE_CREDENTIAL

没设 PIN/密码

引导去设置

小结

BiometricPrompt 真正的安全保障来自跟 AndroidKeyStore 的配合。setUserAuthenticationRequired(true) + CryptoObject 是标准模式,让密钥只在认证后可用,密钥本身在硬件里不可导出。

记住一个检验标准:如果你的 AES key 写在代码里或者 SharedPreferences 里,那不管弹了什么好看的指纹 UI,都是假锁。

下篇讲多窗口与桌面模式,Android 在折叠屏和桌面形态上的适配工程。


评论