Разработка авторизации по биометрии (отпечаток пальца) в Android-приложении
На Android биометрия прошла долгий путь: FingerprintManager (deprecated API 28), BiometricPrompt (появился в API 28, нормально заработал в 29–30), и наконец стабильная androidx.biometric:biometric библиотека версии 1.2+. Если приложение до сих пор использует FingerprintManager — это технический долг, который выстрелит при таргете API 34.
Что ломается чаще всего
BiometricPrompt требует передачи FragmentActivity или Fragment. Разработчики иногда пытаются вызвать его из ViewModel или Repository — получают IllegalStateException в рантайме. Prompt живёт в UI-слое, точка.
Второй камень — CryptoObject. Многие реализации вызывают BiometricPrompt.authenticate() без CryptoObject, то есть проверяют только присутствие биометрии, но не привязывают её к криптографической операции. Это "слабая" биометрия: злоумышленник с root-доступом теоретически может подделать результат аутентификации, инжектируя SUCCESS в AuthenticationCallback. Правильный путь — Class 3 (Strong) биометрия с CryptoObject.
Третий — фрагментация Android. На MIUI 12–13 BiometricManager.canAuthenticate(BIOMETRIC_STRONG) возвращает BIOMETRIC_ERROR_NONE_ENROLLED даже при зарегистрированных отпечатках из-за кастомизации Xiaomi. Приходится добавлять fallback-проверку через FingerprintManagerCompat для таких случаев.
Правильная реализация с CryptoObject
Суть: генерируем ключ в Android Keystore, привязанный к биометрии. При аутентификации Cipher инициализируется этим ключом и передаётся в CryptoObject. Если биометрия прошла успешно — cipher разблокирован и можно шифровать/расшифровывать данные.
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(
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()
)
keyGenerator.generateKey()
setInvalidatedByBiometricEnrollment(true) — ключ инвалидируется при добавлении нового отпечатка. Без этого флага старый ключ остаётся рабочим после изменения биометрии пользователем.
После генерации ключа:
val cipher = Cipher.getInstance("${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}")
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val secretKey = keyStore.getKey(KEY_NAME, null) as SecretKey
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
Затем передаём cryptoObject в biometricPrompt.authenticate(promptInfo, cryptoObject).
Callback обязательно обрабатываем полностью
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
val cipher = result.cryptoObject?.cipher ?: return
// расшифровываем токен из EncryptedSharedPreferences
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
when (errorCode) {
BiometricPrompt.ERROR_LOCKOUT -> showFallback()
BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> showPermanentLockout()
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> showPinAuth()
BiometricPrompt.ERROR_USER_CANCELED -> { /* ничего не делаем */ }
}
}
override fun onAuthenticationFailed() {
// попытка не удалась, но лимит не исчерпан — BiometricPrompt сам показывает ошибку
}
}
onAuthenticationFailed — не финальная ошибка. Система сама обновляет UI промпта. Не прячьте промпт и не показывайте свои ошибки в этом callback.
Хранение токена
Используем EncryptedSharedPreferences из androidx.security:security-crypto. Шифруем token через cipher из успешного CryptoObject, сохраняем зашифрованные байты + IV в EncryptedSharedPreferences. При следующей авторизации: разворачиваем биометрию в режиме DECRYPT_MODE с сохранённым IV → получаем plaintext токен.
Этапы и сроки
Проверка минимального API уровня (наш таргет — API 23+, BiometricPrompt работает с API 28 через androidx.biometric) → реализация KeyStore-ключа и CryptoObject-flow → UI промпта с кастомными текстами → обработка всех error codes → тестирование на реальных устройствах (Samsung Galaxy, Xiaomi, Pixel) → покрытие unit-тестами через mock BiometricPrompt.
Срок — 3–6 рабочих дней. На Xiaomi и устройствах с кастомными прошивками добавляем время на отдельную проверку совместимости.







