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

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

Как правило, для первоначального входа на устройстве следует использовать диспетчер учетных данных . Последующие повторные авторизации можно выполнить с помощью биометрической подсказки или диспетчера учетных данных. Преимущество использования биометрической подсказки заключается в том, что она предлагает больше возможностей настройки, тогда как 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()

Ява

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

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

После того как вы решите, какие элементы аутентификации поддерживает ваше приложение, проверьте, доступны ли эти элементы. Для этого передайте ту же побитовую комбинацию типов, которую вы объявили с помощью метода setAllowedAuthenticators() в метод canAuthenticate() . При необходимости вызовите действие намерения 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)
    }
}

Ява

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. Системный диалог с запросом биометрической аутентификации.

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

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

    Ява

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

Ява

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

    Ява

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

    Ява

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

    Ява

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

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

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

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

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

    Ява

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

    Ява

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

Ява

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

Ява

// 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, обратитесь к следующим ресурсам.

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