Проверка шифрованных записей iOS на облачном Mac

Проверка шифрованных записей iOS на облачном Mac

Проверка шифрованных записей iOS на облачном Mac

В офлайн-заметках содержимое шифруется при сохранении и расшифровывается при чтении. Проверить штатный сценарий обычно несложно. Настоящая опасность возникает, когда файл усечён или выбран неверный ключ, а приложение подавляет ошибку и затем сохраняет пустую запись. Вынеся эту логику в отдельный Swift Package на облачном Mac, можно проверить поведение при сбоях ещё до подключения интерфейса. В этой статье проверяется только целостность шифрованных записей; хранение ключей и система учётных записей остаются за её рамками.

Сначала определим формат записи и поведение при ошибках

Используем AES-GCM из CryptoKit и сохраняем запись в виде байтов, возвращаемых SealedBox.combined. Автоматически созданный nonce, шифротекст и тег аутентификации хранятся вместе; при чтении необходимо восстановить всю структуру целиком. Не обрезайте тег самостоятельно и не считайте nonce секретом.

С одним и тем же ключом нельзя повторно использовать nonce. В обычных вызовах достаточно поручить его создание AES.GCM.seal; не выводите его из имени файла, текущего времени в секундах или порядкового номера записи. Ключи временно создаются внутри тестов и не попадают ни в репозиторий, ни в отчёты о тестировании.

Передавать содержимое в бизнес-логику можно только после успешной расшифровки. Ошибка аутентификации не означает пустое содержимое и не должна запускать автоматическую перезапись исходного файла.

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

Создадим пакет, в котором можно запустить тесты

На выделенном физическом узле с установленным набором инструментов Xcode выполните следующие команды от имени обычного пользователя-разработчика. Используйте новый рабочий каталог, чтобы не перезаписать существующий проект. Перед инициализацией убедитесь, что swift --version выполняется без ошибок:

mkdir SecureRecord
cd SecureRecord
swift --version
swift package init --type library

Замените содержимое созданного файла Tests/SecureRecordTests/SecureRecordTests.swift кодом из следующего раздела. В исходном файле можно оставить реализацию по умолчанию: эти тесты напрямую вызывают системный CryptoKit и не требуют загрузки сторонних зависимостей.

Здесь используется XCTest, чтобы пример не зависел от специфического синтаксиса нового фреймворка тестирования. В реальном проекте вспомогательные функции следует перенести в производственный модуль и импортировать его в тесты. Иначе можно получить ситуацию, в которой тестовая реализация верна, а приложение использует другую.

Проверим корректную запись и четыре нештатных сценария

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

import Foundation
import CryptoKit
import XCTest

final class SecureRecordTests: XCTestCase {
    private func encode(
        _ data: Data,
        key: SymmetricKey
    ) throws -> Data {
        let box = try AES.GCM.seal(data, using: key)
        return try XCTUnwrap(box.combined)
    }

    private func decode(
        _ record: Data,
        key: SymmetricKey
    ) throws -> Data {
        let box = try AES.GCM.SealedBox(combined: record)
        return try AES.GCM.open(box, using: key)
    }

    func testRoundTripAndFreshNonce() throws {
        let key = SymmetricKey(size: .bits256)
        let input = Data("draft-v1".utf8)
        let first = try encode(input, key: key)
        let second = try encode(input, key: key)

        XCTAssertEqual(try decode(first, key: key), input)
        XCTAssertEqual(try decode(second, key: key), input)
        XCTAssertNotEqual(first, second)
    }

    func testChangedTagIsRejected() throws {
        let key = SymmetricKey(size: .bits256)
        var record = try encode(Data("draft-v1".utf8), key: key)
        let last = record.index(before: record.endIndex)
        record[last] ^= 0x01

        XCTAssertThrowsError(try decode(record, key: key))
    }

    func testWrongKeyIsRejected() throws {
        let key = SymmetricKey(size: .bits256)
        let otherKey = SymmetricKey(size: .bits256)
        let record = try encode(Data("draft-v1".utf8), key: key)

        XCTAssertThrowsError(try decode(record, key: otherKey))
    }

    func testTruncatedRecordsAreRejected() throws {
        let key = SymmetricKey(size: .bits256)
        let record = try encode(Data("draft-v1".utf8), key: key)

        for length in [0, 1, record.count - 1] {
            let truncated = Data(record.prefix(length))
            XCTAssertThrowsError(try decode(truncated, key: key))
        }
    }

    func testEmptyPlaintextIsValid() throws {
        let key = SymmetricKey(size: .bits256)
        let record = try encode(Data(), key: key)

        XCTAssertFalse(record.isEmpty)
        XCTAssertEqual(try decode(record, key: key), Data())
    }
}

Запустите swift test: все пять тестов должны пройти. Даже пустой открытый текст создаёт непустую запись, поскольку в неё также входят nonce и тег. Проверка «длина файла больше нуля» не заменяет расшифровку с проверкой аутентичности.

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

Свяжем ошибки алгоритма с поведением приложения

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

Проверяйте формат и бизнес-данные отдельно

Даже если полная запись успешно расшифрована, декодирование бизнес-данных всё ещё может завершиться ошибкой. Разделите проверку версии формата, расшифровку с проверкой аутентичности и разбор содержимого на три шага, каждый из которых возвращает различимую внутреннюю ошибку. Пользователю достаточно краткого сообщения. В журнал записывайте только этап и категорию ошибки, без открытого текста и ключей.

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

Сохраним воспроизводимые результаты приёмки

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

В завершение проведите интеграционную проверку на iOS: передайте запись, действительно созданную приложением, той же функции декодирования, а затем убедитесь, что чтение повреждённого файла не перезаписывает исходное содержимое. Облачный Mac подходит для быстрых тестов общего криптографического ядра; доступ к файлам и восстановление через интерфейс нужно отдельно проверять в целевой сборке приложения. Итоговый критерий — «некорректные данные отвергнуты, исходные данные сохранены, пользователь может восстановить запись», а не просто один успешный цикл шифрования и расшифровки.

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

Нужен ли для этих тестов симулятор iOS?

Чистую логику CryptoKit можно проверить командой swift test на облачном Mac. Чтение файлов, сообщения об ошибках и интеграцию приложения проверяют отдельно на iOS.

Можно ли возвращать пустые данные при ошибке аутентификации?

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

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

Выберите модель, город и срок аренды, чтобы работать со своими инструментами разработки на выделенном физическом узле. Доступность проверяйте по актуальным данным в консоли.

Превратите чтение в следующую сборку

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