Проверка безопасного чтения архивов iOS на облачном Mac

Проверка безопасного чтения архивов iOS на облачном Mac

Проверка безопасного чтения архивов iOS на облачном Mac

Самый опасный исход чтения архива старой версии приложения в новой — не ошибка, а возврат пустых данных с последующим сохранением поверх исходного файла. Проверьте этот сценарий на облачном Mac: сначала запустите тест декодирования на чистом Foundation, затем прочитайте те же образцы в тестовом таргете iOS. Цель не в том, чтобы один раз успешно записать и прочитать данные, а в том, чтобы убедиться: неверные типы, неизвестные версии и повреждённые входные данные явно отклоняются.

Сначала определите границы чтения

Здесь рассматриваются архивы объектов, созданные с помощью NSKeyedArchiver, а не шифрование. Пример допускает только один корневой объект с номером версии и текстовым полем. Перед проверкой зафиксируйте три условия:

supportsSecureCoding — это декларация типа, а не автоматическая проверка прикладных полей. На стороне чтения всё равно нужно включить requiresSecureCoding, а каждое объектное поле декодировать методом с ограничением допустимых типов.

Безопасное декодирование ограничивает типы объектов, экземпляры которых могут быть созданы. Оно не подтверждает происхождение данных, не обеспечивает конфиденциальность и не ограничивает размер файла. Сначала ограничьте объём входных данных и только потом передавайте их декодеру.

Напишите декодер, который отклоняет некорректные данные

Сохраните следующий код в archive-check.swift. Он зависит только от Foundation; сначала убедитесь, что команда xcrun swift --version выполняется. Номер версии в примере относится к формату архива, а не к версии операционной системы.

import Foundation

final class Memo: NSObject, NSSecureCoding {
    static var supportsSecureCoding: Bool { true }
    let schema: Int
    let text: String

    init(schema: Int = 1, text: String) {
        self.schema = schema
        self.text = text
        super.init()
    }

    required init?(coder: NSCoder) {
        guard coder.containsValue(forKey: "schema"),
              coder.decodeInteger(forKey: "schema") == 1,
              let value = coder.decodeObject(
                  of: NSString.self, forKey: "text"
              ),
              value.length <= 4096 else {
            return nil
        }
        schema = 1
        text = value as String
        super.init()
    }

    func encode(with coder: NSCoder) {
        coder.encode(schema, forKey: "schema")
        coder.encode(text as NSString, forKey: "text")
    }
}

enum ArchiveFailure: Error {
    case invalidSize
    case invalidObject
}

func readMemo(_ data: Data) throws -> Memo {
    guard !data.isEmpty, data.count <= 1_048_576 else {
        throw ArchiveFailure.invalidSize
    }
    let reader = try NSKeyedUnarchiver(forReadingFrom: data)
    reader.requiresSecureCoding = true
    reader.decodingFailurePolicy = .setErrorAndReturn
    let memo = reader.decodeObject(
        of: Memo.self, forKey: NSKeyedArchiveRootObjectKey
    )
    reader.finishDecoding()
    if let error = reader.error {
        throw error
    }
    guard let memo else {
        throw ArchiveFailure.invalidObject
    }
    return memo
}

func pack(_ value: Any) throws -> Data {
    try NSKeyedArchiver.archivedData(
        withRootObject: value, requiringSecureCoding: true
    )
}

Сначала код проверяет наличие поля версии: иначе значение по умолчанию при декодировании целого числа могло бы скрыть отсутствие ключа. Длина текста ограничивается в кодовых единицах UTF-16, в соответствии с NSString.length. В рабочей реализации выбирайте предел по требованиям формата данных продукта, а не считайте порог из примера универсальным стандартом безопасности.

Политику setErrorAndReturn необходимо установить до чтения объекта. Не рассчитывайте, что внешний do/catch перехватит любые сбои декодирования: обработка некоторых недопустимых объектов определяется политикой ошибок декодера и не сводится к обычному механизму выбрасывания ошибок Swift.

Соберите минимальный регрессионный набор из четырёх входов

Добавьте следующий код в тот же файл. Корректный образец должен вернуть исходный текст; остальные три должны быть отклонены. При любом отклонении от ожидаемого результата проверочная программа должна завершаться с ненулевым кодом.

func expectRejection(_ data: Data) {
    do {
        _ = try readMemo(data)
    } catch {
        return
    }
    fatalError("Unexpected archive acceptance")
}

let valid = try pack(Memo(text: "draft"))
let restored = try readMemo(valid)
guard restored.text == "draft" else {
    fatalError("Round-trip mismatch")
}

expectRejection(try pack("wrong root" as NSString))
expectRejection(try pack(Memo(schema: 2, text: "future")))
expectRejection(Data(valid.prefix(12)))

Сначала скомпилируйте программу, затем запустите её: так среда интерпретируемого выполнения не повлияет на имена типов в образцах, используемых далее.

set -eu
xcrun swiftc archive-check.swift -o archive-check
./archive-check

При обработке некорректных образцов Foundation может вывести диагностику в стандартный поток ошибок. Само по себе это не означает провала проверки. Ориентируйтесь на код завершения процесса и проверяемые условия, а не на полное отсутствие сообщений в журнале. Если корневой объект неверного типа был принят, исправьте список разрешённых классов, а не удаляйте негативный сценарий ради зелёного теста.

Добавьте вложенные объекты и исторические образцы

Задавайте разрешённые типы для каждого поля

Если реальный архив содержит массивы, разрешить только NSArray недостаточно, чтобы считать их элементы безопасными. Используйте перегрузку decodeObject(of:forKey:), принимающую набор классов: включите в него и контейнер, и ожидаемые классы элементов. Затем проверьте количество элементов, длину полей и связи между объектами. Не добавляйте слишком общий NSObject только для того, чтобы обойти ошибку.

Ограничения типов в архиве не заменяют контроль сложности графа объектов. Ограничение размера входных данных тоже не гарантирует ограниченного потребления памяти после декодирования. Для недоверенных данных дополнительно используйте формат с жёсткими ограничениями или отдельный изолированный контур обработки.

Сохраните реальные файлы старых версий

Добавьте обезличенные архивы старых версий в репозиторий как тестовые ресурсы. Укажите версию приложения, создавшего архив, ожидаемые поля и допустимые результаты миграции. Динамически создаваемые образцы подходят для проверки логики отклонения, но не доказывают, что реальные байты, записанные старой версией, по-прежнему читаются.

Имя класса Swift во время выполнения может измениться при смене модуля. При переносе типа или переименовании модуля сначала проверьте исторические образцы, а затем настройте сопоставление классов по явно заданным старым именам. Нельзя считывать произвольные имена классов из входных данных и автоматически разрешать их. После успешной проверки в командной строке выполните ту же проверку с тем же набором файлов в тестовом таргете iOS, чтобы учесть различия целевой среды выполнения.

Остановите ошибку на этапе чтения, не допуская её до сохранения

Представляйте результат декодирования как успех либо явную ошибку; избегайте конструкции try? с подстановкой пустого объекта. При ошибке чтения сохраняйте исходный файл и показывайте в интерфейсе способ восстановления. Заменять старый файл можно только после завершения миграции и успешного проверочного чтения новых данных.

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

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

Часто задаваемые вопросы

Достаточно ли свойства supportsSecureCoding?

Нет. При чтении включите requiresSecureCoding, ограничьте классы через типизированные методы decodeObject и отдельно проверьте версию и значения полей.

Можно ли вернуть пустой список при ошибке чтения?

Не выдавайте ошибку за успешный пустой результат. Сохраните исходный файл и верните явную ошибку, чтобы последующая запись не уничтожила данные.

Выделенный облачный Mac для следующей сборки

Выделенный физический Mac mini, не виртуальная машина. Выберите срок аренды: день, неделя, месяц или квартал. Конфигурация и фактическая доступность указаны в консоли.