Введение: вопрос про безопасность, на котором сыпятся
«Как вы храните секреты в приложении?» — после этого вопроса собеседование обычно уходит вглубь, и первое уточнение «а если телефон украдут?» заканчивает разговор для половины кандидатов.
Тема «Биометрия и Keystore» звучит страшно, хотя за ней стоит простая идея: ключ шифрования лежит в защищённом хранилище, а использовать его можно только после того, как пользователь подтвердил себя отпечатком пальца. Это стандартный сценарий для банковских приложений, менеджеров паролей и любых продуктов, где есть чувствительные данные.
Причём вопрос почти никогда не приходит в лоб. Собеседующий может спросить «где вы храните токен?», «как защитить данные на устройстве?» или развернуть сценарий: «пользователь потерял телефон — что попадёт злоумышленнику?». И ответ в духе «ну, у нас в SharedPreferences лежит» — это провал, потому что современный ответ начинается с Keystore, а не с файлового хранилища.
На собеседовании тему спрашивают реже, чем lifecycle или корутины, но она отлично показывает глубину: кандидат, который умеет связывать Keystore с биометрией через CryptoObject, заметно выделяется на фоне тех, кто знает только про SharedPreferences. Разберу связку по слоям: Keystore, генерация ключа, BiometricPrompt и момент, где всё соединяется.
Android Keystore: где живут ключи
Android Keystore — системный механизм хранения криптографических ключей. Ключи лежат в защищённом контейнере, из которого их сложно извлечь, а криптографические операции (шифрование, подпись) выполняются самим Keystore: само приложение ключ даже не видит.
Почему это важно для безопасности: если злоумышленник получил доступ к файлам приложения, ключ из Keystore он не достанет — материал ключа не покидает хранилище. А на современных устройствах ключи дополнительно привязываются к защищённому оборудованию: Trusted Execution Environment (TEE) или Secure Element. В этом случае материал ключа вообще никогда не покидает защищённое железо — даже компрометация самой ОС не даст его извлечь.
Три факта, которые нужно знать про Keystore
1. Ключи хранятся в защищённом контейнере, приложение видит только ссылку на них. 2. Ключ может быть привязан к защищённому оборудованию (TEE / Secure Element). 3. Механизм доступен с Android 4.3 (API 18), тип хранилища — "AndroidKeyStore".
Из интересного для уровня senior: на устройствах с Android 9+ есть StrongBox KeyMint — отдельный защищённый чип (Secure Element) для ключей. Ключ можно пометить как StrongBox-backed через setIsStrongBoxBacked() — и если железо не поддерживает нужный алгоритм, вы получите StrongBoxUnavailableException. На собеседовании достаточно сказать, что вы в курсе такой опции.
Генерация ключа: код, который требует аутентификации
Теперь главное: ключ, который нельзя использовать без биометрии. Для этого при генерации указывается setUserAuthenticationRequired(true) — и ключ будет разрешать использование, только если пользователь недавно аутентифицировался (отпечатком, лицом или PIN-кодом от защищённого экрана блокировки).
private const val KEYSTORE = "AndroidKeyStore"
private const val KEY_ALIAS = "app_encryption_key"
fun getOrCreateSecretKey(): SecretKey {
val keyStore = KeyStore.getInstance(KEYSTORE).apply { load(null) }
(keyStore.getKey(KEY_ALIAS, null) as? SecretKey)?.let { return it }
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, KEYSTORE
)
val keyGenSpec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.build()
keyGenerator.init(keyGenSpec)
return keyGenerator.generateKey()
}Это реальный рабочий код: AES-GCM без паддинга — стандартная связка для шифрования в Keystore.
Подводный камень, о котором знают немногие: ключ, требующий биометрию, инвалидируется при добавлении нового отпечатка или лица. Это поведение по умолчанию — система считает, что после смены биометрических данных нужно заново пройти аутентификацию. Отключить можно через setInvalidatedByBiometricEnrollment(false), но в реальном проекте лучше оставить как есть.
Ещё один нюанс для собеседования: ключам с setUserAuthenticationRequired(true) нужен защищённый экран блокировки (PIN, пароль, паттерн). Если он не установлен — такой ключ просто не заработает.
И небольшой совет из практики: сам ключ — это только половина решения. Вторую половину — куда класть зашифрованные данные — решает остальная архитектура приложения. Ключ из Keystore стоит использовать для шифрования того, что действительно чувствительно: токены, персональные данные, кэш ответов API. Не для всего подряд: каждая операция с таким ключом требует аутентификации, и если шифровать им каждое поле, пользователь будет уставать от диалогов.
BiometricPrompt и BiometricManager: как показать диалог
Современный способ работы с биометрией — библиотека androidx.biometric. Класс BiometricPrompt показывает системный диалог аутентификации: на Android 9 (API 28) и выше это системный интерфейс, который нельзя подделать (документация по BiometricPrompt).
Перед показом диалога стоит проверить, что аутентификация вообще возможна — через BiometricManager.canAuthenticate():
val authenticators = Authenticators.BIOMETRIC_STRONG or
Authenticators.DEVICE_CREDENTIAL
when (BiometricManager.from(this).canAuthenticate(authenticators)) {
BiometricManager.BIOMETRIC_SUCCESS -> showBiometricPrompt()
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
suggestEnrollingBiometrics()
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
fallbackToAppPassword()
}Коды результата из документации по BiometricManager:
BIOMETRIC_SUCCESS— пользователь может аутентифицироваться;BIOMETRIC_ERROR_NONE_ENROLLED— биометрия или пароль устройства не настроены;BIOMETRIC_ERROR_NO_HARDWARE— нет подходящего оборудования;BIOMETRIC_ERROR_HW_UNAVAILABLE— оборудование временно недоступно.
Обратите внимание на Authenticators.DEVICE_CREDENTIAL: это фолбэк на PIN или пароль устройства. Комбинация BIOMETRIC_STRONG or DEVICE_CREDENTIAL — стандартная практика: биометрия есть — показываем её, нет — пользователь вводит пароль устройства, а приложение не блокируется.
CryptoObject: момент, где биометрия открывает ключ
Теперь главное — почему Keystore и биометрия работают только в связке. Сам по себе диалог биометрии — это просто проверка личности. Чтобы она открыла доступ к ключу, используется BiometricPrompt.CryptoObject: вы создаёте Cipher с ключом из Keystore, кладёте его в CryptoObject и передаёте в authenticate().
Система в этом случае аутентифицирует использование ключа: только после успешной биометрии Cipher становится рабочим — шифровать можно. Без аутентификации операция с ключом упадёт с KeyPermanentlyInvalidatedException или ошибкой авторизации.
private fun showBiometricPrompt() {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey())
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Подтвердите действие")
.setSubtitle("Используйте отпечаток пальца или пароль устройства")
.setAllowedAuthenticators(
Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL
)
.build()
BiometricPrompt(
this,
ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
val unlockedCipher = result.cryptoObject?.cipher
encryptSensitiveData(unlockedCipher)
}
}
).authenticate(promptInfo, cryptoObject)
}После успешной аутентификации вы получаете обратно CryptoObject с уже разблокированным Cipher — и спокойно шифруете данные. Расшифровка устроена симметрично: DECRYPT_MODE и такой же диалог.
На собеседовании признак понимания темы — упоминание CryptoObject до того, как интервьюер про него спросит. «Показать диалог биометрии» и «дать доступ к ключу по биометрии» — разные вещи, и связка происходит именно через CryptoObject.
Ошибки и фолбэки: что отвечать на «а если отпечаток не работает»
Биометрия — нестабильный механизм: сенсор может быть мокрым, палец — нечитаемым, а пользователь может вообще не настроить отпечаток. Хороший ответ на собеседовании показывает, что вы продумали сценарии отказа:
- Проверка перед показом —
canAuthenticate(): не тратим время пользователя, если аутентификация невозможна. - Фолбэк на пароль устройства —
DEVICE_CREDENTIALвsetAllowedAuthenticators: биометрия сломалась — пользователь вводит PIN. - Обработка ошибок диалога —
onAuthenticationError: пользователь закрыл диалог или сенсор отказал — показываем понятное сообщение, не валимся. - Повторные попытки —
onAuthenticationFailedдаёт ещё попытку, не нужно паниковать. - Инвалидация ключа — добавлен новый отпечаток: ключ умер, данные расшифровываются предыдущим ключом или заново под аутентификацией.
Логика, которую нужно донести
Ключ — в Keystore, доступ к ключу — только через аутентификацию, аутентификация — через BiometricPrompt с CryptoObject. Уберите любой элемент — и безопасная схема превращается в декорацию.
Готовитесь к собеседованию по Android?
Темы безопасности и криптографии редко входят в стандартный список подготовки, но именно такие вопросы отличают сильных кандидатов. Пришлю бесплатный чек-лист подготовки к собеседованию — конкретные пункты, что закрыть и в каком порядке.
Итоги
- Keystore — защищённое хранилище ключей: приложение не видит ключ, а операции выполняет система, часто на защищённом железе.
- Ключ под биометрию —
setUserAuthenticationRequired(true)при генерации; ключ инвалидируется при добавлении новых биометрических данных. - Связка —
BiometricPrompt+CryptoObject: только после аутентификации пользователя ключ реально работает.
Ответ, который закрывает тему
«Ключ генерируется в Android Keystore с требованием аутентификации пользователя. Диалог показывает BiometricPrompt, а доступ к ключу система выдаёт через CryptoObject — после успешной биометрии Cipher становится рабочим. Если биометрии нет — фолбэк на пароль устройства через DEVICE_CREDENTIAL».
Если такие темы кажутся сложными, а собеседование уже назначено, — не нужно штудировать всё подряд в одиночку. Напишите в Telegram, разберём вашу ситуацию и составим план подготовки.