Wenn alle Tests auf einem entfernten Build-Knoten erfolgreich sind, bedeutet das noch nicht, dass die von der App auf den Datenträger geschriebenen Daten wie vorgesehen geschützt sind. Die häufigste Abweichung entsteht nicht dadurch, dass ein Entwickler den Aufruf einer Verschlüsselungsschnittstelle vergisst. Vielmehr wird die ursprünglich festgelegte Schutzklasse nach einer atomaren Ersetzung, einer Datenbankmigration oder dem Neuaufbau eines Caches mitunter nicht auf die endgültige Datei angewendet. Wird diese Prüfung in die Continuous-Integration-Pipeline auf einem Cloud-Mac aufgenommen, validiert jeder Merge die tatsächlichen Dateiattribute, statt lediglich eine bestimmte Codezeile für den Schreibvorgang zu kontrollieren.
Zuerst festlegen, welche Daten geschützt werden müssen
Beim iOS-Dateischutz ist die strengste Einstellung nicht immer die beste. Eine App sollte ihre Daten zunächst danach klassifizieren, ob sie nach dem Sperren des Geräts weiterhin lesbar sein müssen, und anschließend die passende Schutzklasse wählen. Token, offline verfügbare Geschäftsdaten und von Benutzern exportierte private Inhalte sollten bei gesperrtem Gerät in der Regel nicht zugänglich sein. Warteschlangen für Hintergrundübertragungen, notwendige Push-Statusdaten oder Aufgaben, die während der Sperre weiterlaufen müssen, sind dagegen gesondert zu bewerten.
| Datentyp | Empfohlene Basis | Prüfschwerpunkt |
|---|---|---|
| Exportdatei mit Sitzungstoken | Vollständiger Schutz | Darf nach dem Sperren nicht lesbar sein |
| Private Benutzerdokumente | Vollständiger Schutz | Attribute bleiben nach Erstellung, Ersetzung und Wiederherstellung identisch |
| Status von Hintergrundaufgaben | Nach dem ersten Entsperren verfügbar | Bestätigen, dass der Zugriff während der Sperre tatsächlich erforderlich ist |
| Neu erzeugbarer Cache | Nach Risiko festlegen | Darf keine Token oder personenbezogenen Daten enthalten |
| SQLite-Datenbank | An die Geschäftsdaten anpassen | WAL- und SHM-Dateien ebenfalls prüfen |
Das gesamte Documents- oder Library-Verzeichnis darf nicht als einheitlicher Datentyp behandelt werden. Für die Prüfung sollte eine eindeutige Liste gepflegt werden, die den relativen Pfad, die erwartete Schutzklasse, den Vorgang zur Erstellung der Datei und die Begründung für zulässige Ausnahmen dokumentiert.
Geprüft werden müssen die Dateien, die nach dem Testlauf tatsächlich vorhanden sind. Wer nur nach
.completeFileProtectionsucht, übersieht Begleitdateien von Datenbanken, durch Ersetzungen entstandene neue Inodes und Daten, die von Drittanbieterkomponenten angelegt wurden.
Endgültige Dateiattribute mit XCTest auslesen
Der Test sollte Daten zunächst über den regulären Anwendungspfad schreiben und anschließend fileProtection von der endgültigen URL auslesen. Die folgende Hilfsmethode meldet sowohl ein fehlendes Attribut als auch den tatsächlichen Wert in der Fehlermeldung. Dadurch lässt sich die Ursache direkt anhand der CI-Protokolle ermitteln.
import XCTest
final class FileProtectionTests: XCTestCase {
private func assertProtection(
_ url: URL,
equals expected: URLFileProtection,
file: StaticString = #filePath,
line: UInt = #line
) throws {
XCTAssertTrue(
FileManager.default.fileExists(atPath: url.path),
"Expected file does not exist: \(url.path)",
file: file,
line: line
)
let values = try url.resourceValues(forKeys: [.fileProtectionKey])
XCTAssertEqual(
values.fileProtection,
expected,
"Unexpected protection at \(url.path): \(String(describing: values.fileProtection))",
file: file,
line: line
)
}
func testSensitiveExportUsesCompleteProtection() throws {
let root = FileManager.default.temporaryDirectory
let url = root.appendingPathComponent("private-export.json")
let payload = Data(#"{"status":"ready"}"#.utf8)
try payload.write(to: url, options: [.atomic, .completeFileProtection])
try assertProtection(url, equals: .complete)
}
}
Testdaten dürfen keine echten Zugangsdaten enthalten. Auch die Pfade sollten dynamisch im Testcontainer erzeugt werden, damit sie weder vom Benutzernamen eines bestimmten Knotens noch von einem festen Arbeitsverzeichnis abhängen. Unterstützt das Produkt das Überschreiben vorhandener Dateien, muss der Test zwei Schreibvorgänge unmittelbar nacheinander ausführen. Der zweite Schreibvorgang verwendet häufig eine temporäre Datei, die anschließend umbenannt wird.
Ausnahmen in einer expliziten Liste erfassen
Dateien, die tatsächlich .completeUntilFirstUserAuthentication benötigen, müssen in den Tests einzeln benannt werden. Eine beliebige Schutzklasse darf nicht allein deshalb akzeptiert werden, weil sie kein vollständiger Schutz ist. Die Ausnahmeliste muss sowohl den geschäftlichen Anwendungsfall als auch die verantwortliche Person angeben. Sobald eine Funktion während der Gerätesperre keinen Zugriff mehr benötigt, ist die Ausnahme zu entfernen und die Schutzklasse anzuheben.
SQLite-Begleitdateien nicht übersehen
Im WAL-Modus kann SQLite die Dateien database.sqlite-wal und database.sqlite-shm anlegen. Wird nur die Hauptdatei geprüft, entsteht ein falscher Eindruck von Sicherheit. Der Test sollte zunächst eine echte Schreibtransaktion ausführen, das Vorhandensein der Begleitdateien bestätigen und anschließend für alle drei Dateien dieselbe Assertion anwenden.
Bei Core Data sollte der Test Datensätze über den persistenten Container einfügen und speichern, statt manuell eine leere Datenbank anzulegen. Einige Begleitdateien verschwinden, sobald die Verbindung geschlossen wird. Die Prüfung muss deshalb nach Abschluss der Schreibtransaktion, aber vor dem Schließen des Speichers erfolgen. Folgende Angaben sollten protokolliert werden:
- Die absoluten Pfade der Hauptdatenbank sowie der WAL- und SHM-Dateien;
- Die tatsächliche Schutzklasse jeder Datei;
- Der Testschritt, durch den die jeweilige Datei angelegt wurde;
- Ob eine fehlende Datei auf eine erwartete Bereinigung hindeutet oder darauf, dass der Test keinen echten Schreibvorgang ausgeführt hat.
Bei Abläufen, in denen eine Datenbank heruntergeladen und anschließend importiert wird, muss die gesamte Kette abgedeckt sein: Download in eine temporäre Datei, Validierung und Verschieben in das endgültige Verzeichnis. Korrekte Attribute des Zielverzeichnisses garantieren nicht automatisch, dass die verschobene Datei dieselbe Richtlinie übernimmt.
Die Build-Prüfung auf einem Cloud-Mac integrieren
Die Prüfung kann als separater Testplan ausgeführt werden, damit nicht jedes Mal sämtliche UI-Tests laufen müssen. Zunächst werden Projekt, Scheme, Simulatorgerät und Pfad des Ergebnispakets fest vorgegeben. Anschließend entscheidet der Rückgabecode von xcodebuild, ob die Pipeline fortgesetzt wird.
set -euo pipefail
RESULT_DIR="${PWD}/artifacts"
RESULT_BUNDLE="${RESULT_DIR}/FileProtection.xcresult"
rm -rf "${RESULT_BUNDLE}"
mkdir -p "${RESULT_DIR}"
xcodebuild test \
-project App.xcodeproj \
-scheme SecurityRegression \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath "${RESULT_BUNDLE}" \
-only-testing:AppSecurityTests/FileProtectionTests
Der Gerätename muss zu den auf dem Knoten installierten Laufzeitumgebungen passen. Es darf nicht vorausgesetzt werden, dass alle Umgebungen identisch sind. Vor der Ausführung lässt sich das Ziel mit xcrun simctl list devices available überprüfen. In der produktiven Pipeline sollten die zulässigen Geräte und Betriebssystemversionen in der Konfiguration festgelegt werden. Ist das Ziel nicht vorhanden, muss die Pipeline unmittelbar fehlschlagen, statt unbemerkt auf eine andere Umgebung auszuweichen.
Bei einem Fehler sollten mindestens die Datei .xcresult, die Standardausgabe der Tests und die Liste der Schutzklassen aufbewahrt werden. Sensible Beispieldaten aus dem Testcontainer dürfen nicht archiviert werden. Ebenso sollte der Dateiinhalt nicht allein zur bequemeren Fehleranalyse ausgegeben werden. Die Nachweise müssen beantworten, welcher Pfad betroffen ist, welcher Wert erwartet wurde, welcher Wert tatsächlich vorlag und durch welchen Testschritt die Datei entstand, ohne dabei Geschäftsdaten zu kopieren.
Grenzen des Simulators und typische Fehlinterpretationen berücksichtigen
Der Simulator eignet sich, um zu prüfen, ob der Code die Schutzattribute auf die endgültigen Dateien anwendet. Er kann jedoch die Verfügbarkeit von Schlüsseln nach dem Sperren eines physischen Geräts nicht vollständig nachbilden. Diese CI-Prüfung erkennt daher Regressionen bei Attributen. Das tatsächliche Zugriffsverhalten beim Sperren, Neustarten und Aufwecken im Hintergrund muss weiterhin in einem kontrollierten Ablauf auf realen Geräten validiert werden.
Eine weitere Fehlinterpretation entsteht durch die Reihenfolge der Bereinigung. Schließt ein Test zuerst die Datenbank oder löscht das temporäre Verzeichnis und prüft erst danach die Begleitdateien, lautet das Ergebnis lediglich „Datei nicht vorhanden“. Ob eine sichere Bereinigung stattgefunden hat oder der Test den vorgesehenen Pfad gar nicht ausgeführt hat, bleibt dabei offen. Assertions müssen an eindeutig definierten Punkten des Lebenszyklus erfolgen. Eine fehlende Datei sollte als Fehler mit Pfadangabe behandelt werden.
Abschließend sind vier Arten von Änderungen zu prüfen: ob die Schreib-API ausgetauscht wurde, ob Dateien atomar ersetzt werden, ob sich der Datenbankmodus geändert hat und ob eine Drittanbieterkomponente einen neuen persistenten Pfad angelegt hat. Sobald sich einer dieser Punkte ändert, muss die Schutzliste erneut bewertet werden. Eine so aufgebaute Prüfung ist nicht vom menschlichen Erinnerungsvermögen abhängig und verwechselt „war früher korrekt konfiguriert“ nicht mit „ist in der aktuell gespeicherten Datei weiterhin korrekt geschützt“.
Häufig gestellte Fragen
Reicht die Prüfung der Schreiboptionen im Quellcode aus?
Nein. Atomare Ersetzung, Migrationen und Bibliotheken können Dateien neu anlegen; deshalb muss CI die tatsächlich gesetzte Schutzklasse der Zieldatei lesen.
Ersetzt der Simulator eine Prüfung auf einem Gerät?
Nein. Der Simulator bestätigt das Attribut, das Zugriffsverhalten nach einer Sperre muss jedoch getrennt auf einem kontrollierten Gerät geprüft werden.
Die nächste Aufgabe auf einem exklusiven physischen Knoten ausführen
Wählen Sie Knoten und Abrechnungszeitraum und starten Sie die Bereitstellung mit der festen Konfiguration aus M4, 16 GB RAM und 256 GB SSD. Die tatsächliche Verfügbarkeit richtet sich nach der Echtzeitanzeige in der Konsole.