blob: 8b75c12a47cd3ac4b6a6832fead2404ad271011c [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.biometric.integration.testapp
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.widget.Button
import android.widget.CheckBox
import android.widget.TextView
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators
import androidx.biometric.BiometricPrompt
import androidx.biometric.integration.testapp.TestUtils.KEYSTORE_INSTANCE
import androidx.biometric.integration.testapp.TestUtils.KEY_LOG_TEXT
import androidx.biometric.integration.testapp.TestUtils.KEY_NAME
import androidx.biometric.integration.testapp.TestUtils.PAYLOAD
import androidx.biometric.integration.testapp.TestUtils.getCipher
import androidx.biometric.integration.testapp.TestUtils.getSecretKey
import androidx.biometric.integration.testapp.TestUtils.toAuthenticationStatusString
import androidx.biometric.integration.testapp.TestUtils.toDataString
import androidx.fragment.app.FragmentActivity
import java.nio.charset.Charset
import javax.crypto.Cipher
import javax.crypto.KeyGenerator.getInstance
/**
* Main activity for the AndroidX Biometric test app.
*/
@SuppressLint("SyntheticAccessor")
class BiometricTestActivity : FragmentActivity() {
// The prompt used for authentication.
private lateinit var biometricPrompt: BiometricPrompt
// Individual UI elements.
private lateinit var allowBiometricStrongCheckbox: CheckBox
private lateinit var allowBiometricWeakCheckbox: CheckBox
private lateinit var allowDeviceCredentialCheckbox: CheckBox
private lateinit var cancelOnConfigChangeCheckbox: CheckBox
private lateinit var requireConfirmationCheckbox: CheckBox
private lateinit var useCryptoAuthCheckbox: CheckBox
private lateinit var logView: TextView
/**
* A bit field representing the currently allowed authenticator type(s).
*/
private val allowedAuthenticators: Int
get() {
var authenticators = 0
if (allowBiometricStrongCheckbox.isChecked) {
authenticators = authenticators or Authenticators.BIOMETRIC_STRONG
}
if (allowBiometricWeakCheckbox.isChecked) {
authenticators = authenticators or Authenticators.BIOMETRIC_WEAK
}
if (allowDeviceCredentialCheckbox.isChecked) {
authenticators = authenticators or Authenticators.DEVICE_CREDENTIAL
}
return authenticators
}
/**
* A bit field representing the authentication type(s) that can authorize use of the secret key.
*/
private val keyType: Int
get() {
var type = 0
if (allowBiometricStrongCheckbox.isChecked) {
type = type or KeyProperties.AUTH_BIOMETRIC_STRONG
}
if (allowDeviceCredentialCheckbox.isChecked) {
type = type or KeyProperties.AUTH_DEVICE_CREDENTIAL
}
return type
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_biometric_test)
// Get checkboxes from the UI so we can access their checked state later.
allowBiometricStrongCheckbox = findViewById(R.id.checkbox_allow_biometric_strong)
allowBiometricWeakCheckbox = findViewById(R.id.checkbox_allow_biometric_weak)
allowDeviceCredentialCheckbox = findViewById(R.id.checkbox_allow_device_credential)
cancelOnConfigChangeCheckbox = findViewById(R.id.checkbox_cancel_config_change)
requireConfirmationCheckbox = findViewById(R.id.checkbox_require_confirmation)
useCryptoAuthCheckbox = findViewById(R.id.checkbox_use_crypto_auth)
// Set the button callbacks.
findViewById<Button>(R.id.button_can_authenticate).setOnClickListener { canAuthenticate() }
findViewById<Button>(R.id.button_authenticate).setOnClickListener { authenticate() }
findViewById<Button>(R.id.button_clear_log).setOnClickListener { clearLog() }
// Restore logged messages on activity recreation (e.g. due to device rotation).
logView = findViewById(R.id.text_view_log)
if (savedInstanceState != null) {
logView.text = savedInstanceState.getCharSequence(KEY_LOG_TEXT, "")
}
// Reconnect the prompt by reinitializing with the new callback.
biometricPrompt = BiometricPrompt(
this,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
log("onAuthenticationError $errorCode: $errString")
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
log("onAuthenticationSucceeded: ${result.toDataString()}")
// Encrypt a test payload using the result of crypto-based auth.
if (useCryptoAuthCheckbox.isChecked) {
val encryptedPayload = result.cryptoObject?.cipher?.doFinal(
PAYLOAD.toByteArray(Charset.defaultCharset())
)
log("Encrypted payload: ${encryptedPayload?.contentToString()}")
}
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
log("onAuthenticationFailed")
}
}
)
}
override fun onStop() {
super.onStop()
// If option is selected, dismiss the prompt on rotation.
if (cancelOnConfigChangeCheckbox.isChecked && isChangingConfigurations) {
biometricPrompt.cancelAuthentication()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// Save the current log messages to be restored on activity recreation.
outState.putCharSequence(KEY_LOG_TEXT, logView.text)
}
/**
* Logs the authentication status given by [BiometricManager.canAuthenticate].
*/
private fun canAuthenticate() {
val result = BiometricManager.from(this).canAuthenticate(allowedAuthenticators)
log("canAuthenticate: ${result.toAuthenticationStatusString()}")
}
/**
* Launches the [BiometricPrompt] to begin authentication.
*/
private fun authenticate() {
val infoBuilder = BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.biometric_prompt_title))
.setSubtitle(getString(R.string.biometric_prompt_subtitle))
.setDescription(getString(R.string.biometric_prompt_description))
.setConfirmationRequired(requireConfirmationCheckbox.isChecked)
.setAllowedAuthenticators(allowedAuthenticators)
.apply {
// Set the negative button text ONLY if device credential auth is not allowed.
if (allowedAuthenticators and Authenticators.DEVICE_CREDENTIAL == 0) {
setNegativeButtonText(getString(R.string.biometric_prompt_negative_label))
}
}
val info: BiometricPrompt.PromptInfo?
try {
info = infoBuilder.build()
} catch (e: IllegalArgumentException) {
log("IllegalArgumentException: ${e.message}")
return
}
if (useCryptoAuthCheckbox.isChecked) {
authenticateWithCrypto(info)
} else {
biometricPrompt.authenticate(info)
}
}
/**
* Launches the [BiometricPrompt] to begin crypto-based authentication.
*/
@Suppress("DEPRECATION")
private fun authenticateWithCrypto(info: BiometricPrompt.PromptInfo) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
log("Error: Key-gen not supported prior to API 23. Falling back to non-crypto auth.")
biometricPrompt.authenticate(info)
return
}
// Create a spec for the key to be generated.
val keyPurpose = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
val keySpec = KeyGenParameterSpec.Builder(KEY_NAME, keyPurpose)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.apply {
// Require authentication for each use of the key.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setUserAuthenticationParameters(0 /* timeout */, keyType)
} else {
setUserAuthenticationValidityDurationSeconds(-1)
}
}
.build()
// Generate and store the key in the Android keystore.
val keyGenerator = getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_INSTANCE)
keyGenerator.init(keySpec)
keyGenerator.generateKey()
// Prepare the crypto object to use for authentication.
val cipher = getCipher()
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
val crypto = BiometricPrompt.CryptoObject(cipher)
try {
biometricPrompt.authenticate(info, crypto)
} catch (e: IllegalArgumentException) {
log("IllegalArgumentException: ${e.message}")
}
}
/**
* Clears all logged messages from the in-app [TextView].
*/
private fun clearLog() {
logView.text = ""
}
/**
* Logs a new [message] to the in-app [TextView].
*/
@SuppressLint("SetTextI18n")
private fun log(message: CharSequence) {
logView.text = "${message}\n${logView.text}"
}
}