Разработчики часто недооценивают хранение данных в мобильных приложениях. Токен авторизации кидают в UserDefaults, пароль сохраняют в текстовый файл внутри песочницы — и думают, что раз приложение работает, значит всё нормально. Это не нормально. Рассказываю, где хранить чувствительные данные в iOS и как сделать это правильно.

Что вообще считается чувствительными данными

Прежде чем говорить про инструменты, нужно понять, что вообще нужно защищать. Список короткий, но важный:

  • Токены авторизации — JWT, OAuth-токены, session ID
  • Пароли и PIN-коды — даже если они захешированы
  • Приватные ключи — для криптографии, подписи запросов
  • Личные данные пользователя — номер карты, паспортные данные, медицинская информация
  • API-ключи — особенно те, что дают доступ к платным сервисам

Всё остальное — настройки интерфейса, кеш, история поиска — можно хранить в UserDefaults или CoreData без особых церемоний.

Почему UserDefaults — плохой выбор для секретов

UserDefaults хранит данные в обычном .plist-файле внутри папки приложения. На незаджейлбрейкнутом устройстве получить к нему доступ напрямую сложно, но не невозможно:

  1. Резервные копии iTunes/iCloud — если пользователь делает бэкап, файл UserDefaults попадает в него. Бэкап можно скопировать на компьютер и открыть.
  2. Отладочные инструменты — через Xcode или сторонние утилиты разработчик может читать UserDefaults во время отладки.
  3. Межпроцессный доступ — при использовании 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 или в том же файле. Смысла в таком шифровании ноль.

Правильная схема:

  1. Генерируем ключ один раз при первом запуске.
  2. Сохраняем ключ в Keychain с атрибутом kSecAttrAccessibleWhenUnlockedThisDeviceOnly.
  3. Шифруем этим ключом данные в 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-приложения:

  1. Токен авторизации → Keychain, kSecAttrAccessibleWhenUnlockedThisDeviceOnly
  2. Настройки пользователя (тема, язык, уведомления) → UserDefaults
  3. Кеш данных (список товаров, статьи) → CoreData или файловая система
  4. Зашифрованные данные (если есть) → файловая система, ключ в Keychain
  5. Биометрически защищённые данные → 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.