Cómo mostrar un diálogo de autenticación biométrica

Un método para proteger la información sensible o el contenido premium de tu app es solicitar autenticación biométrica, por ejemplo, reconocimiento facial o de huella digital. En esta guía, se explica cómo admitir flujos de acceso biométrico en tu app.

Cómo comprobar que la autenticación biométrica esté disponible

Puedes verificar si un dispositivo admite la autenticación biométrica antes de invocar BiometricPrompt mediante el método canAuthenticate() en la clase BiometricManager.

En el siguiente fragmento de código, se muestra cómo invocar este método:

Kotlin

    val biometricManager = BiometricManager.from(this)
    when (biometricManager.canAuthenticate()) {
        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 ->
            Log.e("MY_APP_TAG", "The user hasn't associated " +
            "any biometric credentials with their account.")
    }
    

Java

    BiometricManager biometricManager = BiometricManager.from(this);
    switch (biometricManager.canAuthenticate()) {
        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:
            Log.e("MY_APP_TAG", "The user hasn't associated " +
                "any biometric credentials with their account.");
            break;
    }
    

Cómo mostrar 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.

Captura de pantalla que muestra un diálogo
Figura 1: Diálogo del sistema que solicita autenticación biométrica

Para agregar autenticación biométrica a tu app con la biblioteca biométrica, sigue estos pasos:

  1. En el archivo app/build.gradle de tu app, agrega una dependencia para la biblioteca biométrica:

        dependencies {
            implementation 'androidx.biometric:biometric:1.0.1'
        }
        
  2. 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);
            });
        }
        

Cómo usar 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 encriptación y 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 autentique usando credenciales biométricas, completa los siguientes pasos:

  1. 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());
        
  2. 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));
        });
        
  3. 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.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.getBytes(Charset.defaultCharset()));
            Log.d("MY_APP_TAG", "Encrypted information: " +
                    Arrays.toString(encryptedInfo));
        }

Cómo autenticar 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, completa los siguientes pasos:

  1. 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)
            .setUserAuthenticationValidityDurationSeconds(VALIDITY_DURATION)
            .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)
            .setUserAuthenticationValidityDurationSeconds(VALIDITY_DURATION)
            .build());
        
  2. Dentro de un período de VALIDITY_DURATION segundos después de que el usuario se autentica, 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.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.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 sin acción explícita del usuario

De manera 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 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.

Captura de pantalla del diálogoCaptura de pantalla del diálogo
Figura 2: Autenticación facial sin confirmación del usuario (arriba) y con confirmación del usuario (abajo)

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

    // Allows user to 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

    // Allows user to 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();
    

Cómo admitir el resguardo de credenciales no biométricas

Si deseas que tu app permita la autenticación con credenciales biométricas o de dispositivo, puedes permitir que se autentiquen con su PIN, patrón o contraseña de pantalla de bloqueo pasando true al método setDeviceCredentialAllowed().

En el siguiente fragmento de código, se muestra cómo admitir la autenticación con credenciales biométricas o de dispositivos:

Kotlin

    // Allows user to authenticate using their lock screen
    // PIN, pattern, or password.
    promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Biometric login for my app")
            .setSubtitle("Log in using your biometric credential")
            // Cannot call setNegativeButtonText() and
            // setDeviceCredentialAllowed() at the same time.
            // .setNegativeButtonText("Use account password")
            .setDeviceCredentialAllowed(true)
            .build()
    

Java

    // Allows user to authenticate using their lock screen
    // PIN, pattern, or password.
    promptInfo = new BiometricPrompt.PromptInfo.Builder()
            .setTitle("Biometric login for my app")
            .setSubtitle("Log in using your biometric credential")
            // Cannot call setNegativeButtonText() and
            // setDeviceCredentialAllowed() at the same time.
            // .setNegativeButtonText("Use account password")
            .setDeviceCredentialAllowed(true)
            .build();
    

Recursos adicionales

Para obtener más información sobre la autenticación biométrica en Android, consulta los siguientes recursos.

Entradas de blog