Биометрия в приложении: Face ID, отпечаток, безопасное хранение
Биометрия — самый удобный способ авторизации в мобильном приложении. И самый часто реализованный неправильно. Разбираем, что хранится в Keychain/Keystore, что нельзя писать в SharedPreferences и как не сломать вход после смены пальца.
Биометрия в приложении выглядит просто: поставил Face ID при логине — готово. На практике 70% реализаций имеют дыры: токен в SharedPreferences, секрет в plain UserDefaults, обход через root, потеря доступа после смены биометрии на устройстве.
Главное правило
Биометрия — это не способ хранения секрета. Это способ разблокировать доступ к секрету, который хранится в системном защищённом хранилище. Сам отпечаток никогда не покидает Secure Enclave.
iOS — Keychain + LocalAuthentication
let access = SecAccessControlCreateWithFlags(
nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet, nil
)!
let attrs: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "refresh_token",
kSecValueData as String: token.data(using: .utf8)!,
kSecAttrAccessControl as String: access
]
SecItemAdd(attrs as CFDictionary, nil)
Флаг .biometryCurrentSet — ключ. При смене Face ID или добавлении нового отпечатка запись автоматически инвалидируется. Это защита от «жена записала свой отпечаток».
Android — EncryptedSharedPreferences + BiometricPrompt
Используйте BiometricPrompt из AndroidX, не legacy FingerprintManager. Ключ создаётся в Keystore с setUserAuthenticationRequired(true).
val spec = KeyGenParameterSpec.Builder("auth_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
setInvalidatedByBiometricEnrollment(true) — аналог iOS-флага. При добавлении нового отпечатка ключ убивается.
Что нельзя
- Токен в SharedPreferences без шифрования.
- Пароль пользователя зашифрован в keystore — биометрия успешна → расшифровали и логиним. Это плохой паттерн — пароль никогда не должен лежать на устройстве.
- Просто флаг
is_biometry_okв локальной БД. Обход через root за минуту. - Свой UI для биометрии. Используйте системный.
Правильный поток
- Первый вход — обычный пароль/OTP.
- Сервер выдаёт refresh-токен с длительным сроком (30-90 дней).
- Refresh-токен сохраняем в Keychain/Keystore с биометрической защитой.
- Следующие входы — биометрия → достали refresh → обменяли на access-токен.
- При смене биометрии на устройстве — токен умирает → пользователь логинится снова паролем.
Backup-сценарий
Биометрия — не единственный способ войти. Всегда добавляйте PIN-код приложения или возможность ввести пароль заново. Иначе после порезанного пальца или новой Face ID юзер останется без приложения навсегда.
Вывод
Биометрия — это разблокировка системного хранилища, не хранение секрета сама по себе. На iOS — Keychain + .biometryCurrentSet. На Android — Keystore + setInvalidatedByBiometricEnrollment. Бэкап — PIN или пароль. Без этих штрихов реализация ломается при первой же смене отпечатка.