Показать диалоговое окно биометрической аутентификации

Один из способов защиты конфиденциальной информации или премиум-контента в вашем приложении — это использование биометрической аутентификации, например, распознавания лица или отпечатков пальцев. В этом руководстве объясняется, как обеспечить поддержку биометрической авторизации в вашем приложении.

Как правило, для первоначального входа в систему на устройстве следует использовать Credential Manager . Последующие повторные авторизации можно выполнить либо с помощью Biometric Prompt, либо с помощью Credential Manager. Преимущество использования Biometric Prompt заключается в большем количестве вариантов настройки, в то время как Credential Manager предлагает единую реализацию для обоих процессов.

Укажите типы аутентификации, которые поддерживает ваше приложение.

Для определения типов аутентификации, поддерживаемых вашим приложением, используйте интерфейс BiometricManager.Authenticators . Система позволяет объявлять следующие типы аутентификации:

BIOMETRIC_STRONG
Аутентификация с использованием биометрических данных класса 3 , как определено на странице описания совместимости Android .
BIOMETRIC_WEAK
Аутентификация с использованием биометрических данных класса 2 , как определено на странице описания совместимости Android .
DEVICE_CREDENTIAL
Аутентификация осуществляется с помощью учетных данных для блокировки экрана — PIN-кода, графического ключа или пароля пользователя.

Для начала использования аутентификатора пользователю необходимо создать PIN-код, графический ключ или пароль. Если у пользователя его еще нет, процедура биометрической регистрации предложит ему создать его.

Чтобы определить типы биометрической аутентификации, которые принимает ваше приложение, передайте тип аутентификации или побитовую комбинацию типов в метод setAllowedAuthenticators() . Следующий фрагмент кода показывает, как поддерживать аутентификацию с использованием биометрических данных класса 3 или учетных данных блокировки экрана.

Котлин

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

Следующие комбинации типов аутентификаторов не поддерживаются в Android 10 (уровень API 29) и ниже: DEVICE_CREDENTIAL и BIOMETRIC_STRONG | DEVICE_CREDENTIAL . Для проверки наличия PIN-кода, графического ключа или пароля в Android 10 и ниже используйте метод KeyguardManager.isDeviceSecure() .

Убедитесь, что доступна биометрическая аутентификация.

После того, как вы определите, какие элементы аутентификации поддерживает ваше приложение, проверьте, доступны ли эти элементы. Для этого передайте в метод canAuthenticate() ту же побитовую комбинацию типов, которую вы объявили с помощью метода setAllowedAuthenticators() . При необходимости вызовите действие интента ACTION_BIOMETRIC_ENROLL . В дополнительном параметре интента укажите набор аутентификаторов, которые принимает ваше приложение. Этот интент предложит пользователю зарегистрировать учетные данные для аутентификатора, который принимает ваше приложение.

Котлин

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;
}

Определите, каким способом пользователь прошел аутентификацию.

После аутентификации пользователя вы можете проверить, использовал ли он учетные данные устройства или биометрические данные, вызвав getAuthenticationType() .

Отобразить приглашение для входа в систему

Для отображения системного запроса на аутентификацию пользователя с использованием биометрических данных используйте библиотеку «Биометрия» . Этот предоставляемый системой диалог является единообразным для всех приложений, которые его используют, что обеспечивает более доверительный пользовательский опыт. Пример диалога показан на рисунке 1.

Скриншот, демонстрирующий диалог
Рисунок 1. Диалоговое окно системы с запросом биометрической аутентификации.

Чтобы добавить биометрическую аутентификацию в ваше приложение с помощью библиотеки Biometric, выполните следующие шаги:

  1. В файле build.gradle вашего модуля приложения добавьте зависимость от библиотеки androidx.biometric .

  2. В активности или фрагменте, содержащем диалоговое окно биометрического входа, отобразите его, используя логику, показанную в следующем фрагменте кода:

    Котлин

    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);
        });
    }

Используйте криптографическое решение, основанное на аутентификации.

Для дополнительной защиты конфиденциальной информации в вашем приложении вы можете интегрировать криптографию в процесс биометрической аутентификации, используя экземпляр CryptoObject . Фреймворк поддерживает следующие криптографические объекты: Signature , Cipher и Mac .

После успешной аутентификации пользователя с помощью биометрических данных ваше приложение может выполнить криптографическую операцию. Например, если аутентификация выполняется с использованием объекта Cipher , ваше приложение может затем выполнить шифрование и дешифрование с помощью объекта SecretKey .

В следующих разделах рассматриваются примеры использования объектов Cipher и SecretKey для шифрования данных. В каждом примере используются следующие методы:

Котлин

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);
}

Аутентификация с использованием только биометрических данных.

Если ваше приложение использует секретный ключ, для разблокировки которого требуются биометрические данные, пользователю необходимо будет подтверждать свои биометрические данные каждый раз, прежде чем ваше приложение получит доступ к ключу.

Чтобы зашифровать конфиденциальную информацию только после аутентификации пользователя с использованием биометрических данных, выполните следующие шаги:

  1. Сгенерируйте ключ, используя следующую конфигурацию KeyGenParameterSpec :

    Котлин

    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. Запустите процесс биометрической аутентификации, включающий использование шифрования:

    Котлин

    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. В функциях обратного вызова для биометрической аутентификации используйте секретный ключ для шифрования конфиденциальной информации:

    Котлин

    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));
    }

Аутентификация может осуществляться с использованием биометрических данных или данных экрана блокировки.

Вы можете использовать секретный ключ, позволяющий аутентифицироваться с помощью биометрических данных или данных блокировки экрана (PIN-код, графический ключ или пароль). При настройке этого ключа укажите период действия. В течение этого периода ваше приложение может выполнять множество криптографических операций без необходимости повторной аутентификации пользователя.

Для шифрования конфиденциальной информации после аутентификации пользователя с использованием биометрических данных или данных экрана блокировки выполните следующие шаги:

  1. Сгенерируйте ключ, используя следующую конфигурацию KeyGenParameterSpec :

    Котлин

    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());
  2. В течение периода времени VALIDITY_DURATION_SECONDS после аутентификации пользователя зашифруйте конфиденциальную информацию:

    Котлин

    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);
        }
    }

Аутентификация с использованием ключей аутентификации для каждого использования.

В вашем экземпляре BiometricPrompt можно реализовать поддержку ключей с авторизацией на каждое использование. Такой ключ требует от пользователя предъявления либо биометрических данных, либо учетных данных устройства каждый раз, когда вашему приложению необходимо получить доступ к данным, защищенным этим ключом. Ключи с авторизацией на каждое использование могут быть полезны для транзакций с высокой стоимостью, таких как совершение крупного платежа или обновление медицинских записей человека.

Чтобы связать объект BiometricPrompt с ключом авторизации для каждого использования, добавьте код, аналогичный следующему:

Котлин

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();

Аутентификация без явных действий пользователя.

По умолчанию система требует от пользователей выполнения определенного действия, например, нажатия кнопки, после подтверждения их биометрических данных. Такая конфигурация предпочтительна, если ваше приложение отображает диалоговое окно для подтверждения конфиденциального или рискованного действия, например, совершения покупки.

Если ваше приложение отображает диалог биометрической аутентификации для действий с низким уровнем риска, вы можете указать системе, что пользователю не нужно подтверждать аутентификацию. Эта подсказка позволит пользователю быстрее просматривать контент в вашем приложении после повторной аутентификации с использованием пассивного метода, такого как распознавание лица или радужной оболочки глаза. Чтобы указать это, передайте значение false в метод setConfirmationRequired() .

На рисунке 2 показаны две версии одного и того же диалогового окна. Одна версия требует явного действия от пользователя, а другая — нет.

Скриншот диалогаСкриншот диалога
Рисунок 2. Аутентификация по лицу без подтверждения пользователя (сверху) и с подтверждением пользователя (снизу).

Приведённый ниже фрагмент кода показывает, как отобразить диалоговое окно, для завершения процесса аутентификации в котором не требуется явного действия со стороны пользователя:

Котлин

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

Предусмотреть возможность использования небиометрических учетных данных в качестве резервного варианта.

Если вы хотите, чтобы ваше приложение позволяло аутентифицироваться с использованием биометрических данных или учетных данных устройства, вы можете указать, что ваше приложение поддерживает учетные данные устройства, добавив DEVICE_CREDENTIAL в набор значений, которые вы передаете в setAllowedAuthenticators() .

Если ваше приложение в настоящее время использует createConfirmDeviceCredentialIntent() или setDeviceCredentialAllowed() для предоставления этой возможности, переключитесь на использование setAllowedAuthenticators() .

Дополнительные ресурсы

Чтобы узнать больше о биометрической аутентификации на Android, обратитесь к следующим ресурсам.

Сообщения в блоге