Один из способов защитить конфиденциальную информацию или платный контент в приложении – запросить биометрическую аутентификацию, например распознавание лица или отпечатков пальцев. В этом руководстве объясняется, как поддерживать биометрический вход в систему. потоки в вашем приложении.
Как правило, для первоначального входа на устройстве следует использовать Менеджер учетных данных. Последующие повторные авторизации можно выполнять с помощью запроса биометрических данных или Менеджера учетных данных. Преимущество Biometric Prompt заключается в том, что он предлагает больше возможностей настройки, в то время как Менеджер учетных данных предлагает единую реализацию для обоих потоков.
Укажите, какие типы аутентификации поддерживает ваше приложение
Чтобы определить типы аутентификации, которые поддерживает ваше приложение, используйте интерфейс BiometricManager.Authenticators. Система позволяет заявлять следующие типы аутентификации:
BIOMETRIC_STRONG- Аутентификация с помощью биометрических данных класса 3, как описано на странице Android Compatibility Definition Document.
BIOMETRIC_WEAK- Аутентификация с помощью биометрических данных класса 2, как определено на странице Совместимость с Android.
DEVICE_CREDENTIAL- Аутентификация с использованием учетных данных для блокировки экрана – PIN-кода, графического ключа или пароля пользователя.
Чтобы начать использовать аутентификатор, пользователю нужно создать PIN-код, графический ключ или пароль. Если у пользователя его нет, ему будет предложено его создать.
Чтобы определить типы биометрической аутентификации, которые принимает ваше приложение, передайте тип аутентификации или побитовую комбинацию типов в метод setAllowedAuthenticators(). В следующем фрагменте кода показано, как реализовать аутентификацию с помощью биометрических данных класса 3 или учетных данных для блокировки экрана.
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();
Следующие комбинации типов аутентификаторов не поддерживаются в Android 10 (уровень API 29) и более ранних версиях: DEVICE_CREDENTIAL и BIOMETRIC_STRONG | DEVICE_CREDENTIAL. Чтобы проверить, задан ли PIN-код, графический ключ или пароль на устройстве с Android 10 или более ранней версии, используйте метод KeyguardManager.isDeviceSecure().
Проверьте, доступна ли биометрическая аутентификация
Решив, какие элементы аутентификации поддерживает ваше приложение, проверьте, доступны ли они. Для этого передайте в метод canAuthenticate() ту же побитовую комбинацию типов, которую вы объявили с помощью метода setAllowedAuthenticators().
При необходимости вызовите действие интента ACTION_BIOMETRIC_ENROLL. В дополнительном параметре намерения укажите набор аутентификаторов, которые принимает ваше приложение. Это намерение предлагает пользователю зарегистрировать учетные данные для аутентификатора, который поддерживает ваше приложение.
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;
}
Определите, как пользователь прошел аутентификацию
После аутентификации пользователя вы можете проверить, использовались ли учетные данные устройства или биометрические данные, вызвав метод getAuthenticationType().
Показывать запрос на вход
Чтобы показать системный запрос на аутентификацию с помощью биометрических данных, используйте библиотеку Biometric. Это системное диалоговое окно одинаково во всех приложениях, которые его используют, что повышает доверие пользователей. Пример диалогового окна показан на рисунке 1.
Чтобы добавить в приложение биометрическую аутентификацию с помощью библиотеки Biometric, выполните следующие действия:
В файле
build.gradleмодуля приложения добавьте зависимость от библиотекиandroidx.biometric.В действии или фрагменте, в котором размещено диалоговое окно биометрического входа, отобразите диалоговое окно, используя логику, показанную в следующем фрагменте кода:
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); }); }
Использовать криптографическое решение, зависящее от аутентификации
Чтобы дополнительно защитить конфиденциальную информацию в приложении, вы можете добавить в процесс биометрической аутентификации криптографию, используя экземпляр CryptoObject.
Платформа поддерживает следующие криптографические объекты: Signature, Cipher, Mac и KeyAgreement.
После того как пользователь успешно пройдет аутентификацию с помощью биометрических данных, ваше приложение сможет выполнить криптографическую операцию. Например, если вы выполнили аутентификацию с помощью объекта Cipher, ваше приложение может выполнять шифрование и дешифрование с помощью объекта SecretKey.
В разделах ниже приведены примеры использования объектов Cipher и SecretKey для шифрования данных. В каждом примере используются следующие методы:
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);
}
Аутентификация только с помощью биометрических данных
Если в вашем приложении используется секретный ключ, для разблокировки которого требуются биометрические данные, пользователь должен проходить аутентификацию с помощью биометрических данных каждый раз, когда приложению требуется доступ к ключу.
Чтобы зашифровать конфиденциальную информацию только после того, как пользователь пройдет аутентификацию с помощью биометрических данных, выполните следующие действия:
Создайте ключ, используя следующую конфигурацию
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());Запустите рабочий процесс биометрической аутентификации, в котором используется шифр:
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)); });В обратных вызовах биометрической аутентификации используйте секретный ключ для шифрования конфиденциальной информации:
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)); }
Аутентификация с помощью биометрических данных или способа разблокировки экрана
Вы можете использовать секретный ключ, который позволяет проходить аутентификацию с помощью биометрических данных или учетных данных для разблокировки экрана (PIN-кода, графического ключа или пароля). При настройке этого ключа укажите период действия. В течение этого периода времени приложение может выполнять несколько криптографических операций без необходимости повторной аутентификации пользователя.
Чтобы зашифровать конфиденциальную информацию после аутентификации пользователя с помощью биометрических данных или учетных данных заблокированного экрана, выполните следующие действия:
Создайте ключ, который использует следующую конфигурацию
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());В течение
VALIDITY_DURATION_SECONDSпосле аутентификации пользователя зашифруйте конфиденциальную информацию: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); } }
Как выполнить аутентификацию с помощью ключей, требующих подтверждения при каждом использовании
Вы можете настроить поддержку ключей с авторизацией при каждом использовании в своем экземпляре BiometricPrompt. Такой ключ требует от пользователя предоставлять биометрические данные или учетные данные устройства каждый раз, когда приложению требуется доступ к данным, защищенным этим ключом. Ключи с авторизацией при каждом использовании могут быть полезны для ценных транзакций, например при совершении крупного платежа или обновлении информации о здоровье.
Чтобы связать объект BiometricPrompt с ключом, требующим аутентификации при каждом использовании, добавьте код, похожий на следующий:
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();
Аутентификация без явного действия пользователя
По умолчанию после принятия биометрических данных система требует от пользователей выполнить определенное действие, например нажать кнопку. Этот вариант предпочтительнее, если в приложении показывается диалоговое окно для подтверждения конфиденциального или рискованного действия, например покупки.
Если ваше приложение показывает диалоговое окно биометрической аутентификации для действия с низким уровнем риска, вы можете предоставить системе подсказку о том, что пользователю не нужно подтверждать аутентификацию. Это позволит пользователю быстрее получить доступ к контенту в приложении после повторной аутентификации с помощью пассивного метода, например распознавания лица или радужной оболочки глаза. Чтобы предоставить подсказку, передайте значение false в метод setConfirmationRequired().
На рисунке 2 показаны две версии одного и того же диалогового окна. Одна версия требует явного действия пользователя, а другая – нет.
В приведенном ниже фрагменте кода показано, как показать диалоговое окно, которое не требует явного действия пользователя для завершения процесса аутентификации.
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();
Разрешить использовать резервные учетные данные, не связанные с биометрией
Если вы хотите, чтобы в вашем приложении можно было выполнять аутентификацию с помощью биометрических данных или учетных данных устройства, вы можете объявить, что приложение поддерживает учетные данные устройства, включив DEVICE_CREDENTIAL в набор значений, которые вы передаете в setAllowedAuthenticators().
Если в вашем приложении для этого используются createConfirmDeviceCredentialIntent() или setDeviceCredentialAllowed(), перейдите на setAllowedAuthenticators().
Дополнительные ресурсы
Чтобы узнать больше о биометрической аутентификации на устройствах Android, ознакомьтесь со следующими ресурсами: