Разработчики часто недооценивают хранение данных в мобильных приложениях. Токен авторизации кидают в UserDefaults, пароль сохраняют в текстовый файл внутри песочницы — и думают, что раз приложение работает, значит всё нормально. Это не нормально. Рассказываю, где хранить чувствительные данные в iOS и как сделать это правильно.
Что вообще считается чувствительными данными
Прежде чем говорить про инструменты, нужно понять, что вообще нужно защищать. Список короткий, но важный:
- Токены авторизации — JWT, OAuth-токены, session ID
- Пароли и PIN-коды — даже если они захешированы
- Приватные ключи — для криптографии, подписи запросов
- Личные данные пользователя — номер карты, паспортные данные, медицинская информация
- API-ключи — особенно те, что дают доступ к платным сервисам
Всё остальное — настройки интерфейса, кеш, история поиска — можно хранить в UserDefaults или CoreData без особых церемоний.
Почему UserDefaults — плохой выбор для секретов
UserDefaults хранит данные в обычном .plist-файле внутри папки приложения. На незаджейлбрейкнутом устройстве получить к нему доступ напрямую сложно, но не невозможно:
- Резервные копии iTunes/iCloud — если пользователь делает бэкап, файл
UserDefaults попадает в него. Бэкап можно скопировать на компьютер и открыть.
- Отладочные инструменты — через Xcode или сторонние утилиты разработчик может читать
UserDefaults во время отладки.
- Межпроцессный доступ — при использовании App Groups данные
UserDefaults становятся доступны нескольким приложениям сразу.
Итог: токен авторизации в UserDefaults — это как оставить ключ от квартиры под ковриком. Вроде дома никого нет, но всё равно странно.
Keychain — правильное место для секретов
iOS Keychain — это зашифрованное хранилище, которое Apple встроила в операционную систему. Данные в Keychain шифруются аппаратно, привязываются к конкретному устройству и защищены отдельно от остального содержимого приложения.
Главные преимущества:
- Переживает удаление приложения — если пользователь удалил и переустановил приложение, данные в Keychain остаются (если явно не очищать).
- Не попадает в незашифрованные бэкапы — при правильных настройках.
- Синхронизируется через iCloud Keychain — опционально, при желании.
- Поддерживает биометрию — можно потребовать Face ID/Touch ID для чтения.
Базовая работа с Keychain в Swift
Apple предоставляет низкоуровневый C-интерфейс через Security.framework. Он рабочий, но многословный. Вот минимальный пример записи строки:
import Security
func saveToKeychain(key: String, value: String) -> Bool {
let data = value.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary) // удаляем старое, если есть
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
И чтение:
func readFromKeychain(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let string = String(data: data, encoding: .utf8) else {
return nil
}
return string
}
Код работает, но на практике лучше использовать обёртки — KeychainSwift, SwiftKeychainWrapper или написать свою тонкую абстракцию.
Атрибут kSecAttrAccessible — самый важный параметр
Это то, от чего зависит, когда именно iOS разрешает читать данные из Keychain. Значений несколько:
| Значение |
Когда доступно |
kSecAttrAccessibleAlways |
Всегда, даже при заблокированном экране. Устарело, не использовать. |
kSecAttrAccessibleWhenUnlocked |
Только когда устройство разблокировано. |
kSecAttrAccessibleWhenUnlockedThisDeviceOnly |
То же, но не синхронизируется в iCloud. |
kSecAttrAccessibleAfterFirstUnlock |
После первой разблокировки после перезагрузки. Подходит для фоновых задач. |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly |
Только если на устройстве установлен пароль. Максимальная защита. |
Для токенов авторизации оптимальный вариант — kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Для данных, которые нужны в фоне (например, при получении push-уведомлений) — kSecAttrAccessibleAfterFirstUnlock.
Значение kSecAttrAccessibleAlways использовать нельзя — Apple его официально deprecate-нула, и это правильно.
Биометрия для дополнительной защиты
Если в приложении хранятся особо чувствительные данные — банковские реквизиты, медицинские записи — можно потребовать биометрическую аутентификацию при чтении из Keychain.
import LocalAuthentication
func saveSecureItem(key: String, value: String) throws {
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet, // требует Face ID/Touch ID
nil
)!
let data = value.data(using: .utf8)!
let context = LAContext()
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessControl as String: access,
kSecUseAuthenticationContext as String: context
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
if status != errSecSuccess {
throw KeychainError.saveFailed(status)
}
}
Флаг .biometryCurrentSet означает, что если пользователь добавит новый отпечаток пальца или перенастроит Face ID, доступ к данным будет закрыт. Это защита от сценария «злоумышленник добавил свой отпечаток».
Есть ещё флаг .biometryAny — он разрешает доступ при любой биометрии на устройстве, менее строгий вариант.
CryptoKit — шифрование своими руками
IOS 13 принёс CryptoKit — нативный фреймворк для криптографии. Теперь не нужно тащить OpenSSL или CommonCrypto напрямую.
Симметричное шифрование AES-GCM
Подходит, когда нужно зашифровать данные для хранения или передачи:
import CryptoKit
// Генерация ключа
let key = SymmetricKey(size: .bits256)
// Шифрование
func encrypt(data: Data, key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined!
}
// Дешифрование
func decrypt(data: Data, key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: key)
}
AES-GCM — аутентифицированное шифрование. Это значит, что при дешифровании автоматически проверяется целостность данных. Если кто-то подменил байты в зашифрованном файле — расшифровка завершится ошибкой.
Где хранить ключ шифрования
Вот типичная ошибка: генерируют ключ шифрования, шифруют им данные, а сам ключ сохраняют рядом — в UserDefaults или в том же файле. Смысла в таком шифровании ноль.
Правильная схема:
- Генерируем ключ один раз при первом запуске.
- Сохраняем ключ в Keychain с атрибутом
kSecAttrAccessibleWhenUnlockedThisDeviceOnly.
- Шифруем этим ключом данные в CoreData или файловой системе.
Таким образом, даже если злоумышленник получит доступ к файлам приложения, без ключа из Keychain он ничего не расшифрует.
Асимметричное шифрование и Secure Enclave
Secure Enclave — отдельный защищённый процессор внутри чипов Apple (начиная с A7). Ключи, созданные в Secure Enclave, никогда не покидают его — даже само приложение не видит приватный ключ напрямую.
import CryptoKit
// Создание ключа в Secure Enclave
let privateKey = try SecureEnclave.P256.Signing.PrivateKey()
let publicKey = privateKey.publicKey
// Подпись данных
let dataToSign = "важные данные".data(using: .utf8)!
let signature = try privateKey.signature(for: dataToSign)
// Верификация
let isValid = publicKey.isValidSignature(signature, for: dataToSign)
Secure Enclave поддерживает только P-256 (secp256r1). Для шифрования используют ECIES — гибридную схему, где P-256 согласует симметричный ключ, а им уже шифруют данные.
Это уровень защиты банков и платёжных приложений. Если разрабатываете что-то с реальными деньгами — Secure Enclave обязателен.
Распространённые ошибки
Хранить токены в UserDefaults. Уже обсудили — не делайте так.
Логировать чувствительные данные. print(token) в дебаг-сборке — кажется безобидным, но логи могут попасть в crash-репорты, системные логи, Xcode Organizer.
Не чистить Keychain при выходе из аккаунта. Пользователь нажал «Выйти» — токен должен быть удалён. Не просто помечен как неактивный, а физически удалён из Keychain.
Использовать одинаковый kSecAttrService для разных окружений. Если приложение работает с dev/staging/prod — данные могут перемешаться. Используйте разные идентификаторы для каждого окружения.
Не обрабатывать ошибки Keychain. SecItemCopyMatching возвращает статус. errSecItemNotFound — нормальная ситуация, когда пользователь впервые запустил приложение. Игнорировать ошибки нельзя.
Практическая схема для большинства приложений
Вот минимальная рабочая архитектура хранения данных для типичного iOS-приложения:
- Токен авторизации → Keychain,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
- Настройки пользователя (тема, язык, уведомления) →
UserDefaults
- Кеш данных (список товаров, статьи) → CoreData или файловая система
- Зашифрованные данные (если есть) → файловая система, ключ в Keychain
- Биометрически защищённые данные → Keychain с
SecAccessControl и .biometryCurrentSet
Эта схема покрывает 90% приложений. Для fintech и healthtech поверх добавляется Secure Enclave и дополнительный слой проверок целостности.
Проверка защиты на практике
Чтобы убедиться, что всё работает правильно:
- Сделайте незашифрованный бэкап через iTunes и проверьте, видны ли секреты в папке бэкапа.
- Удалите приложение, переустановите — токен не должен читаться, если вы явно его удаляли при выходе.
- Проверьте поведение на симуляторе: Keychain на симуляторе не защищён так, как на реальном устройстве. Тестируйте на железе.
- Используйте
IPHONE_SIMULATOR_DEVICE environment variable или #if targetEnvironment(simulator) чтобы отключать чувствительный код на симуляторе при необходимости.
Безопасность в мобильных приложениях — это не «включить галочку перед релизом», а набор правильных привычек. Keychain решает задачу хранения секретов, CryptoKit — шифрования данных, Secure Enclave — защиты ключей на аппаратном уровне. Каждый инструмент на своём месте.
Если вы заказываете мобильное приложение в студии — уточните, как там обращаются с чувствительными данными. Правильный ответ: Keychain для токенов, никаких секретов в UserDefaults, биометрия там, где это оправдано. В REEXY это закрыто на уровне стандартов разработки — можно уточнить детали через форму на r3xy.ru.