Einführung
Zwei-Faktor-Authentifizierung ist heute überall. Dank ihr reicht es nicht aus, nur ein Passwort zu stehlen, um ein Konto zu stehlen. Und obwohl seine Anwesenheit nicht garantiert, dass Ihr Konto nicht entfernt wird, um es zu umgehen, ist ein komplexerer und mehrstufiger Angriff erforderlich. Wie Sie wissen, ist es umso wahrscheinlicher, dass etwas nicht funktioniert, je komplizierter etwas auf dieser Welt ist.
Ich bin sicher, dass jeder, der diesen Artikel liest, mindestens einmal in seinem Leben eine Zwei-Faktor-Authentifizierung (im Folgenden: 2FA, eine lange, schmerzhafte Phrase) verwendet hat. Heute lade ich Sie ein, herauszufinden, wie diese Technologie funktioniert, die täglich unzählige Konten schützt.
Aber für den Anfang können Sie sich die Demo ansehen, was wir heute machen werden.
Die Grundlagen
Das erste, was bei Einmalkennwörtern erwähnenswert ist, ist, dass es sich um zwei Typen handelt: HOTP und TOTP . HMAC-basiertes Einmalkennwort und zeitbasiertes OTP . TOTP ist nur ein Add-On zu HOTP. Lassen Sie uns zunächst über einen einfacheren Algorithmus sprechen.
HOTP wird durch die RFC4226- Spezifikation beschrieben. Es ist klein, nur 35 Seiten und enthält alles, was Sie brauchen: eine formale Beschreibung, eine Beispielimplementierung und Testdaten. Schauen wir uns die Grundkonzepte an.
Was ist HMAC ? HMAC steht auf Russisch für Hash-basierten Nachrichtenauthentifizierungscode oder "Nachrichtenauthentifizierungscode mit Hash-Funktionen". MAC ist ein Mechanismus zum Überprüfen des Absenders einer Nachricht. Der MAC- Algorithmus generiert ein MAC-Tag unter Verwendung eines geheimen Schlüssels, der nur dem Sender und dem Empfänger bekannt ist. Nach Erhalt der Nachricht können Sie das MAC-Tag selbst generieren und die beiden Tags vergleichen. Wenn sie zusammenfallen - alles ist in Ordnung, gab es keine Störung im Kommunikationsprozess. Als Bonus können Sie auf die gleiche Weise überprüfen, ob die Nachricht während der Übertragung beschädigt wurde. Natürlich wird es nicht funktionieren, Interferenzen von Schäden zu unterscheiden, aber die Tatsache der Informationskorruption reicht aus.

Was ist ein Hash? Ein Hash ist das Ergebnis der Anwendung einer Hash-Funktion auf eine Nachricht. Hash-Funktionen nehmen Ihre Daten und machen sie zu einer Zeichenfolge fester Länge. Ein gutes Beispiel ist die bekannte MD5- Funktion, die häufig zur Überprüfung der Dateiintegrität verwendet wird.
MAC selbst ist kein spezifischer Algorithmus, sondern nur ein allgemeiner Begriff. HMAC wiederum ist bereits eine konkrete Implementierung. Insbesondere HMAC- X , wobei X eine der kryptografischen Hash-Funktionen ist. HMAC akzeptiert zwei Argumente: einen geheimen Schlüssel und eine Nachricht, mischt sie auf eine bestimmte Weise, wendet die ausgewählte Hash-Funktion zweimal an und gibt ein MAC-Tag zurück.
Wenn Sie sich gerade überlegen, was dies alles mit Einmalkennwörtern zu tun hat - keine Sorge, wir haben fast den Hauptpunkt erreicht.
Gemäß der Spezifikation wird HOTP basierend auf zwei Werten berechnet:
- K ist der geheime Schlüssel , den Client und Server kennen. Es sollte mindestens 128 Bit lang und vorzugsweise 160 Bit lang sein und bei der Konfiguration von 2FA erstellt werden.
- C ist der Zähler .
Ein Zähler ist ein 8-Byte-Wert, der zwischen Client und Server synchronisiert wird. Es wird aktualisiert, wenn Sie neue Passwörter generieren. Im HOTP-Schema wird jedes Mal, wenn Sie ein neues Kennwort generieren, ein clientseitiger Zähler erhöht. Auf der Serverseite jedes Mal, wenn das Kennwort die Validierung erfolgreich besteht. Da es möglich ist, ein Kennwort zu generieren, es aber nicht zu verwenden, lässt der Server den Zählerwert im Fenster etwas weiter laufen. Wenn Sie jedoch im HOTP-Schema zu viel mit dem Passwortgenerator spielen, müssen Sie ihn erneut synchronisieren.
Also. Wie Sie wahrscheinlich bemerkt haben, nimmt HMAC auch zwei Argumente an. RFC4226 definiert die HOTP-Generierungsfunktion wie folgt:
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
Erwartungsgemäß wird K als geheimer Schlüssel verwendet. Der Zähler wird wiederum als Nachricht verwendet. Nachdem die HMAC-Funktion ein MAC-Tag generiert hat, zieht die mysteriöse Funktion " Truncate
das uns bereits bekannte Einmalkennwort heraus, das Sie in Ihrer Generatoranwendung oder auf dem Token sehen.
Beginnen wir mit dem Schreiben des Codes und kümmern uns um den Rest.
Umsetzungsplan
Um Einmalkennwörter zu erhalten, müssen wir diese Schritte ausführen.

- Generieren Sie einen HMAC-SHA1-Hash aus den Parametern K und C. Dies ist eine 20-Byte-Zeichenfolge.
- Ziehen Sie 4 Bytes auf eine bestimmte Weise aus dieser Zeichenfolge.
- Konvertieren Sie den herausgezogenen Wert in eine Zahl und teilen Sie ihn durch 10 ^ n, wobei n = die Anzahl der Stellen im Einmalkennwort ist (normalerweise n = 6). Und schließlich nehmen Sie den Rest dieser Abteilung. Dies wird unser Passwort sein.
Das klingt nicht zu schwer, oder? Beginnen wir mit der Hash-Generation.
Generieren Sie HMAC-SHA1
Dies ist möglicherweise der einfachste der oben aufgeführten Schritte. Wir werden nicht versuchen, den Algorithmus selbst neu zu erstellen (wir müssen niemals versuchen, etwas aus der Kryptographie selbst zu implementieren). Stattdessen verwenden wir die Web Crypto API . Das kleine Problem ist, dass diese Spezifikations-API nur unter Secure Context (HTTPS) verfügbar ist. Für uns ist dies mit der Tatsache behaftet, dass wir es nicht verwenden können, ohne HTTPS auf dem Entwicklungsserver einzurichten. Ein bisschen Geschichte und Diskussion darüber, wie dies die richtige Entscheidung ist, finden Sie hier .
Glücklicherweise können Sie in Firefox Web Crypto in einem unsicheren Kontext verwenden und müssen das Rad nicht neu erfinden oder in Bibliotheken von Drittanbietern ziehen. Für die Entwicklung einer Demo empfehle ich daher die Verwendung von FF.
Die Crypto-API selbst ist in window.crypto.subtle
definiert. Wenn Sie vom Namen überrascht sind, zitiere ich aus der Spezifikation:
Die API wird als SubtleCrypto
, da viele der Algorithmen spezifische Verwendungsanforderungen haben. Nur wenn diese Anforderungen erfüllt sind, behalten sie ihre Haltbarkeit.
Lassen Sie uns die Methoden durchgehen, die wir brauchen. Hinweis: Alle hier genannten Methoden sind asynchron und geben Promise
.
Erstens benötigen wir die importKey
Methode, da wir unseren privaten Schlüssel verwenden und ihn nicht im Browser generieren. importKey
5 Argumente:
importKey( format, keyData, algorithm, extractable, usages );
In unserem Fall:
format
wird 'raw'
, d.h. Wir werden den Schlüssel als ArrayBuffer
Byte-Array ArrayBuffer
.keyData
ist der gleiche ArrayBuffer. Sehr bald werden wir darüber sprechen, wie man es generiert.algorithm
wird gemäß der Spezifikation HMAC-SHA1
. Dieses Argument muss mit dem Format von HmacImportParams übereinstimmen .extractable
auf false, da wir nicht vorhaben, den geheimen Schlüssel zu exportieren- Und schließlich brauchen wir ausgerechnet nur das
'sign'
.
Unser geheimer Schlüssel wird eine lange zufällige Zeichenfolge sein. In der realen Welt kann dies eine Folge von Bytes sein, die möglicherweise nicht druckbar sind. Der Einfachheit halber werden wir in diesem Artikel jedoch eine Zeichenfolge betrachten. Um es in ArrayBuffer
zu konvertieren, ArrayBuffer
wir die TextEncoder
Schnittstelle. Damit wird der Schlüssel in zwei Codezeilen vorbereitet:
const encoder = new TextEncoder('utf-8'); const secretBytes = encoder.encode(secret);
Nun lassen Sie uns alles zusammenfügen:
const Crypto = window.crypto.subtle; const encoder = new TextEncoder('utf-8'); const secretBytes = encoder.encode(secret); const key = await Crypto.importKey( 'raw', secretBytes, { name: 'HMAC', hash: { name: 'SHA-1' } }, false, ['sign'] );
Großartig! Kryptographie wurde vorbereitet. Jetzt werden wir uns um den Zähler kümmern und schließlich die Nachricht unterschreiben.
Gemäß der Spezifikation sollte unser Zähler 8 Byte lang sein. Wir werden wieder damit arbeiten, wie mit ArrayBuffer
. Um es in diese Form zu übersetzen, verwenden wir den Trick, der normalerweise in JS verwendet wird, um Nullen in den oberen Ziffern einer Zahl zu speichern. Danach werden wir jedes Byte mithilfe einer DataView
in einen ArrayBuffer
. Beachten Sie, dass das Format laut Spezifikation für alle Binärdaten Big Endian ist .
function padCounter(counter) { const buffer = new ArrayBuffer(8); const bView = new DataView(buffer); const byteString = '0'.repeat(64);

Nachdem Sie den Schlüssel und den Zähler vorbereitet haben, können Sie einen Hash generieren! Dazu verwenden wir die sign
von SubtleCrypto
.
const counterArray = padCounter(counter); const HS = await Crypto.sign('HMAC', key, counterArray);
Damit haben wir den ersten Schritt abgeschlossen. Am Ausgang erhalten wir einen etwas mysteriösen Wert namens HS. Und obwohl dies nicht der beste Name für eine Variable ist, wird sie (und einige der folgenden) in der Spezifikation so genannt. Wir lassen diese Namen, um den Vergleich des Codes zu vereinfachen. Was weiter?
Schritt 2: Generieren Sie eine 4-Byte-Zeichenfolge (Dynamic Truncation).
Sei Sbits = DT (HS) // DT, unten definiert,
// gibt eine 31-Bit-Zeichenfolge zurück
DT steht für Dynamic Truncation. Und so funktioniert es:
function DT(HS) {

Beachten Sie, wie wir bitweise UND auf das erste Byte von HS angewendet haben. 0x7f
im Binärsystem ist 0b01111111
, also verwerfen wir im Wesentlichen nur das erste Bit. In JS endet hier die Bedeutung dieses Ausdrucks, in anderen Sprachen wird jedoch auch das Vorzeichenbit abgeschnitten, um Verwechslungen zwischen positiven und negativen Zahlen zu beseitigen und diese Zahl als vorzeichenlos darzustellen.
Fast fertig! Es bleibt nur, den von DT erhaltenen Wert in eine Zahl umzuwandeln und zum dritten Schritt weiterzuleiten.
function truncate(uKey) { const Sbits = DT(uKey); const Snum = parseInt(Sbits, 2); return Snum; }
Der dritte Schritt ist auch ziemlich klein. Alles, was getan werden muss, ist, die resultierende Zahl durch 10 ** ( )
zu teilen und dann den Rest dieser Teilung zu übernehmen. Daher schneiden wir die letzten N Ziffern von dieser Zahl ab. Gemäß der Spezifikation sollte unser Code in der Lage sein, mindestens sechsstellige Passwörter und möglicherweise sieben- und achtstellige Passwörter abzurufen. Theoretisch hätten wir, da dies eine 31-Bit-Zahl ist, 9 Zeichen herausziehen können, aber in Wirklichkeit habe ich persönlich nie mehr als 6 Zeichen gesehen. Und Sie?
Der Code für die endgültige Funktion, der alle vorherigen kombiniert, sieht in diesem Fall ungefähr so aus:
async function generateHOTP(secret, counter) { const key = await generateKey(secret, counter); const uKey = new Uint8Array(key); const Snum = truncate(uKey);
Hurra! Aber wie kann man jetzt überprüfen, ob unser Code korrekt ist?
Testen
Um die Implementierung zu testen, verwenden wir Beispiele aus dem RFC. Anhang D enthält Testwerte für den geheimen Schlüssel "12345678901234567890"
und Zählerwerte von 0 bis 9. Es werden auch HMAC-Hashes und Zwischenergebnisse der Funktion "Abschneiden" gezählt. Ziemlich nützlich zum Debuggen aller Schritte des Algorithmus. Hier ist ein kleines Beispiel für diese Tabelle (nur der Zähler und HOTP sind noch übrig):
Count HOTP 0 755224 1 287082 2 359152 3 969429 ...
Wenn Sie die Demo noch nicht gesehen haben, ist jetzt die richtige Zeit dafür. Sie können darin die Werte aus dem RFC eintreiben. Und komm zurück, weil wir TOTP starten.
Totp
So kamen wir endlich zum moderneren Teil von 2FA. Wenn Sie Ihren Einmalkennwortgenerator öffnen und einen kleinen Timer sehen, der zählt, wie viel mehr Code gültig ist, handelt es sich um TOTP. Was ist der Unterschied?
Zeitbasiert bedeutet, dass anstelle eines statischen Werts die aktuelle Zeit als Zähler verwendet wird. Oder genauer gesagt das "Intervall" (Zeitschritt). Oder sogar die Nummer des aktuellen Intervalls. Um dies zu berechnen, nehmen wir die Unix-Zeit (die Anzahl der Millisekunden seit Mitternacht am 1. Januar 1970 UTC) und teilen die Gültigkeit des Passworts durch das Fenster (normalerweise 30 Sekunden). Der Server toleriert normalerweise kleine Abweichungen aufgrund einer unvollständigen Taktsynchronisation. Normalerweise 1 Intervall hin und her je nach Konfiguration.
Dies ist natürlich viel sicherer als das HOTP-Schema. In einem zeitgebundenen Schema ändert sich der gültige Code alle 30 Sekunden, auch wenn er nicht verwendet wurde. Im ursprünglichen Algorithmus wird ein gültiges Kennwort durch den aktuellen Zählerwert im Fenster Server + Toleranz bestimmt. Wenn Sie sich nicht authentifizieren, wird das Passwort nicht auf unbestimmte Zeit geändert. Weitere Informationen zu TOTP finden Sie in RFC6238 .
Da das zeitbasierte Schema eine Ergänzung zum ursprünglichen Algorithmus darstellt, müssen wir keine Änderungen an der ursprünglichen Implementierung vornehmen. Wir werden requestAnimationFrame
und für jeden Frame prüfen, ob wir uns noch innerhalb des Zeitintervalls befinden. Wenn nicht, generieren Sie einen neuen Zähler und berechnen Sie den HOTP erneut. Wenn Sie den gesamten Steuercode weglassen, sieht die Lösung ungefähr so aus:
let stepWindow = 30 * 1000;
Finishing Touches - QR-Code-Unterstützung
Wenn wir 2FA konfigurieren, scannen wir normalerweise die Anfangsparameter mit einem QR-Code. Es enthält alle erforderlichen Informationen: das ausgewählte Schema, den geheimen Schlüssel, den Kontonamen, den Anbieternamen und die Anzahl der Ziffern im Kennwort.
In einem früheren Artikel habe ich darüber gesprochen, wie Sie QR-Codes mithilfe der getDisplayMedia
API direkt vom Bildschirm aus getDisplayMedia
. Basierend auf diesem Material habe ich eine kleine Bibliothek erstellt, die wir jetzt verwenden werden. Die Bibliothek heißt Stream-Display und zusätzlich verwenden wir das wunderbare jsQR- Paket.
Der QR-codierte Link hat das folgende Format:
otpauth://TYPE/LABEL?PARAMETERS
Zum Beispiel:
otpauth://totp/label?secret=oyu55d4q5kllrwhy4euqh3ouw7hebnhm5qsflfcqggczoafxu75lsagt&algorithm=SHA1&digits=6&period=30
Ich werde den Code weglassen, der den Prozess zum Starten der Bildschirmaufnahme und -erkennung einrichtet, da dies alles in der Dokumentation zu finden ist. Stattdessen können Sie diesen Link folgendermaßen analysieren:
const setupFromQR = data => { const url = new URL(data);
In der realen Welt ist der geheime Schlüssel eine Base- 32 (!) -Codierte Zeichenfolge, da einige Bytes möglicherweise nicht druckbar sind. Der Einfachheit halber lassen wir diesen Punkt jedoch weg. Leider konnte ich keine Informationen finden, warum Base-32 oder nur ein solches Format. Anscheinend gibt es keine offizielle Spezifikation für dieses URL-Format, und das Format selbst wurde von Google geprägt. Hier können Sie ein wenig über ihn lesen .
Um Test-QR-Codes zu generieren, empfehle ich die Verwendung von FreeOTP .
Fazit
Und das ist alles! Vergessen Sie nicht, sich die Demo anzuschauen. Es gibt auch einen Link zum Repository mit dem Code, der dahinter steckt.
Heute haben wir eine ziemlich wichtige Technologie zerlegt, die wir täglich verwenden. Ich hoffe du hast etwas Neues für dich gelernt. Dieser Artikel hat viel länger gedauert als ich dachte. Es ist jedoch sehr interessant, eine Papierspezifikation in etwas Funktionierendes und Vertrautes zu verwandeln.
Bis bald!