Wenn dasselbe Passwort eine Datei unter Windows öffnet, unter iOS aber als falsch zurückgewiesen wird, sollten Sie die Daten nicht vorschnell zurücksetzen. Die Ursache liegt möglicherweise nicht im Verschlüsselungsalgorithmus, sondern in der Passwortkodierung, der Iterationszahl oder der Länge des abgeleiteten Schlüssels. Führen Sie zunächst Tests mit bekannten Sollwerten auf einem Cloud-Mac aus. Lassen Sie anschließend das iOS-Target die tatsächlich verwendete Anwendungskapselung aufrufen, um den Fehler auf eine konkrete Schicht einzugrenzen.
Den Eingabevertrag festlegen
Dieser Artikel richtet sich an plattformübergreifende Projekte mit einem bestehenden PBKDF2-Protokoll. PBKDF2 wird hier nicht als Standardwahl für alle neuen Systeme empfohlen. Dokumentieren Sie zunächst sechs Felder: Algorithmus, PRF, Bytekodierung des Passworts, Salt-Bytes, Iterationszahl und Ausgabelänge. Hier verwenden wir PBKDF2-HMAC-SHA256 mit einer Ausgabe von 32 Byte.
Passwörter werden als UTF-8 kodiert, ohne automatisch Leerraum zu entfernen, die Groß- und Kleinschreibung zu ändern oder eine Unicode-Normalisierung vorzunehmen. Optisch identische Zeichenfolgen können unterschiedliche Bytes enthalten. Definiert das bisherige Protokoll bereits Normalisierungsregeln, müssen diese beibehalten und als Teil der Protokollversion dokumentiert werden.
Tests mit bekannten Sollwerten prüfen, ob die Berechnung korrekt ist. Messungen auf Geräten zeigen, ob der Arbeitsfaktor angemessen ist. Die Einstellungen mit einer oder zwei Iterationen aus den Testvektoren dürfen keinesfalls direkt in die Produktionskonfiguration übernommen werden.
Prüfen Sie außerdem, ob der Salt als rohe Bytes und nicht als Hexadezimaltext übergeben wird. Die Zeichenfolge 73616c74 als Salt und die Bytes des Wortes salt sind zwei unterschiedliche Eingaben.
Standard-Testvektoren ausführen
Speichern Sie den folgenden Code auf einem Mac, auf dem die vollständige Entwicklungs-Toolchain ausgewählt ist, als pbkdf2-check.swift. Er ruft die systemeigene CommonCrypto-Bibliothek direkt auf; zusätzliche Abhängigkeiten müssen nicht heruntergeladen werden. Bei einem Fehler wird die Ausführung abgebrochen, ohne Passwörter oder abgeleitete Ergebnisse auszugeben.
import Foundation
import CommonCrypto
enum DerivationError: Error {
case invalidInput
case failed(Int32)
}
func derive(
password: String,
salt: [UInt8],
rounds: UInt32
) throws -> [UInt8] {
guard rounds > 0, !salt.isEmpty else {
throw DerivationError.invalidInput
}
let passwordBytes = password.utf8CString
var output = [UInt8](repeating: 0, count: 32)
let status = passwordBytes.withUnsafeBufferPointer { p in
salt.withUnsafeBufferPointer { s in
output.withUnsafeMutableBufferPointer { o in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
p.baseAddress,
p.count - 1,
s.baseAddress,
s.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
rounds,
o.baseAddress,
o.count
)
}
}
}
guard status == kCCSuccess else {
throw DerivationError.failed(status)
}
return output
}
func hex(_ bytes: [UInt8]) -> String {
bytes.map { String(format: "%02x", $0) }.joined()
}
let salt = Array("salt".utf8)
let vectors: [(UInt32, String)] = [
(1, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b"),
(2, "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43")
]
for (rounds, expected) in vectors {
let actual = try derive(
password: "password",
salt: salt,
rounds: rounds
)
precondition(hex(actual) == expected)
}
let full = try derive(password: "ab\u{0000}cd", salt: salt, rounds: 2)
let prefix = try derive(password: "ab", salt: salt, rounds: 2)
precondition(full != prefix)
let composed = try derive(password: "\u{00e9}", salt: salt, rounds: 2)
let decomposed = try derive(password: "e\u{0301}", salt: salt, rounds: 2)
precondition(composed != decomposed)
Führen Sie xcrun swift pbkdf2-check.swift aus. Der Exitcode null bedeutet, dass diese Zusicherungen erfüllt wurden. Hier wird das Skript interpretiert ausgeführt; ersetzen Sie dies nicht durch einen optimierten Build, der Zusicherungen deaktiviert.
Der Code übergibt die Bytelänge des Passworts explizit. Dabei wird das abschließende Nullzeichen ausgeschlossen, während eingebettete Nullzeichen erhalten bleiben. Eine Schnittstelle, die die Länge anhand eines Nullterminators bestimmt, würde den restlichen Teil des Passworts abschneiden.
Die Anwendungskapselung testen
Sobald das Skript erfolgreich durchläuft, übernehmen Sie dieselben Testdaten in das XCTest-Target für iOS. Rufen Sie dabei jedoch den Ableitungseinstieg auf, den die Anwendung tatsächlich verwendet, statt lediglich eine eigene Implementierung für die Tests zu kopieren. Andernfalls kann die zugrunde liegende Berechnung korrekt sein, während die darüberliegende Schicht den Salt weiterhin falsch dekodiert.
Ergänzen Sie mindestens diese vier Gruppen von Prüfungen:
- Leere Passwörter: gemäß Protokoll ausdrücklich zulassen oder ablehnen, niemals stillschweigend durch einen anderen Wert ersetzen.
- Chinesische Zeichen, Emojis und eingebettete Nullzeichen: auf beiden Plattformen sowohl die Eingabebytes als auch das Endergebnis prüfen.
- Null Iterationen, fehlerhafte Salt-Kodierungen und unbekannte Algorithmuskennungen: vor dem Aufruf der Ableitungsfunktion zurückweisen.
- Dasselbe Passwort mit unterschiedlichen Salts: anhand fester Testdaten prüfen, dass sich die Ergebnisse unterscheiden, um den versehentlichen Einsatz eines konstanten Salts zu erkennen.
Die Beispielfunktion lehnt einen leeren Salt aufgrund der Eingaberichtlinie dieses Projekts ab. Das bedeutet nicht, dass der Algorithmus keine leeren Salts verarbeiten kann. Erzeugen Sie Salts für den Produktivbetrieb mit der kryptografisch sicheren Zufallsquelle des Systems. Schlägt diese fehl, muss ein Fehler zurückgegeben werden; ein Zeitstempel ist kein zulässiger Ersatz.
Die Testebenen trennen
Das Mac-Skript prüft die korrekte Anbindung des Algorithmus, Tests im Simulator prüfen die Anwendungskapselung, und Tests auf physischen Geräten messen Laufzeit und Auswirkungen auf die Benutzeroberfläche. Dokumentieren Sie auf allen drei Ebenen denselben Eingabevertrag. Setzen Sie die Geschwindigkeit des Macs jedoch nicht mit der eines Smartphones gleich.
Parameter für spätere Aktualisierungen speichern
Ein Ableitungsdatensatz muss mindestens die Formatversion, die Algorithmuskennung, die Iterationszahl, den Salt und die Ausgabelänge enthalten. Falls Textkodierungen verwendet werden, ist auch deren Format festzuhalten, etwa die für den Salt geltenden Base64-Regeln. Ein Salt ist kein Passwort; seine Sicherheit beruht nicht darauf, dass er geheim bleibt.
Prüfen Sie beim Lesen eines Datensatzes zunächst die Feldtypen, Längen und den zulässigen Rechenaufwand, bevor Sie die Ableitung ausführen. Eine beschädigte Datei darf keine extrem hohe Iterationszahl vorgeben können, die die CPU der Anwendung lange auslastet. Legen Sie den Arbeitsfaktor anhand der Sicherheitsanforderungen und von Messungen auf unterstützten Geräten fest, statt ihn aus Testvektoren zu übernehmen.
Überschreiben Sie bei einer Parameteraktualisierung nicht einfach globale Konstanten, um anschließend alte Datensätze einzulesen. Alte Datensätze müssen mit ihren bisherigen Parametern verifiziert werden. Erst wenn der benötigte Klartext erfolgreich vorliegt, darf mit einem neuen Salt und der neuen Richtlinie ein Datensatz der neuen Version erzeugt werden.
Wird das abgeleitete Ergebnis zur Entschlüsselung verwendet, lässt sich alter Geheimtext allein nicht direkt auf die neuen Parameter umstellen. Entschlüsseln Sie ihn zunächst erfolgreich und verschlüsseln Sie ihn anschließend erneut. Behalten Sie den alten Datensatz bei, bis der neue vollständig geschrieben und geprüft wurde. Wenn Sie einen Passwort-Prüfwert speichern, verwenden Sie für den Vergleich eine geprüfte Implementierung mit konstanter Laufzeit, nicht den gewöhnlichen Array-Vergleich aus dem Beispiel.
Prüfungen vor dem Merge einrichten
Sehen Sie in der kontinuierlichen Integration zwei unabhängige Prüfungen vor: Die Standard-Testvektoren müssen Byte für Byte übereinstimmen, und die Anwendung muss Datensätze älterer Versionen lesen sowie ungültige Parameter zurückweisen können. Hinterlegen Sie außerdem einen alten Datensatz ausschließlich für Testzwecke. Prüfen Sie damit, dass er sich nach einer Aktualisierung weiterhin öffnen lässt und neu geschriebene Datensätze tatsächlich die neue Version verwenden.
Fehlerberichte dürfen nur den Namen des Testfalls, den Algorithmus und nicht sensible Parameter enthalten. Laden Sie keine Passwörter, abgeleiteten Schlüssel oder echten Benutzerdatensätze hoch. Führen Sie Leistungsmessungen in kontrollierten Testläufen auf physischen Geräten aus. Ein einzelner Laufzeitwert aus einer gemeinsam genutzten Ausführungsumgebung darf keinen Merge blockieren.
Prüfen Sie abschließend die Fehlerpfade der Migration: Auch nach einem Abbruch, einem Schreibfehler auf dem Datenträger oder dem Beenden der Anwendung muss der alte Datensatz weiterhin lesbar sein. Damit prüfen Sie nicht nur eine Passwortfunktion, sondern einen vollständigen Ablauf, der bei Parameteraktualisierungen keine Daten verliert.
Häufig gestellte Fragen
Sind eine oder zwei Iterationen für den Produktivbetrieb geeignet?
Nein. Diese Werte dienen ausschließlich dem Abgleich mit bekannten Testergebnissen. Der produktive Arbeitsfaktor muss anhand der Sicherheitsvorgaben und Messungen auf Zielgeräten festgelegt werden. Nicht vertrauenswürdige Parameter brauchen zudem eine Rechenobergrenze.
Reicht ein erfolgreicher Test auf dem Mac für iOS aus?
Nein. Das Mac-Skript prüft Algorithmus und Byte-Vertrag. Zusätzlich muss ein iOS-Testziel den tatsächlich verwendeten Anwendungscode aufrufen; Laufzeit und Auswirkungen auf die Bedienung werden auf den Zielgeräten gemessen.
Cloud-Mac mieten
Mieten Sie einen exklusiven physischen Mac tage-, wochen-, monats- oder quartalsweise. Planen Sie Builds nach Bedarf – nach Ablauf endet die Miete automatisch.
Mac auswählen