Розробка авторизації по біометрії (відбиток пальця) у 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 та пристроях з кастомними прошивками добавляємо час на окремої перевірки сумісності.







