Как безопасно хранить конфиденциальные данные (токены, пароли) в iOS-приложении?

«Как безопасно хранить конфиденциальные данные (токены, пароли) в iOS-приложении?» — вопрос из категории Базы данных, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Keychain Services — это стандартный и наиболее безопасный способ хранения конфиденциальных данных в iOS, macOS и др. Он обеспечивает аппаратное шифрование и изоляцию данных.

Базовое использование Keychain (без сторонних библиотек):

import Security

// 1. Сохранение данных (например, токена)
func saveToken(_ token: String, for account: String) -> Bool {
    guard let tokenData = token.data(using: .utf8) else { return false }

    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account, // Уникальный идентификатор (e.g., "user_access_token")
        kSecAttrService as String: "com.yourapp.service", // Идентификатор вашего приложения
        kSecValueData as String: tokenData,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked // Уровень доступа
    ]

    // Сначала удаляем старую запись (если есть)
    SecItemDelete(query as CFDictionary)

    // Добавляем новую
    let status = SecItemAdd(query as CFDictionary, nil)
    return status == errSecSuccess
}

// 2. Извлечение данных
func loadToken(for account: String) -> String? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: "com.yourapp.service",
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]

    var dataTypeRef: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)

    guard status == errSecSuccess, let data = dataTypeRef as? Data else { return nil }
    return String(data: data, encoding: .utf8)
}

Ключевые аспекты безопасности:

  • kSecAttrAccessible: Определяет, когда доступны данные.

    • kSecAttrAccessibleWhenUnlocked (рекомендуется по умолчанию) — только когда устройство разблокировано.
    • kSecAttrAccessibleAfterFirstUnlock — после первой разблокировки до перезагрузки.
    • kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly — максимальная защита, требует включенного пароля, не синхронизируется с iCloud.
  • Атрибуты kSecAttrAccessGroup и kSecAttrSynchronizable:

    • Используйте kSecAttrAccessGroup для Shared Keychain между приложениями одной команды.
    • kSecAttrSynchronizable позволяет синхронизировать запись через iCloud Keychain (осторожно с конфиденциальными данными!).

Практические рекомендации:

  1. Не храните в UserDefaults/plist-файлах: Они не шифруются и легко извлекаются с джейлбрейкнутых устройств.
  2. Используйте обертки: Для удобства используйте проверенные библиотеки, такие как KeychainAccess.
    import KeychainAccess
    let keychain = Keychain(service: "com.yourapp.service")
    keychain["user_access_token"] = "eyJhbGciOiJ..."
    let token = keychain["user_access_token"]
  3. Для биометрической аутентификации: Используйте фреймворк LocalAuthentication вместе с Keychain, устанавливая политику доступа kSecAccessControlBiometryCurrentSet в запросе.
  4. Криптографические ключи: Для хранения асимметричных ключей (RSA, EC) используйте kSecClassKey. Для генерации и хранения ключей в Secure Enclave используйте kSecAttrTokenIDSecureEnclave.