Введение: вопрос про безопасность, на котором сыпятся

«Как вы храните секреты в приложении?» — после этого вопроса собеседование обычно уходит вглубь, и первое уточнение «а если телефон украдут?» заканчивает разговор для половины кандидатов.

Тема «Биометрия и 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-кодом от защищённого экрана блокировки).

SecretKeyProvider.kt
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():

AuthenticationActivity.kt
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 или ошибкой авторизации.

AuthenticationActivity.kt
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, разберём вашу ситуацию и составим план подготовки.