Перейти к содержанию
Innovialabs

Хранение чувствительных данных: Keychain и Android Keystore

Продвинутое4 мин

Коротко

Токены и ключи храните в iOS Keychain и под защитой Android Keystore, а не в SharedPreferences, UserDefaults или AsyncStorage. Биометрию привязывайте к криптографическому ключу, а не к проверке «да/нет», и помните: секрет, вшитый в приложение, секретом не является.

Куда класть секреты

Короткий ответ:

  • iOS — в Keychain. Это системное зашифрованное хранилище с настройками доступа.
  • Android — данные шифруются ключом из Android Keystore. Сам ключ не покидает защищённую среду (TEE или отдельный чип StrongBox, если он есть на устройстве), а зашифрованные данные лежат в файле или базе.
  • Кроссплатформенно — через обёртки: flutter_secure_storage, react-native-keychain, expo-secure-store. Внутри они используют те же Keychain и Keystore.
const storage = FlutterSecureStorage();
await storage.write(key: "refresh_token", value: token);
final saved = await storage.read(key: "refresh_token");

Почему SharedPreferences и AsyncStorage не подходят

SharedPreferences, UserDefaults и AsyncStorage — это обычные файлы в папке приложения, без шифрования. От других приложений их защищает песочница ОС, но этого мало:

  • на устройстве с root или jailbreak файлы читаются напрямую;
  • данные могут попасть в резервные копии устройства, если их явно не исключить;
  • при отладке и в незащищённых сборках содержимое легко извлечь;
  • вредоносный код внутри вашего процесса (например, скомпрометированная библиотека) видит всё.

Эти хранилища подходят для настроек: тема, язык, флаг «онбординг пройден». Для токенов и ключей — нет.

Что именно хранить и как

ДанныеРекомендация
Access tokenКороткий срок жизни; в памяти или в защищённом хранилище
Refresh tokenKeychain / Keystore, с удалением при выходе
Ключ шифрования локальной базыГенерировать на устройстве, хранить в Keychain / Keystore
Пароль пользователяНе хранить вообще, обменять на токен
Секрет серверного APIНе класть в приложение — держать на бэкенде

Последняя строка важна: всё, что вшито в бинарник, можно извлечь декомпиляцией. Ключи сторонних сервисов, которые дают доступ к платным или приватным действиям, должны жить на сервере, а приложение должно ходить через ваш бэкенд.

Настройки доступа в Keychain

У элемента Keychain есть атрибут доступности:

  • WhenUnlocked — доступен, только когда устройство разблокировано;
  • AfterFirstUnlock — доступен после первой разблокировки, нужен для фоновых задач;
  • варианты с ThisDeviceOnly не переносятся в резервные копии на другое устройство.

Для токенов обычно выбирают самый строгий вариант, который не ломает фоновую работу. Учтите, что на iOS элементы Keychain могут сохраниться после удаления приложения — при первом запуске стоит проверять это и очищать старые данные.

Биометрия: правильно и неправильно

Неправильно: вызвать Face ID или отпечаток, получить true и после этого прочитать токен из обычного хранилища. Такую проверку можно обойти, подменив результат на взломанном устройстве.

Правильно: привязать к биометрии сам ключ.

  • iOS — элемент Keychain с access control (biometryCurrentSet): система отдаст данные только после успешной биометрии.
  • Android — ключ в Keystore с setUserAuthenticationRequired(true) и BiometricPrompt с CryptoObject: расшифровка возможна только после подтверждения.

Вариант biometryCurrentSet делает ключ недействительным при добавлении нового отпечатка или лица — это защищает от ситуации, когда кто-то добавил свою биометрию.

Основы certificate pinning

Pinning — это проверка, что сервер предъявил конкретный ключ или сертификат, а не просто любой сертификат, которому доверяет устройство. Он защищает от перехвата трафика через подставной корневой сертификат.

Правила, без которых pinning опасен:

  • закрепляйте хеш публичного ключа, а не весь сертификат;
  • всегда держите резервный пин — ключ, который вы будете использовать после ротации;
  • спланируйте обновление: если сертификат сменился, а приложение знает только старый пин, оно перестанет работать у всех пользователей.

На Android pinning задаётся в Network Security Config:

<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">api.example.com</domain>
    <pin-set expiration="2027-01-01">
      <pin digest="SHA-256">PRIMARY_KEY_HASH_BASE64=</pin>
      <pin digest="SHA-256">BACKUP_KEY_HASH_BASE64=</pin>
    </pin-set>
  </domain-config>
</network-security-config>

На iOS — через ключ NSPinnedDomains в Info.plist или проверку в делегате URLSession.

Частые ошибки

  • Токены в AsyncStorage «временно, потом перенесём».
  • Секреты в .env, который попадает в сборку.
  • Логи с токенами в консоли или системах аналитики.
  • Нет очистки хранилища при выходе из аккаунта.
  • Pinning без резервного ключа.

Частые вопросы

Нет, это один слой. Нужны также короткоживущие токены, отзыв сессий на сервере, HTTPS везде и проверка прав на бэкенде. Защищённое хранилище снижает риск кражи данных с устройства, но не заменяет серверную безопасность.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.