Nach der Integration zeitbasierter Einmalpasswörter in ein iOS-Projekt sind oft gerade die Fehler am schwersten nachzustellen, bei denen es heißt: „Eben hat es noch funktioniert, jetzt stimmen die Codes nicht mehr überein.“ Wer eine plattformübergreifende Oberfläche unter Windows entwickelt und die nativen Module anschließend auf einem Cloud-Mac prüft, sollte nicht auf gut Glück immer wieder Codes eingeben. Zeit muss als Eingabe behandelt werden, Standard-Testvektoren als Referenz. Nur so lassen sich Fehler im Algorithmus, Darstellungsfehler und die serverseitige Toleranz für Uhrzeitabweichungen auseinanderhalten.
Den Vertrag festlegen
TOTP wandelt die Unix-Zeit in einen fortlaufenden Zähler um und erzeugt daraus per HMAC einen Zahlencode mit fester Stellenzahl. Zunächst müssen vier Parameter feststehen: die Rohbytes des Schlüssels, der Hashalgorithmus, die Zeitschrittlänge und die Anzahl der Ausgabestellen. Dieser Artikel verwendet die SHA-1-Testkonfiguration aus RFC 6238: Startzeit null, Zeitschrittlänge dreißig Sekunden und acht Ausgabestellen.
SHA-1 dient hier dazu, die Kompatibilität anhand standardisierter Testvektoren zu prüfen. Das ist keine Empfehlung, für ein neues Protokoll beliebig einen Algorithmus auszuwählen. Vereinbaren beide Seiten einen anderen Hashalgorithmus, müssen auch die zugehörigen Testvektoren ausgetauscht werden. Nur den Client zu ändern, reicht nicht aus.
Der Base32-kodierte Schlüsseltext, der häufig auf Konfigurationsseiten steht, ist nicht die direkte Eingabe für HMAC. Er muss zunächst gemäß dem vereinbarten Format dekodiert werden; erst dann werden die Rohbytes übergeben. Die UTF-8-Bytes des Textes dürfen nicht mit dem dekodierten Schlüssel verwechselt werden.
Der folgende Schlüssel ist öffentliches Testmaterial und darf ausschließlich für Regressionstests verwendet werden. Echte Schlüssel, QR-Code-Inhalte und Einmalpasswörter gehören nicht in Build-Logs.
Die Berechnung implementieren
Speichere im Projektverzeichnis auf dem XcodeHost-Cloud-Mac die Datei totp-check.swift. Prüfe zunächst, ob sich xcrun swift --version ausführen lässt, und füge dann den folgenden Code ein. Er benötigt nur das System-SDK; Drittanbieterpakete müssen nicht heruntergeladen werden.
Der Zähler muss als acht Byte in Big-Endian-Reihenfolge kodiert werden. Bei der dynamischen Trunkierung dienen die unteren vier Bits des letzten Digest-Bytes als Offset. Anschließend wird das höchstwertige Bit des ausgelesenen Werts gelöscht und das Ergebnis per Modulo auf die gewünschte Stellenzahl begrenzt. Der Rückgabewert muss eine Zeichenkette sein, sonst gehen führende Nullen verloren.
import Foundation
import CryptoKit
enum TOTPError: Error {
case invalidInput
}
func totp(
secret: Data,
unixSeconds: Int64,
digits: Int = 8,
period: Int64 = 30
) throws -> String {
guard !secret.isEmpty,
unixSeconds >= 0,
period > 0,
digits == 6 || digits == 8 else {
throw TOTPError.invalidInput
}
let counter = UInt64(unixSeconds / period)
let message = Data((0..<8).map { index in
UInt8(truncatingIfNeeded: counter >> ((7 - index) * 8))
})
let digest = Array(HMAC<Insecure.SHA1>.authenticationCode(
for: message,
using: SymmetricKey(data: secret)
))
let offset = Int(digest[digest.count - 1] & 0x0f)
let binary =
(UInt32(digest[offset] & 0x7f) << 24) |
(UInt32(digest[offset + 1]) << 16) |
(UInt32(digest[offset + 2]) << 8) |
UInt32(digest[offset + 3])
let modulus: UInt32 = digits == 6 ? 1_000_000 : 100_000_000
let text = String(binary % modulus)
return String(repeating: "0", count: digits - text.count) + text
}
Leite den Zähler weder aus einer lokalen Datumszeichenkette noch aus einem Zeitzonenoffset oder dem Countdown der Oberfläche ab. Das Protokoll benötigt Unix-Sekunden; der Countdown dient nur der Anzeige. Eine andere Anzeigezeitzone darf den Code für denselben Zeitpunkt nicht verändern.
Die Referenzvektoren ausführen
Ergänze am Ende derselben Datei die folgenden sechs Standard-Testvektoren. Der zwanzig Byte lange ASCII-Schlüssel besteht hier aus den ausdrücklich im Standard angegebenen Rohbytes und wird nicht Base32-dekodiert. Die großen Zeitstempel decken außerdem Implementierungen auf, die unbemerkt zu schmale Ganzzahltypen verwenden.
let secret = Data("12345678901234567890".utf8)
let vectors: [(Int64, String)] = [
(59, "94287082"),
(1111111109, "07081804"),
(1111111111, "14050471"),
(1234567890, "89005924"),
(2000000000, "69279037"),
(20000000000, "65353130")
]
for (time, expected) in vectors {
let actual = try totp(secret: secret, unixSeconds: time)
precondition(actual == expected, "TOTP vector mismatch")
}
Führe das Skript mit xcrun swift totp-check.swift aus. Bei Erfolg gibt es keine Ausgabe; schlägt eine Prüfung fehl, endet der Prozess mit einem Status ungleich null. Verwende keine Optimierungsoptionen, die diese Prüfungen deaktivieren. Nach der Integration in das Test-Target der App ersetzt du die Prüfungen durch XCTAssertEqual, damit jeder fehlgeschlagene Testfall einzeln gemeldet wird.
Schlagen alle sechs Vektoren fehl, prüfe zuerst den Schlüssel und die Byte-Reihenfolge. Scheitern nur die Fälle mit führenden Nullen, liegt die erste Prüfung bei der Umwandlung von Zahlen in Zeichenketten. Scheitern nur große Zeitstempel, prüfe die Bitbreite des Ganzzahltyps, statt die Authentifizierungstoleranz zu vergrößern.
Die Zeitschrittgrenzen prüfen
Wenn die Standardvektoren erfolgreich durchlaufen, müssen auch die Ganzzahldivision und die Ablehnung ungültiger Parameter geprüft werden. Die folgenden Tests zeigen, dass neunundfünfzig Sekunden noch im zweiten Zeitschritt liegen, während bei sechzig Sekunden bereits der nächste beginnt. Dafür sind weder Wartepausen noch Änderungen an der Systemuhr nötig.
let at30 = try totp(secret: secret, unixSeconds: 30)
let at59 = try totp(secret: secret, unixSeconds: 59)
let at60 = try totp(secret: secret, unixSeconds: 60)
precondition(at30 == "94287082")
precondition(at59 == at30)
precondition(at60 == "37359152")
do {
_ = try totp(secret: secret, unixSeconds: -1)
preconditionFailure("Negative time accepted")
} catch TOTPError.invalidInput {
}
Ergänze ebenso Tests, die die Ablehnung eines leeren Schlüssels, einer Zeitschrittlänge von null und nicht unterstützter Stellenzahlen prüfen. Codes aus unterschiedlichen Zeitschritten können zufällig identisch sein. „In verschiedenen Zeitfenstern müssen die Codes immer unterschiedlich sein“ eignet sich daher nicht als allgemeine Zusicherung. Vergleiche stattdessen wie oben mit bekannten Ergebnissen.
Empfängt die App Zeitstempel in Millisekunden, rechne sie in der Adapterschicht ausdrücklich genau einmal um. Sowohl zweimaliges Teilen durch tausend als auch eine vollständig ausgelassene Umrechnung erzeugen formal korrekte Codes, mit denen die Authentifizierung dennoch fehlschlägt.
In die Regressionstests integrieren
Lege die Generatorfunktion in einem Modul ab, auf das das Test-Target zugreifen kann. Die UI-Schicht ermittelt die aktuelle Zeit und übergibt sie; die Funktion selbst darf die Uhr nicht direkt auslesen. Kehrt die App aus dem Hintergrund zurück, muss sie die Zeit erneut einlesen. Der Countdown darf nicht einfach weiter heruntergezählt werden, als wären keine Timer-Callbacks ausgefallen.
Im Simulator lassen sich Stellenzahl, führende Nullen und die Anzeige beim Wechsel des Zeitfensters prüfen. Serverseitige Abnahmetests müssen zusätzlich falsche Codes, Codes außerhalb der zulässigen Toleranz und wiederholte Einreichungen abdecken. Benachbarte Zeitschritte zuzulassen, dient nur dem Ausgleich von Uhrzeitabweichungen. Es bedeutet nicht, dass ein Code unbegrenzt wiederverwendet werden darf.
Halte vor dem Produktivstart drei Arten von Nachweisen fest: die Ergebnisse der Standardvektoren, der Tests zur Ablehnung ungültiger Parameter und der serverseitigen Tests zur Replay-Abwehr. Protokolliere nur die Namen der Testfälle und ihren Erfolgsstatus, keine echten Authentifizierungsdaten. So lassen sich dieselben Prüfungen auch nach dem Wechsel auf eine andere Cloud-Mac-Workstation oder einem Toolchain-Upgrade offline wiederholen. Aus sporadischen Authentifizierungsfehlern werden damit klar eingrenzbare Abweichungen vom vereinbarten Vertrag.
Häufig gestellte Fragen
Benötigen TOTP-Regressionstests Netzwerkzugriff oder echte Wartezeiten?
Nein. Unix-Sekunden werden explizit an die Funktion übergeben. Mit festem Schlüssel und Referenzvektoren lassen sich auch Zeitgrenzen offline prüfen.
Ist die Anmeldung sicher, wenn die Codeberechnung stimmt?
Nein. Der Server muss Versuche begrenzen, eine zulässige Zeitabweichung festlegen und akzeptierte Zeitschritte je Zugangsdaten speichern, um Wiederholungen zu verhindern.
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