Biometric для Kotlin: полное руководство с примерами
Биометрическая аутентификация стала неотъемлемой частью современных мобильных приложений. Она позволяет пользователям подтверждать свою личность с помощью отпечатка пальца, сканера лица или радужной оболочки глаза, что значительно удобнее и безопаснее, чем ввод паролей. Библиотека Biometric для Kotlin (часть AndroidX) предоставляет унифицированный API для работы со всеми биометрическими датчиками на устройствах Android.
Зачем нужна эта библиотека? Во-первых, она абстрагирует разработчика от различий в реализации биометрических датчиков у разных производителей. Во-вторых, она автоматически обрабатывает сценарии, когда биометрия недоступна (например, на устройстве нет сканера) или заблокирована. В-третьих, библиотека тесно интегрируется с Android Keystore, позволяя создавать криптографически защищенные ключи, которые можно использовать только после успешной аутентификации пользователя.
В этой статье мы подробно разберем все аспекты работы с библиотекой Biometric для Kotlin: от установки и минимального примера до продвинутых техник и типичных ошибок. Вы узнаете, как добавить биометрическую аутентификацию в свое приложение, сделать ее безопасной и удобной для пользователя.
Установка
Для использования библиотеки Biometric в вашем Android-проекте на Kotlin, добавьте следующую зависимость в файл build.gradle (уровня модуля):
dependencies {
implementation("androidx.biometric:biometric:1.2.0-alpha05")
}
Также убедитесь, что в файле AndroidManifest.xml объявлено разрешение на использование биометрических датчиков:
Если ваше приложение ориентировано на Android 12 (API 31) и выше, рекомендуется также добавить разрешение для точного определения типа биометрии:
Быстрый старт: минимальный рабочий пример
Давайте создадим простейшую Activity, которая при нажатии на кнопку запускает биометрическую аутентификацию и выводит результат в Toast.
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val authButton: Button = findViewById(R.id.auth_button)
// Проверяем, доступна ли биометрия на устройстве
val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
// Биометрия доступна, настраиваем диалог
setupBiometricPrompt()
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
Toast.makeText(this, "На устройстве нет биометрического датчика", Toast.LENGTH_LONG).show()
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
Toast.makeText(this, "Биометрический датчик временно недоступен", Toast.LENGTH_LONG).show()
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
Toast.makeText(this, "Не добавлены биометрические данные", Toast.LENGTH_LONG).show()
}
}
authButton.setOnClickListener {
if (::biometricPrompt.isInitialized) {
biometricPrompt.authenticate(promptInfo)
}
}
}
private fun setupBiometricPrompt() {
val executor = ContextCompat.getMainExecutor(this)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Toast.makeText(this@MainActivity, "Ошибка: $errString", Toast.LENGTH_SHORT).show()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Toast.makeText(this@MainActivity, "Аутентификация успешна!", Toast.LENGTH_SHORT).show()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Toast.makeText(this@MainActivity, "Не удалось распознать биометрию", Toast.LENGTH_SHORT).show()
}
}
biometricPrompt = BiometricPrompt(this, executor, callback)
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Биометрическая аутентификация")
.setSubtitle("Подтвердите свою личность")
.setDescription("Приложите палец к сканеру")
.setNegativeButtonText("Отмена")
.build()
}
}
Этот пример демонстрирует базовый сценарий: проверку доступности биометрии, создание диалогового окна и обработку результатов аутентификации.
Основные классы и методы
Библиотека Biometric предоставляет несколько ключевых классов. Рассмотрим их подробно.
1. BiometricManager
Сигнатура: class BiometricManager
Что делает: Это главный класс для проверки состояния биометрической системы на устройстве. Позволяет узнать, доступна ли биометрия, есть ли зарегистрированные шаблоны и т.д.
val biometricManager = BiometricManager.from(context)
val canAuthenticate = biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
when (canAuthenticate) {
BiometricManager.BIOMETRIC_SUCCESS -> {
// Можно запускать аутентификацию
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
// Нет датчика
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
// Датчик недоступен
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// Нет зарегистрированных биометрических данных
}
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
// Требуется обновление безопасности
}
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
// Неподдерживаемая комбинация аутентификаторов
}
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
// Статус неизвестен
}
}
2. BiometricPrompt
Сигнатура: class BiometricPrompt
Что делает: Основной класс для запуска биометрической аутентификации. Отображает системный диалог и управляет процессом проверки.
val executor = ContextCompat.getMainExecutor(this)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
// Обработка успеха
}
}
val biometricPrompt = BiometricPrompt(this, executor, callback)
biometricPrompt.authenticate(promptInfo)
3. BiometricPrompt.PromptInfo
Сигнатура: class BiometricPrompt.PromptInfo
Что делает: Класс-строитель для настройки внешнего вида и поведения диалога аутентификации.
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Вход в приложение")
.setSubtitle("Используйте отпечаток пальца или Face ID")
.setDescription("Это необходимо для доступа к вашим данным")
.setNegativeButtonText("Использовать пароль")
.setConfirmationRequired(false) // Для Face ID на Android 10+
.setDeviceCredentialAllowed(true) // Разрешить использование PIN/пароля
.build()
4. BiometricPrompt.AuthenticationCallback
Сигнатура: abstract class BiometricPrompt.AuthenticationCallback
Что делает: Абстрактный класс с методами обратного вызова для обработки результатов аутентификации.
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
// Ошибка аутентификации (например, слишком много попыток)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
// Успешная аутентификация
}
override fun onAuthenticationFailed() {
// Неудачная попытка (неверный отпечаток)
}
}
5. BiometricPrompt.AuthenticationResult
Сигнатура: class BiometricPrompt.AuthenticationResult
Что делает: Содержит результат успешной аутентификации, включая опциональный CryptoObject.
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
val cryptoObject: BiometricPrompt.CryptoObject? = result.cryptoObject
// Используем cryptoObject для криптографических операций
if (cryptoObject != null) {
val signature = cryptoObject.signature
// ... работа с подписью
}
}
6. BiometricPrompt.CryptoObject
Сигнатура: class BiometricPrompt.CryptoObject
Что делает: Обертка для криптографических объектов (Signature, Cipher, Mac), которые связываются с биометрической аутентификацией. Позволяет создавать ключи, доступные только после успешной биометрической проверки.
// Создание Cipher, защищенного биометрией
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true) // Ключ доступен только после биометрии
.setInvalidatedByBiometricEnrollment(true) // Сброс ключа при добавлении нового отпечатка
.build()
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val secretKey = keyStore.getKey(KEY_NAME, null) as SecretKey
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" +
KeyProperties.BLOCK_MODE_CBC + "/" +
KeyProperties.ENCRYPTION_PADDING_PKCS7)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
// Запуск аутентификации с CryptoObject
biometricPrompt.authenticate(promptInfo, cryptoObject)
7. BiometricManager.Authenticators
Сигнатура: object BiometricManager.Authenticators
Что делает: Содержит константы для указания типов аутентификаторов: BIOMETRIC_STRONG (отпечаток, Face ID), BIOMETRIC_WEAK (менее надежные), DEVICE_CREDENTIAL (PIN, пароль, графический ключ).
// Проверка только для сильной биометрии
biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
// Проверка для биометрии или PIN-кода
biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
8. BiometricPrompt.AuthenticationCallback (onAuthenticationError с кодами)
Сигнатура: override fun onAuthenticationError(errorCode: Int, errString: CharSequence)
Что делает: Обрабатывает различные ошибки, которые могут возникнуть в процессе аутентификации. Коды ошибок определены в BiometricPrompt.
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
when (errorCode) {
BiometricPrompt.ERROR_LOCKOUT -> {
// Слишком много неудачных попыток, временная блокировка
Toast.makeText(this, "Слишком много попыток. Попробуйте позже.", Toast.LENGTH_LONG).show()
}
BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> {
// Постоянная блокировка, нужно использовать пароль
Toast.makeText(this, "Биометрия заблокирована. Используйте пароль.", Toast.LENGTH_LONG).show()
}
BiometricPrompt.ERROR_USER_CANCELED -> {
// Пользователь отменил аутентификацию
}
else -> {
Toast.makeText(this, "Ошибка: $errString", Toast.LENGTH_SHORT).show()
}
}
}
Реальный практический пример: защищенное хранилище паролей
Давайте создадим более сложный пример — приложение для хранения паролей, которое использует биометрию для доступа к зашифрованным данным. Этот пример объединяет все основные концепции библиотеки.
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
class SecureVaultActivity : AppCompatActivity() {
companion object {
private const val KEY_NAME = "secure_vault_key"
private const val PREFS_NAME = "vault_prefs"
private const val ENCRYPTED_DATA_KEY = "encrypted_password"
private const val IV