Android 生物识别验证

2,394 阅读3分钟

现在的App中,通过识别人脸或者指纹来验证身份,实现登录、支付、加解密信息等功能已经十分常见。本篇文章介绍下Android端如何通过androidx.biometric库实现生物识别验证。

官方文档

androidx.biometric

使用androidx.biometric,开发者不需要根据SDK Version判断使用FingerPrintManger还是BiometricPrompt,并且该库提供了一致标准的UI,所以开发者不需要自己实现认证UI。

添加依赖库

在项目app module的build.gradle中的dependencies中添加依赖:

dependencies {
    // java
    implementation("androidx.biometric:biometric:1.1.0")

    // Kotlin
    implementation("androidx.biometric:biometric:1.2.0-alpha04")
}

实现生物识别验证

1. 检测生物识别是否可用

使用生物识别之前,先判断当前的设备是否支持,代码如下:

val biometricManager = BiometricManager.from(this)
biometricManager.run {
    // 如果可以允许用户不使用生物识别而是密码,可以设置DEVICE_CREDENTIAL
    val allowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
    when (canAuthenticate(allowedAuthenticators)) {
        BiometricManager.BIOMETRIC_SUCCESS -> {
            // 可以使用生物识别
        }
        BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
            // 设备没有相应的硬件
        }
        BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
            // 生物识别当前不可以用
        }
        BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
            // 没有录入相应的生物识别信息(指纹或者人脸)
            // androidx.biometric提供的录入生物信息的API仅在Android R(30)以上有效
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
                    putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, allowedAuthenticators)
                }
                startActivityForResult(enrollIntent)
            } else {
                // 创建弹窗提示用户录入生物信息(指纹或者人脸),进入设置页面
                AlertDialog.Builder(this@BiometricActivity)
                    .setTitle("Create credentials")
                    .setMessage("Record fingerprints to log into the ExampleDemo")
                    .setPositiveButton("ok") { _, _ -> 
                        val intent = Intent().apply {
                            action = Intent.ACTION_VIEW
                            component = ComponentName("com.android.settings", "com.android.settings.Settings")
                        }
                        startActivityForResult(intent)
                    }
                    .setNegativeButton("not now", null)
                    .show()
            }
        }
        BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
            // 当设备的Android版本等于或小于Q
            // 并且allowedAuthenticators设定为BIOMETRIC_STRONG or DEVICE_CREDENTIAL时,回调此方法
            // 需要通过其他API判断设备是否支持生物识别,因此当设备的Android版本等于或小于Q时
            // allowedAuthenticators建议设置为BIOMETRIC_STRONG、BIOMETRIC_STRONG或 BIOMETRIC_WEAK or DEVICE_CREDENTIAL
        }
        else -> {}
    }
}

2. 调用生物识别

设备支持生物识别的情况下,通过如下代码调用生物识别:

val biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this), object : BiometricPrompt.AuthenticationCallback() {
    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
        super.onAuthenticationSucceeded(result)
        // 验证通过,可以通过result.authenticationType获取使用的生物识别类型。
    }

    override fun onAuthenticationFailed() {
        super.onAuthenticationFailed()
        // 验证失败
    }

    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
        super.onAuthenticationError(errorCode, errString)
        // 验证错误
    }
})
val promptInfo = BiometricPrompt.PromptInfo.Builder().run {
    setTitle("Biometric login for ExampleDemo")
    setSubtitle("Login using your biometric credential")
    // 这里的allowedAuthenticators与检测生物识别是否可用步骤中的配置一样
    setAllowedAuthenticators(allowedAuthenticators)
    // 需要注意,allowedAuthenticators如果没有配置DEVICE_CREDENTIAL
    // 则需要配置NegativeButtonText,否则会抛出异常 IllegalArgumentException("Negative text must be set and non-empty.")
    // 反之不可配置NegativeButtonText,否则会抛出异常 IllegalArgumentException("Negative text must not be set if device credential authentication is allowed.")
    if (allowedAuthenticators and DEVICE_CREDENTIAL == 0) {
        setNegativeButtonText("use other login")
    }
    build()
}
        
biometricPrompt.authenticate(promptInfo)

通过生物识别验证加解密

如果需要在生物识别验证通过后加解密信息,可以通过CryptoObject来实现,CryptoObject支持以下加密对象:SignatureCipher和 Mac

跟官方文档一样,我使用的是Cipher进行测试,在调用生物识别时进行一些调整即可,代码如下:

注意:如果要调用加密,则promptInfo的allowedAuthenticators不能为BIOMETRIC_WEAK,否测会抛出异常

// 获取或者生成秘钥
fun getOrGenerateSecretKey(keyName: String): SecretKey {
    try {
        val keyStore = KeyStore.getInstance("AndroidKeyStore")
        keyStore.load(null)
        keyStore.getKey(keyName, null)?.let {
            return it as SecretKey
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
    val keyGenParameterSpec = KeyGenParameterSpec.Builder(keyName, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT).run {
        setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        setUserAuthenticationRequired(true)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            // 在Android版本为N(24)以上时,可以使用此API
            // true 表示用户录入新的生物识别信息后使当前的秘钥无效
            setInvalidatedByBiometricEnrollment(false)
        }
        build()
    }
    val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    keyGenerator.init(keyGenParameterSpec)
    return keyGenerator.generateKey()
}

// 调用加密
val biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this), object : BiometricPrompt.AuthenticationCallback() {
    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
        super.onAuthenticationSucceeded(result)
        result.cryptoObject?.cipher?.run {
            // 加密处理
            val encryptByteArray = doFinal(“encrypt message”.toByteArray(Charset.defaultCharset()))
        }
    }
})
val encryptCipher = Cipher.getInstance("${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}")
encryptCipher.init(Cipher.ENCRYPT_MODE, getOrGenerateSecretKey(keyName))
// 生成解密密钥时需要用到ivParameterSpec
val ivParameterSpec = encryptCipher.parameters.getParameterSpec(IvParameterSpec::class.java)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(encryptCipher))

// 调用解密
val biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this), object : BiometricPrompt.AuthenticationCallback() {
    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
        super.onAuthenticationSucceeded(result)
        result.cryptoObject?.cipher?.run {
            // 解密处理
            doFinal(encryptByteArray)
        }
    }
})
val decryptCipher = Cipher.getInstance("${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}")
decryptCipher.init(Cipher.DECRYPT_MODE, getOrGenerateSecretKey(keyName), ivParameterSpec)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(decryptCipher))

示例

将2部分整合做了个示例,代码如下:

@RequiresApi(Build.VERSION_CODES.M)
object CryptographyManager {

    private const val ANDROID_KEYSTORE = "AndroidKeyStore"

    private const val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
    private const val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
    private const val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
    private var ivParameterSpec: IvParameterSpec? = null

    private fun getCipher(): Cipher {
        return Cipher.getInstance("$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING")
    }

    fun getEncryptCipher(keyName: String): Cipher {
        val cipher = getCipher()
        cipher.init(Cipher.ENCRYPT_MODE, getOrGenerateSecretKey(keyName))
        ivParameterSpec = cipher.parameters.getParameterSpec(IvParameterSpec::class.java)
        return cipher
    }

    fun getDecryptCipher(keyName: String): Cipher? {
        ivParameterSpec?.let { it ->
            val cipher = getCipher()
            cipher.init(Cipher.DECRYPT_MODE, getOrGenerateSecretKey(keyName), it)
            return cipher
        }
        return null
    }

    fun deleteKey(keyName: String) {
        try {
            val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
            keyStore.load(null)
            keyStore.deleteEntry(keyName)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun getOrGenerateSecretKey(keyName: String): SecretKey {
        try {
            val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
            keyStore.load(null)
            keyStore.getKey(keyName, null)?.let {
                return it as SecretKey
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        val keyGenParameterSpec = KeyGenParameterSpec.Builder(keyName, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT).run {
            setBlockModes(ENCRYPTION_BLOCK_MODE)
            setEncryptionPaddings(ENCRYPTION_PADDING)
            setUserAuthenticationRequired(true)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                setInvalidatedByBiometricEnrollment(false)
            }
            build()
        }
        val keyGenerator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, ANDROID_KEYSTORE)
        keyGenerator.init(keyGenParameterSpec)
        return keyGenerator.generateKey()
    }
}

class BiometricActivity : AppCompatActivity() {

    private val forActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
        checkBiometricAuthenticate()
    }

    private lateinit var binding: LayoutBiometricActivityBinding

    private var biometricManager: BiometricManager? = null
    private var biometricPrompt: BiometricPrompt? = null
    private var promptInfo: BiometricPrompt.PromptInfo? = null

    private val keyName = "ExampleDemoKey"
    private var encrypt: Boolean = false
    private var encryptedInfo: ByteArray? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.layout_biometric_activity)
        checkBiometricAuthenticate()
        binding.btnBiometric.setOnClickListener {
            biometricPrompt?.run { promptInfo?.let { authenticate(it) } }
        }
        binding.btnEncrypt.setOnClickListener {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                biometricPrompt?.run {
                    promptInfo?.let {
                        encrypt = true
                        authenticate(it, BiometricPrompt.CryptoObject(CryptographyManager.getEncryptCipher(keyName)))
                    }
                }
            }
        }
        binding.btnDecrypt.setOnClickListener {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                biometricPrompt?.run {
                    promptInfo?.let {
                        encrypt = false
                        CryptographyManager.getDecryptCipher(keyName)?.let { cipher -> authenticate(it, BiometricPrompt.CryptoObject(cipher)) }
                    }
                }
            }
        }
    }

    private fun checkBiometricAuthenticate() {
        if (biometricManager == null) {
            biometricManager = BiometricManager.from(this)
        }
        biometricManager?.run {
            val allowedAuthenticators = BIOMETRIC_STRONG
            when (canAuthenticate(allowedAuthenticators)) {
                BiometricManager.BIOMETRIC_SUCCESS -> {
                    initBiometric(allowedAuthenticators)
                }
                BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
                    // 设备没有相应的硬件
                }
                BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
                    // 生物识别当前不可以用
                }
                BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                        val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
                            putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, allowedAuthenticators)
                        }
                        forActivityResultLauncher.launch(enrollIntent)
                    } else {
                        //创建弹窗提示用户录入生物信息(指纹或者人脸),进入设置页面
                        AlertDialog.Builder(this@BiometricActivity)
                            .setTitle("Create credentials")
                            .setMessage("Record fingerprints to log into the ExampleDemo")
                            .setPositiveButton("ok") { _, _ -> gotoSettings() }
                            .setNegativeButton("not now", null)
                            .show()
                    }
                }
                else -> {}
            }
        }
    }

    private fun initBiometric(allowedAuthenticators: Int) {
        biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this), object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                super.onAuthenticationSucceeded(result)
                result.cryptoObject?.cipher?.run {
                    binding.etInputData.text?.let {
                        if (encrypt) {
                            val encryptByteArray = doFinal(it.toString().toByteArray(Charset.defaultCharset()))
                            binding.tvEncryptData.run { post { text = "Encrypt Data : ${Arrays.toString(encryptByteArray)} " } }
                            encryptedInfo = encryptByteArray
                        } else {
                            binding.tvDecryptData.run { post { text = "Decrypt Data : ${String(doFinal(encryptedInfo))}" } }
                        }
                    }
                }
            }

            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
            }

            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
            }
        })
        promptInfo = BiometricPrompt.PromptInfo.Builder().run {
            setTitle("Biometric login for ExampleDemo")
            setSubtitle("Login using your biometric credential")
            setAllowedAuthenticators(allowedAuthenticators)
            if (allowedAuthenticators and DEVICE_CREDENTIAL == 0) {
                setNegativeButtonText("use other login")
            }
            build()
        }
    }

    private fun gotoSettings() {
        val intent = Intent().apply {
            action = Intent.ACTION_VIEW
            component = ComponentName("com.android.settings", "com.android.settings.Settings")
        }
        forActivityResultLauncher.launch(intent)
    }
}

系统的生物识别弹窗会导致录制黑屏,实际效果可以用示例代码自己尝试,效果如图:

device-2022-09-03-091946.gif