Verschlüsselte iOS-Datensätze auf einem Cloud-Mac prüfen

Verschlüsselte iOS-Datensätze auf einem Cloud-Mac prüfen

Verschlüsselte iOS-Datensätze auf einem Cloud-Mac prüfen

Offline-Notizen werden beim Speichern verschlüsselt und beim Lesen entschlüsselt. Der reguläre Ablauf lässt sich meist problemlos testen. Wirklich gefährlich wird es, wenn eine Datei abgeschnitten ist oder der falsche Schlüssel verwendet wird, die Anwendung den Fehler aber verschluckt und anschließend einen leeren Datensatz speichert. Auf einem Cloud-Mac lässt sich diese Logik in ein eigenständiges Swift Package auslagern, um das Fehlerverhalten schon vor der Anbindung an die Oberfläche zu prüfen. Dieser Artikel behandelt ausschließlich die Integrität verschlüsselter Datensätze, nicht die dauerhafte Speicherung von Schlüsseln oder die Benutzerkontenverwaltung.

Zuerst Datensatz und Fehlerverhalten festlegen

Wir verwenden AES-GCM aus CryptoKit und speichern den Datensatz als die von SealedBox.combined zurückgegebenen Bytes. Die automatisch erzeugte Nonce, der Chiffretext und das Authentifizierungstag werden gemeinsam gespeichert und müssen beim Lesen vollständig wiederhergestellt werden. Kürzen Sie das Tag nicht selbst und behandeln Sie die Nonce nicht als Geheimnis.

Mit demselben Schlüssel darf eine Nonce nicht erneut verwendet werden. Lassen Sie sie bei regulären Aufrufen einfach von AES.GCM.seal erzeugen. Leiten Sie sie nicht aus dem Dateinamen, der aktuellen Zeit in Sekunden oder der laufenden Nummer eines Datensatzes ab. Die Schlüssel werden nur vorübergehend innerhalb der Tests erzeugt und gelangen weder ins Repository noch in Testberichte.

Inhalte dürfen erst nach erfolgreicher Entschlüsselung an die Geschäftslogik übergeben werden. Eine fehlgeschlagene Authentifizierung bedeutet nicht, dass der Inhalt leer ist, und darf kein automatisches Überschreiben der Originaldatei auslösen.

Benötigt die Geschäftslogik externe Felder wie eine Datensatz-ID, können diese kanonisch kodiert und als zusätzliche authentifizierte Daten übergeben werden. Verschlüsselung und Entschlüsselung müssen dabei dieselben Bytes erhalten. Hier prüfen wir zunächst einen minimalen Datensatz ohne externe Felder.

Ein ausführbares Testpaket anlegen

Führen Sie die folgenden Befehle auf einem dedizierten physischen Knoten mit installierter Xcode-Toolchain unter einem normalen Entwicklerkonto aus. Verwenden Sie ein neu angelegtes Arbeitsverzeichnis und überschreiben Sie kein bestehendes Projekt. Prüfen Sie vor der Initialisierung, ob swift --version erfolgreich ausgeführt wird:

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

Ersetzen Sie den Inhalt der erzeugten Datei Tests/SecureRecordTests/SecureRecordTests.swift durch den Code im nächsten Abschnitt. Die Quelldatei kann ihre Standardimplementierung behalten: Diese Tests rufen direkt das systemseitige CryptoKit auf und benötigen keine heruntergeladenen Drittanbieter-Abhängigkeiten.

Wir verwenden hier XCTest, damit das Beispiel nicht von der speziellen Syntax eines neueren Testframeworks abhängt. In einem echten Projekt sollten die Hilfsfunktionen in das Produktionsmodul verschoben und dieses Modul in die Tests importiert werden. So vermeiden Sie, dass die Testimplementierung korrekt ist, die Anwendung aber eine andere Implementierung nutzt.

Einen gültigen Datensatz und vier Sonderfälle abdecken

Nachfolgend steht eine vollständige Testdatei. Der reguläre Ablauf weist zunächst nach, dass nach dem Ver- und Entschlüsseln dieselben Bytes vorliegen. Anschließend werden ein verändertes Tag, ein falscher Schlüssel, abgeschnittene Eingabedaten und ein zulässiger leerer Inhalt separat geprüft. Jeder Test verwendet einen eigenen Schlüssel; es gibt keinen gemeinsam genutzten veränderlichen Zustand.

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())
    }
}

Führen Sie swift test aus. Alle fünf Tests müssen erfolgreich sein. Auch leerer Klartext ergibt einen nicht leeren Datensatz, weil die gekapselte Darstellung zusätzlich die Nonce und das Tag enthält. Die Prüfung „Dateilänge größer als null“ ersetzt keine authentifizierte Entschlüsselung.

Die Assertion, dass sich die beiden Chiffretexte unterscheiden, soll Regressionen aufdecken, bei denen die Implementierung versehentlich eine feste Nonce verwendet. Sie ist kein Nachweis für die Qualität der Zufallszahlen. Die Tests vergleichen auch keine vollständigen Chiffretext-Snapshots, da sonst die erwartete Zufälligkeit als Fehler gewertet würde.

Algorithmusfehler mit dem Anwendungsverhalten verknüpfen

Erfolgreiche Tests für Authentifizierungsfehler zeigen lediglich, dass die untere Schicht ungültige Bytes zurückweist. Ergänzen Sie Abnahmeprüfungen für den aufrufenden Code: Nach einem Lesefehler muss die Anwendung einen Fehler mit Wiederherstellungsmöglichkeit anzeigen, die Originaldatei erhalten und das automatische Speichern eines leeren Modells im Hintergrund verhindern. Gibt es eine Kopie der vorherigen Version, muss sie über einen ausdrücklichen Wiederherstellungsablauf ausgewählt werden, statt unbemerkt auf sie umzuschalten.

Format und Nutzdaten getrennt prüfen

Auch wenn ein vollständiger Datensatz erfolgreich entschlüsselt wurde, kann die Dekodierung der Nutzdaten noch scheitern. Teilen Sie die Prüfung der Formatversion, die authentifizierte Entschlüsselung und das Parsen des Inhalts in drei Schritte auf, die jeweils unterscheidbare interne Fehler zurückgeben. Für Benutzer genügt eine kurze Meldung. Protokollieren Sie nur die Verarbeitungsphase und die Fehlerkategorie, niemals Klartext oder Schlüssel.

Begrenzen Sie außerdem die Eingabegröße: Prüfen Sie zuerst die Dateilänge und laden und entschlüsseln Sie die Datei erst danach, damit ungewöhnlich große Dateien nicht den gesamten Arbeitsspeicher belegen. Die Obergrenze muss sich aus der für die Anwendung zulässigen Datensatzgröße ergeben; übernehmen Sie keine beliebige Zahl. Wenn Sie eine neue Formatversion einführen, bewahren Sie sowohl gültige Beispiele der alten Version als auch Beispiele einer ausdrücklich nicht unterstützten Version auf.

Reproduzierbare Abnahmenachweise festhalten

Committen Sie die Testdatei, die Formatbeschreibung und die Ausführungsbefehle. Halten Sie für jeden Lauf die Commit-ID, die Swift-Version, die Anzahl der Tests und den Exit-Status fest. Laden Sie keine echten Benutzerdatensätze hoch. Während der Entwicklung können Sie Tests mit byteweisen Änderungen ergänzen. Scheitert ein Test mit einer zufälligen Mutation, müssen jedoch die Änderungsposition und die Eingabelänge erhalten bleiben.

Führen Sie abschließend eine iOS-Integrationsprüfung durch: Übergeben Sie einen tatsächlich von der Anwendung geschriebenen Datensatz an dieselbe Dekodierfunktion und prüfen Sie anschließend, dass das Lesen einer beschädigten Datei den ursprünglichen Inhalt nicht überschreibt. Ein Cloud-Mac eignet sich für schnelle Tests des gemeinsam genutzten Verschlüsselungskerns; Dateizugriff und Wiederherstellung über die Oberfläche müssen im App-Target separat abgenommen werden. Der abschließende Maßstab lautet: „Ungültige Daten werden zurückgewiesen, die Originaldaten bleiben erhalten, und Benutzer können sie wiederherstellen“ — nicht bloß ein einmal erfolgreiches Ver- und Entschlüsseln.

Häufig gestellte Fragen

Brauchen diese Tests einen iOS-Simulator?

Reine CryptoKit-Logik lässt sich auf dem Cloud-Mac mit swift test prüfen. Dateizugriff, Fehlermeldungen und die App-Integration benötigen zusätzliche Tests im iOS-Ziel.

Darf ein Authentifizierungsfehler leere Daten liefern?

Nicht als erfolgreiches Ergebnis. Bewahren Sie die ursprünglichen Bytes auf und melden Sie einen Fehler, damit gültige leere Inhalte unterscheidbar bleiben.

Miete einen Cloud-Mac für deinen nächsten Build

Wähle Modell, Standort und Mietdauer und nutze einen dedizierten physischen Knoten für deine Entwicklungswerkzeuge. Die Verfügbarkeit wird in Echtzeit über die Konsole angezeigt.

Mach aus der Lektüre deinen nächsten Build

Prüfe zuerst Modell und Mietdauer und plane dann dein Projekt.