Un método para proteger la información sensible o el contenido premium de tu app es solicitar autenticación biométrica, por ejemplo, con reconocimiento facial o de huella digital. En esta guía, se explica cómo admitir flujos de acceso biométrico en tu app.
Como regla general, debes usar Credential Manager para el acceso inicial en un dispositivo. Las reautorizaciones posteriores se pueden realizar con el mensaje biométrico o el Administrador de credenciales. La ventaja de usar el mensaje biométrico es que ofrece más opciones de personalización, mientras que el Administrador de credenciales ofrece una sola implementación en ambos flujos.
Declara los tipos de autenticación que admite tu app
Para definir los tipos de autenticación que admite tu app, usa la interfaz de BiometricManager.Authenticators
. El sistema te permite declarar los siguientes tipos de autenticación:
BIOMETRIC_STRONG
- Autenticación con un método biométrico de clase 3, como se define en la página Definición de compatibilidad con Android
BIOMETRIC_WEAK
- Autenticación con un método biométrico de clase 2, como se define en la página Definición de compatibilidad con Android
DEVICE_CREDENTIAL
- Autenticación a través de una credencial de bloqueo de pantalla: el PIN, el patrón o la contraseña del usuario
Para comenzar usando un autenticador, el usuario debe crear un PIN, un patrón o una contraseña. Si el usuario todavía no tiene uno, el flujo de inscripción biométrica le pedirá que lo cree.
Para definir los tipos de autenticación biométrica que acepta tu app, pasa un tipo de autenticación o una combinación de bits de tipos al método setAllowedAuthenticators()
. En el siguiente fragmento de código, se muestra cómo admitir la autenticación con un método biométrico de clase 3 o una credencial de bloqueo de pantalla.
Kotlin
// Lets the user authenticate using either a Class 3 biometric or // their lock screen credential (PIN, pattern, or password). promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) .build()
Java
// Lets user authenticate using either a Class 3 biometric or // their lock screen credential (PIN, pattern, or password). promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) .build();
Las siguientes combinaciones de tipos de autenticador no son compatibles con Android 10 (nivel de API 29) y versiones anteriores: DEVICE_CREDENTIAL
y BIOMETRIC_STRONG | DEVICE_CREDENTIAL
. Para verificar la presencia de un PIN, un patrón o una contraseña en Android 10 y versiones anteriores, usa el método KeyguardManager.isDeviceSecure()
.
Comprueba que la autenticación biométrica esté disponible
Después de decidir qué elementos de autenticación admite tu app, verifica si esos elementos están disponibles. Para ello, pasa la misma combinación a nivel de bits de tipos que declaraste mediante el método setAllowedAuthenticators()
al método canAuthenticate()
.
Si es necesario, invoca la acción de intent ACTION_BIOMETRIC_ENROLL
. En el intent adicional, proporciona el conjunto de autenticadores que acepta tu app. Este intent solicita al usuario que registre las credenciales de un autenticador que acepte tu app.
Kotlin
val biometricManager = BiometricManager.from(this) when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) { BiometricManager.BIOMETRIC_SUCCESS -> Log.d("MY_APP_TAG", "App can authenticate using biometrics.") BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> Log.e("MY_APP_TAG", "No biometric features available on this device.") BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> Log.e("MY_APP_TAG", "Biometric features are currently unavailable.") BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { // Prompts the user to create credentials that your app accepts. val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply { putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BIOMETRIC_STRONG or DEVICE_CREDENTIAL) } startActivityForResult(enrollIntent, REQUEST_CODE) } }
Java
BiometricManager biometricManager = BiometricManager.from(this); switch (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)) { case BiometricManager.BIOMETRIC_SUCCESS: Log.d("MY_APP_TAG", "App can authenticate using biometrics."); break; case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE: Log.e("MY_APP_TAG", "No biometric features available on this device."); break; case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE: Log.e("MY_APP_TAG", "Biometric features are currently unavailable."); break; case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED: // Prompts the user to create credentials that your app accepts. final Intent enrollIntent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL); enrollIntent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BIOMETRIC_STRONG | DEVICE_CREDENTIAL); startActivityForResult(enrollIntent, REQUEST_CODE); break; }
Determina cómo se autenticó el usuario
Después de que el usuario se autentique, podrás verificar si lo hizo usando una credencial de dispositivo o una credencial biométrica llamando al método getAuthenticationType()
.
Muestra la solicitud de acceso
Para mostrar un mensaje del sistema que solicite al usuario autenticarse mediante credenciales biométricas, utiliza la biblioteca biométrica. Este diálogo proporcionado por el sistema es coherente en todas las apps que lo usan, lo que crea una experiencia del usuario más confiable. En la Figura 1, aparece un ejemplo de este diálogo.
Para agregar una autenticación biométrica a tu app con la biblioteca Biometric, sigue estos pasos:
En el archivo
build.gradle
del módulo de tu app, agrega una dependencia a la bibliotecaandroidx.biometric
.En la actividad o el fragmento que aloja el diálogo de acceso biométrico, muestra el diálogo con la lógica indicada en el siguiente fragmento de código:
Kotlin
private lateinit var executor: Executor private lateinit var biometricPrompt: BiometricPrompt private lateinit var promptInfo: BiometricPrompt.PromptInfo override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) executor = ContextCompat.getMainExecutor(this) biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) Toast.makeText(applicationContext, "Authentication error: $errString", Toast.LENGTH_SHORT) .show() } override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) Toast.makeText(applicationContext, "Authentication succeeded!", Toast.LENGTH_SHORT) .show() } override fun onAuthenticationFailed() { super.onAuthenticationFailed() Toast.makeText(applicationContext, "Authentication failed", Toast.LENGTH_SHORT) .show() } }) promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Use account password") .build() // Prompt appears when user clicks "Log in". // Consider integrating with the keystore to unlock cryptographic operations, // if needed by your app. val biometricLoginButton = findViewById<Button>(R.id.biometric_login) biometricLoginButton.setOnClickListener { biometricPrompt.authenticate(promptInfo) } }
Java
private Executor executor; private BiometricPrompt biometricPrompt; private BiometricPrompt.PromptInfo promptInfo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); executor = ContextCompat.getMainExecutor(this); biometricPrompt = new BiometricPrompt(MainActivity.this, executor, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { super.onAuthenticationError(errorCode, errString); Toast.makeText(getApplicationContext(), "Authentication error: " + errString, Toast.LENGTH_SHORT) .show(); } @Override public void onAuthenticationSucceeded( @NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); Toast.makeText(getApplicationContext(), "Authentication succeeded!", Toast.LENGTH_SHORT).show(); } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); Toast.makeText(getApplicationContext(), "Authentication failed", Toast.LENGTH_SHORT) .show(); } }); promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Use account password") .build(); // Prompt appears when user clicks "Log in". // Consider integrating with the keystore to unlock cryptographic operations, // if needed by your app. Button biometricLoginButton = findViewById(R.id.biometric_login); biometricLoginButton.setOnClickListener(view -> { biometricPrompt.authenticate(promptInfo); }); }
Usa una solución criptográfica que dependa de la autenticación
Para proteger aún más la información sensible de tu app, puedes incorporar criptografía en tu flujo de trabajo de autenticación biométrica con una instancia de CryptoObject
.
El marco de trabajo admite los siguientes objetos criptográficos: Signature
, Cipher
y Mac
.
Una vez que se autentica el usuario correctamente mediante un mensaje biométrico, tu app puede realizar una operación criptográfica. Por ejemplo, si autenticas con un objeto Cipher
, tu app puede realizar una encriptación y una desencriptación con un objeto SecretKey
.
En las siguientes secciones, se muestran ejemplos de cómo usar un objeto Cipher
y un objeto SecretKey
para encriptar datos. Cada ejemplo utiliza los siguientes métodos:
Kotlin
private fun generateSecretKey(keyGenParameterSpec: KeyGenParameterSpec) { val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey() } private fun getSecretKey(): SecretKey { val keyStore = KeyStore.getInstance("AndroidKeyStore") // Before the keystore can be accessed, it must be loaded. keyStore.load(null) return keyStore.getKey(KEY_NAME, null) as SecretKey } private fun getCipher(): Cipher { return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7) }
Java
private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) { KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); keyGenerator.init(keyGenParameterSpec); keyGenerator.generateKey(); } private SecretKey getSecretKey() { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); // Before the keystore can be accessed, it must be loaded. keyStore.load(null); return ((SecretKey)keyStore.getKey(KEY_NAME, null)); } private Cipher getCipher() { return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); }
Cómo autenticar solo con credenciales biométricas
Si tu app usa una clave secreta que requiere credenciales biométricas para desbloquearse, el usuario debe autenticar sus credenciales biométricas cada vez antes de que tu app acceda a la clave.
Para encriptar información sensible solo después de que el usuario realice la autenticación mediante credenciales biométricas, sigue estos pasos:
Genera una clave que use la siguiente configuración
KeyGenParameterSpec
:Kotlin
generateSecretKey(KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) // Invalidate the keys if the user has registered a new biometric // credential, such as a new fingerprint. Can call this method only // on Android 7.0 (API level 24) or higher. The variable // "invalidatedByBiometricEnrollment" is true by default. .setInvalidatedByBiometricEnrollment(true) .build())
Java
generateSecretKey(new KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) // Invalidate the keys if the user has registered a new biometric // credential, such as a new fingerprint. Can call this method only // on Android 7.0 (API level 24) or higher. The variable // "invalidatedByBiometricEnrollment" is true by default. .setInvalidatedByBiometricEnrollment(true) .build());
Inicia un flujo de trabajo de autenticación biométrica que incorpore un algoritmo de cifrado:
Kotlin
biometricLoginButton.setOnClickListener { // Exceptions are unhandled within this snippet. val cipher = getCipher() val secretKey = getSecretKey() cipher.init(Cipher.ENCRYPT_MODE, secretKey) biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) }
Java
biometricLoginButton.setOnClickListener(view -> { // Exceptions are unhandled within this snippet. Cipher cipher = getCipher(); SecretKey secretKey = getSecretKey(); cipher.init(Cipher.ENCRYPT_MODE, secretKey); biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipher)); });
Dentro de tus devoluciones de llamada de autenticación biométrica, usa la clave secreta para encriptar la información sensible:
Kotlin
override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult) { val encryptedInfo: ByteArray = result.cryptoObject.cipher?.doFinal( // plaintext-string text is whatever data the developer would like // to encrypt. It happens to be plain-text in this example, but it // can be anything plaintext-string.toByteArray(Charset.defaultCharset()) ) Log.d("MY_APP_TAG", "Encrypted information: " + Arrays.toString(encryptedInfo)) }
Java
@Override public void onAuthenticationSucceeded( @NonNull BiometricPrompt.AuthenticationResult result) { // NullPointerException is unhandled; use Objects.requireNonNull(). byte[] encryptedInfo = result.getCryptoObject().getCipher().doFinal( // plaintext-string text is whatever data the developer would like // to encrypt. It happens to be plain-text in this example, but it // can be anything plaintext-string.getBytes(Charset.defaultCharset())); Log.d("MY_APP_TAG", "Encrypted information: " + Arrays.toString(encryptedInfo)); }
Autentica con credenciales biométricas o de pantalla de bloqueo
Puedes usar una clave secreta que permita la autenticación mediante credenciales biométricas o credenciales de pantalla de bloqueo (PIN, patrón o contraseña). Cuando configures esta clave, especifica un período de validez. Durante este período, tu app puede realizar varias operaciones criptográficas sin que el usuario necesite volver a autenticarse.
Para encriptar información sensible después de que el usuario se autentique con credenciales biométricas o de pantalla de bloqueo, sigue estos pasos:
Genera una clave que use la siguiente configuración
KeyGenParameterSpec
:Kotlin
generateSecretKey(KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS, ALLOWED_AUTHENTICATORS) .build())
Java
generateSecretKey(new KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS, ALLOWED_AUTHENTICATORS) .build());
Dentro de un período de
VALIDITY_DURATION_SECONDS
después de que se autentique el usuario, encripta la información sensible:Kotlin
private fun encryptSecretInformation() { // Exceptions are unhandled for getCipher() and getSecretKey(). val cipher = getCipher() val secretKey = getSecretKey() try { cipher.init(Cipher.ENCRYPT_MODE, secretKey) val encryptedInfo: ByteArray = cipher.doFinal( // plaintext-string text is whatever data the developer would // like to encrypt. It happens to be plain-text in this example, // but it can be anything plaintext-string.toByteArray(Charset.defaultCharset())) Log.d("MY_APP_TAG", "Encrypted information: " + Arrays.toString(encryptedInfo)) } catch (e: InvalidKeyException) { Log.e("MY_APP_TAG", "Key is invalid.") } catch (e: UserNotAuthenticatedException) { Log.d("MY_APP_TAG", "The key's validity timed out.") biometricPrompt.authenticate(promptInfo) }
Java
private void encryptSecretInformation() { // Exceptions are unhandled for getCipher() and getSecretKey(). Cipher cipher = getCipher(); SecretKey secretKey = getSecretKey(); try { // NullPointerException is unhandled; use Objects.requireNonNull(). ciper.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedInfo = cipher.doFinal( // plaintext-string text is whatever data the developer would // like to encrypt. It happens to be plain-text in this example, // but it can be anything plaintext-string.getBytes(Charset.defaultCharset())); } catch (InvalidKeyException e) { Log.e("MY_APP_TAG", "Key is invalid."); } catch (UserNotAuthenticatedException e) { Log.d("MY_APP_TAG", "The key's validity timed out."); biometricPrompt.authenticate(promptInfo); } }
Autentica con claves de autenticación según el uso
Puedes proporcionar compatibilidad con las claves de autenticación según el uso dentro de tu instancia de BiometricPrompt
. Para esta clave, el usuario deberá presentar una credencial biométrica o una credencial de dispositivo cada vez que tu app necesite acceder a datos protegidos por esa clave. Las claves de autenticación según el uso pueden ser útiles para transacciones importantes, como realizar un pago grande o actualizar los historiales médicos de una persona.
Para asociar un objeto BiometricPrompt
con una clave de autenticación por uso, agrega un código similar al siguiente:
Kotlin
val authPerOpKeyGenParameterSpec = KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose) // Accept either a biometric credential or a device credential. // To accept only one type of credential, include only that type as the // second argument. .setUserAuthenticationParameters(0 /* duration */, KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL) .build()
Java
KeyGenParameterSpec authPerOpKeyGenParameterSpec = new KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose) // Accept either a biometric credential or a device credential. // To accept only one type of credential, include only that type as the // second argument. .setUserAuthenticationParameters(0 /* duration */, KeyProperties.AUTH_BIOMETRIC_STRONG | KeyProperties.AUTH_DEVICE_CREDENTIAL) .build();
Autentica sin acción explícita del usuario
De forma predeterminada, el sistema requiere que los usuarios realicen una acción específica, como presionar un botón, una vez que se aceptan las credenciales biométricas. Esta configuración es la que se prefiere si tu app muestra el diálogo para confirmar una acción sensible o de alto riesgo, como realizar una compra.
Sin embargo, si tu app muestra un cuadro de diálogo de autenticación biométrica para una acción de menor riesgo, puedes proporcionar una sugerencia al sistema de que el usuario no necesita confirmar la autenticación. Esta sugerencia puede permitir al usuario ver el contenido en tu app más rápidamente después de volver a autenticarse mediante una modalidad pasiva, como el reconocimiento facial o de iris. Para proporcionar esta sugerencia, pasa el objeto false
al método setConfirmationRequired()
.
La Figura 2 muestra dos versiones del mismo diálogo. Una versión requiere una acción del usuario explícita, mientras que la otra no.
En el siguiente fragmento de código, se muestra cómo presentar un diálogo que no requiere de una acción explícita del usuario para completar el proceso de autenticación:
Kotlin
// Lets the user authenticate without performing an action, such as pressing a // button, after their biometric credential is accepted. promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Use account password") .setConfirmationRequired(false) .build()
Java
// Lets the user authenticate without performing an action, such as pressing a // button, after their biometric credential is accepted. promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Use account password") .setConfirmationRequired(false) .build();
Admite el resguardo de credenciales no biométricas
Si deseas que tu app permita la autenticación con credenciales biométricas o de dispositivo, puedes declarar que tu app admita credenciales de dispositivo con la inclusión del objeto DEVICE_CREDENTIAL
en el conjunto de valores que pasas al elemento setAllowedAuthenticators()
.
Si, en la actualidad, tu app usa los objetos createConfirmDeviceCredentialIntent()
o setDeviceCredentialAllowed()
para proporcionar esta función, cambia al elemento setAllowedAuthenticators()
.
Recursos adicionales
Para obtener más información sobre la autenticación biométrica en Android, consulta los siguientes recursos.
Entradas de blog
- Migrating from FingerprintManager to BiometricPrompt (Migra de FingerprintManager a BiometricPrompt)