现在的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支持以下加密对象:Signature、Cipher和 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)
}
}
系统的生物识别弹窗会导致录制黑屏,实际效果可以用示例代码自己尝试,效果如图: