


OTPs entmystifizieren: die Logik hinter der Offline-Generierung von Token
Dec 12, 2024 pm 10:23 PMHallo! An einem anderen Abend, auf dem Heimweg, beschloss ich, den Briefkasten zu überprüfen. Ich meine nicht mein E-Mail-Postfach, sondern den altmodischen Briefkasten, in den der Postbote die physischen Briefe legt. Und zu meiner gro?en überraschung fand ich dort einen Umschlag mit etwas darin! W?hrend ich es ?ffnete, hoffte ich einige Augenblicke, dass es sich um den um Jahrzehnte versp?teten Brief aus Hogwarts handelte. Doch dann musste ich wieder auf den Boden der Tatsachen zurück, als mir auffiel, dass es sich um einen langweiligen ?Erwachsenen“-Brief von der Bank handelte. Ich überflog den Text und stellte fest, dass meine ?nur digitale“ Bank für coole Kinder vom gr??ten Player auf dem lokalen Markt übernommen worden war. Und als Zeichen des Neuanfangs fügten sie dem Umschlag Folgendes bei:
Neben der Gebrauchsanweisung.
Wenn Sie wie ich sind und noch nie auf eine solche technische Innovation gesto?en sind, lassen Sie mich Ihnen mitteilen, was ich aus dem Brief gelernt habe: Neue Eigentümer haben beschlossen, die Sicherheitsrichtlinien ihres Unternehmens durchzusetzen, was bedeutet, dass alle Benutzer davon betroffen sind Konten werden von nun an die MFA aktiviert haben (übrigens ein gro?es Lob dafür). Und das Ger?t, das Sie oben sehen k?nnen, generiert einmalige, 6-stellige Token, die als zweiter Faktor beim Anmelden bei Ihrem Bankkonto verwendet werden. Im Prinzip so, wie Apps wie Authy, Google Authenticator oder 2FAS funktionieren, allerdings in physischer Form.
Also habe ich es ausprobiert und der Anmeldevorgang verlief reibungslos: Das Ger?t zeigte mir einen 6-stelligen Code an, ich gab ihn in meine Banking-App ein und schon war ich dabei. Hurra! Aber dann fiel mir etwas auf: Wie funktioniert das Ding? Es besteht keine M?glichkeit, dass es irgendwie mit dem Internet verbunden ist, aber es schafft es, korrekte Codes zu generieren, die von meinem Bankserver akzeptiert werden. Hm... K?nnte es eine SIM-Karte oder etwas ?hnliches darin haben? Auf keinen Fall!
Als mir klar wurde, dass mein Leben nie mehr das gleiche sein wird, begann ich mich über die Apps zu wundern, die ich oben erw?hnt habe (Authy und Freunde)? Mein innerer Forscher wurde geweckt, also schaltete ich mein Telefon in den Flugmodus und stellte zu meiner gro?en überraschung fest, dass sie offline einwandfrei funktionieren: Sie generieren weiterhin Codes, die von den Servern der Apps akzeptiert werden. Interessant!
Bei Ihnen bin ich mir nicht sicher, aber ich habe den einmaligen Token-Flow immer als selbstverst?ndlich angesehen und nie wirklich darüber nachgedacht (insbesondere aufgrund der Tatsache, dass es heutzutage selten vorkommt, dass mein Telefon kein Internet hat, es sei denn Ich mache einige Outdoor-Abenteuer. Das war also der Grund für meine überraschung. Ansonsten ist es aus Sicherheitsgründen absolut sinnvoll, auf diese Weise zu arbeiten, da der Generierungsprozess rein lokal erfolgt und daher vor externen Akteuren geschützt ist. Aber wie funktioniert es?
Nun, moderne Technologien wie Google oder ChatGPT machen es einfach, die Antwort leicht zu finden. Aber dieses technische Problem schien mir Spa? zu machen, also beschloss ich, es auszuprobieren und es zun?chst selbst zu l?sen.
Anforderungen
Beginnen wir mit dem, was wir haben:
- ein Offline-Ger?t, das 6-stellige Codes generiert
- ein Server, der diese Codes akzeptiert, validiert und ein grünes Signal gibt, wenn sie korrekt sind
Der Servervalidierungsteil weist darauf hin, dass der Server in der Lage sein muss, denselben Code wie das Offline-Ger?t zu generieren, um sie zu vergleichen. Hm...das kann hilfreich sein.
Meine weiteren Beobachtungen meines neuen ?Spielzeugs“ brachten noch mehr Entdeckungen:
- Wenn ich es aus- und wieder ausschalte, sehe ich normalerweise den gleichen Code wie zuvor
- ab und zu ?ndert es sich
Die einzige logische Erkl?rung, die mir einf?llt, ist, dass diese Codes eine bestimmte Lebensdauer haben. Ich würde gerne eine Geschichte darüber erz?hlen, wie ich versucht habe, die Dauer nach ?1-2-3-...-N“ zu z?hlen, aber das stimmt nicht: Ich habe einen gro?en Hinweis von den Apps bekommen, z Authy und Co, wo ich die 30-Sekunden-TTL gesehen habe. Guter Fund, fügen wir das zur Liste der bekannten Fakten hinzu.
Lassen Sie uns die Anforderungen zusammenfassen, die wir bisher haben:
- Vorhersehbare (statt zuf?llige) Codegenerierung im 6-stelligen Format
- Die Generierungslogik sollte reproduzierbar sein, damit unabh?ngig von der Plattform das gleiche Ergebnis erzielt werden kann
- Die Codelebensdauer betr?gt 30 Sekunden, was bedeutet, dass der Generierungsalgorithmus innerhalb dieses Zeitraums denselben Wert erzeugt
Die gro?e Frage
In Ordnung, aber die Hauptfrage bleibt unbeantwortet: Wie kommt es, dass die Offline-App den Wert generieren kann, der mit dem Wert der anderen App übereinstimmt? Was haben sie gemeinsam?
Wenn Sie sich für das Herr der Ringe-Universum interessieren, erinnern Sie sich vielleicht daran, wie Bilbo mit Gollum ein R?tselspiel gespielt hat und dieses R?tsel l?sen musste:
Dieses Ding verschlingt alles:
V?gel, Tiere, B?ume, Blumen;
Nagt Eisen, bei?t Stahl;
Mahlt harte Steine ??zu Mehl;
T?tet den K?nig, ruiniert die Stadt,
Und schl?gt hohen Berg hinunter.
Spoiler-Alarm, aber Herr Beutlin hatte Glück und kam aus Versehen auf die richtige Antwort: ?Zeit!“. Ob Sie es glauben oder nicht, aber genau das ist auch die Antwort auf unser R?tsel: Zwei beliebige (oder mehr) Apps haben Zugriff auf die gleiche Zeit, solange sie über eine eingebettete Uhr verfügen. Letzteres ist heutzutage kein Problem mehr und das betreffende Ger?t ist gro? genug, um darauf Platz zu finden. Schauen Sie sich um, und die Chancen stehen gut, dass die Uhrzeit auf Ihrer Armbanduhr, Ihrem Mobiltelefon, Ihrem Fernseher, Ihrem Ofen und der Uhr an der Wand gleich ist. Wir sind hier an etwas interessiert, es scheint, als h?tten wir die Basis für die OTP-Berechnung (Einmalpasswort) gefunden!
Herausforderungen
Sich auf die Zeit zu verlassen, bringt seine eigenen Herausforderungen mit sich:
- Zeitzonen – welche soll ich verwenden?
- Uhren neigen dazu, nicht mehr synchron zu sein, was in verteilten Systemen eine gro?e Herausforderung darstellt
Lassen Sie uns sie einzeln ansprechen:
- Zeitzone: Die einfachste L?sung besteht hier darin, sicherzustellen, dass alle Ger?te auf dieselbe Zone angewiesen sind, und UTC kann ein guter standortunabh?ngiger Kandidat sein
- Was Uhren betrifft, die nicht mehr synchron sind: Eigentlich müssen wir das Problem vielleicht nicht einmal l?sen, sondern akzeptieren es als etwas Unvermeidliches, solange die Abweichung innerhalb von ein oder zwei Sekunden liegt, was angesichts der 30-Sekunden-TTL ertr?glich sein k?nnte. Der Hardwarehersteller des Ger?ts sollte in der Lage sein, vorherzusagen, wann eine solche Abweichung erreicht wird, sodass das Ger?t es dann als Ablaufdatum verwendet und die Bank sie einfach durch die neuen ersetzt oder eine M?glichkeit hat, sie anzuschlie?en Verbindung zum Netzwerk herstellen, um die Uhr zu kalibrieren. Zumindest ist das hier mein Gedankengang.
Durchführung
Okay, das ist gekl?rt, also versuchen wir, die allererste Version unseres Algorithmus auf der Grundlage der Zeit zu implementieren. Da wir an einem 6-stelligen Ergebnis interessiert sind, scheint es eine kluge Entscheidung zu sein, sich auf einen Zeitstempel statt auf das für Menschen lesbare Datum zu verlassen. Fangen wir dort an:
// gets current timestamp: current := time.Now().Unix() fmt.Println("Current timestamp: ", current)
Laut den Go-Dokumenten gibt .Unix() zurück
die Anzahl der Sekunden, die seit dem 1. Januar 1970 UTC vergangen sind.
Das wurde am Terminal ausgegeben:
Current timestamp: 1733691162
Das ist ein guter Anfang, aber wenn wir diesen Code erneut ausführen, ?ndert sich der Zeitstempelwert, w?hrend wir ihn gerne 30 Sekunden lang stabil halten würden. Nun, ganz einfach, teilen wir es durch 30 und verwenden diesen Wert als Basis:
// gets current timestamp current := time.Now().Unix() fmt.Println("Current timestamp: ", current) // gets a number that is stable within 30 seconds base := current / 30 fmt.Println("Base: ", base)
Lass es uns ausführen:
Current timestamp: 1733691545 Base: 57789718
Und noch einmal:
Current timestamp: 1733691552 Base: 57789718
Der Grundwert bleibt gleich. Warten wir einen Moment und führen Sie es erneut aus:
Current timestamp: 1733691571 Base: 57789719
Der Basiswert hat sich ge?ndert, da das 30-Sekunden-Fenster abgelaufen ist – gute Arbeit!
Wenn die Logik ?durch 30 dividieren“ keinen Sinn ergibt, lassen Sie es mich anhand eines einfachen Beispiels erkl?ren:
- Stellen Sie sich vor, unser Zeitstempel gibt 1 zurück
- Wenn wir 1 durch 30 dividieren, ist das Ergebnis 0, denn wenn wir eine streng typisierte Programmiersprache verwenden, gibt die Division von Ganzzahl durch Ganzzahl eine andere Ganzzahl zurück, die sich nicht um den Gleitkommateil kümmert
- Das bedeutet, dass wir für die n?chsten 30 Sekunden 0 erhalten, w?hrend der Zeitstempel zwischen 0 und 29 liegt
- Sobald der Zeitstempel den Wert 30 erreicht, ist das Ergebnis der Division 1, bis 60 (wo es zu 2 wird) und so weiter
Ich hoffe, dass es jetzt mehr Sinn ergibt.
Allerdings sind noch nicht alle Anforderungen erfüllt, da wir ein 6-stelliges Ergebnis ben?tigen, w?hrend die aktuelle Basis heute 8 Ziffern hat, aber irgendwann in der Zukunft m?glicherweise 9 Ziffern erreicht werden, und so weiter . Nun, nutzen wir einen weiteren einfachen mathematischen Trick: Teilen Sie die Basis durch 1.000.000 und erhalten Sie den Rest, der immer genau 6 Ziffern hat, da die Erinnerung eine beliebige Zahl von 0 bis 999.999 sein kann, jedoch niemals gr??er:
// gets current timestamp: current := time.Now().Unix() fmt.Println("Current timestamp: ", current)
Der Teil fmt.Sprintf(" d", code) h?ngt führende Nullen an, falls unser Codewert weniger als 6 Ziffern hat. Beispielsweise wird 1234 in 001234 umgewandelt.
Den gesamten Code für diesen Beitrag finden Sie hier.
Lassen Sie uns diesen Code ausführen:
Current timestamp: 1733691162
Alles klar, wir bekommen unseren 6-stelligen Code, Hurra! Aber irgendetwas fühlt sich hier nicht richtig an, oder? Wenn ich Ihnen diesen Code gebe und Sie ihn gleichzeitig mit mir ausführen, erhalten Sie denselben Code wie ich. Das macht es aber nicht zu einem sicheren Einmalpasswort, oder? Hier kommt eine neue Anforderung:
- Das Ergebnis sollte für verschiedene Benutzer unterschiedlich sein
Natürlich sind einige Kollisionen unvermeidlich, wenn wir mehr als 1 Million Benutzer haben, da dies die maximal m?glichen eindeutigen Werte pro 6 Ziffern sind. Aber das sind seltene und technisch unvermeidbare Kollisionen, nicht der Algorithmus-Designfehler, wie wir ihn jetzt haben.
Ich glaube nicht, dass uns hier irgendwelche cleveren mathematischen Tricks helfen werden: Wenn wir separate Ergebnisse pro Benutzer ben?tigen, brauchen wir einen benutzerspezifischen Status, um dies zu erm?glichen. Als Ingenieure und gleichzeitig Benutzer vieler Dienste wissen wir, dass Dienste für die Gew?hrung des Zugriffs auf ihre APIs auf private Schlüssel angewiesen sind, die für jeden Benutzer einzigartig sind. Lassen Sie uns auch für unseren Anwendungsfall einen privaten Schlüssel einführen, um zwischen den Benutzern zu unterscheiden.
Privater Schlüssel
Eine einfache Logik zum Generieren privater Schlüssel als Ganzzahlen zwischen 1.000.000 und 999.999.999:
// gets current timestamp current := time.Now().Unix() fmt.Println("Current timestamp: ", current) // gets a number that is stable within 30 seconds base := current / 30 fmt.Println("Base: ", base)
Wir verwenden die pkDb-Zuordnung, um Duplikate zwischen privaten Schlüsseln zu verhindern, und wenn das Duplikat erkannt wurde, führen wir die Generierungslogik noch einmal aus, bis wir ein eindeutiges Ergebnis erhalten.
Lassen Sie uns diesen Code ausführen, um das Beispiel des privaten Schlüssels zu erhalten:
Current timestamp: 1733691545 Base: 57789718
Lassen Sie uns diesen privaten Schlüssel in unserer Codegenerierungslogik verwenden, um sicherzustellen, dass wir für jeden privaten Schlüssel unterschiedliche Ergebnisse erhalten. Da unser privater Schlüssel vom Typ Ganzzahl ist, k?nnen wir ihn am einfachsten zum Basiswert hinzufügen und den verbleibenden Algorithmus unver?ndert lassen:
Current timestamp: 1733691552 Base: 57789718
Stellen wir sicher, dass es für verschiedene private Schlüssel unterschiedliche Ergebnisse liefert:
Current timestamp: 1733691571 Base: 57789719
Das Ergebnis sieht so aus, wie wir es wollten und erwartet haben:
// gets current timestamp: current := time.Now().Unix() fmt.Println("Current timestamp: ", current) // gets a number that is stable within 30 seconds: base := current / 30 fmt.Println("Base: ", base) // makes sure it has only 6 digits: code := base % 1_000_000 // adds leading zeros if necessary: formattedCode := fmt.Sprintf("%06d", code) fmt.Println("Code: ", formattedCode)
Funktioniert wie ein Zauber! Das bedeutet, dass der private Schlüssel in das Ger?t eingeschleust werden sollte, das Codes generiert, bevor er an Benutzer wie mich gesendet wird: Das sollte für die Banken überhaupt kein Problem darstellen.
Sind wir jetzt fertig? Nun ja, nur, wenn wir mit dem künstlichen Szenario, das wir verwendet haben, zufrieden sind. Wenn Sie die MFA jemals für Dienste/Websites aktiviert haben, bei denen Sie ein Konto haben, ist Ihnen m?glicherweise aufgefallen, dass die Webressource Sie auffordert, den QR-Code mit der Zweitfaktor-App Ihrer Wahl (Authy, Google Authenticator, 2FAS usw.) zu scannen. ), das den Geheimcode in Ihre App eingibt und von diesem Moment an mit der Generierung eines 6-stelligen Codes beginnt. Alternativ besteht die M?glichkeit, den Code manuell einzugeben.
Ich erw?hne dies, um zu erw?hnen, dass es m?glich ist, einen Blick auf das Format der echten privaten Schlüssel zu werfen, die in der Branche verwendet werden. Normalerweise handelt es sich um 16–32 Zeichen lange Base32-codierte Zeichenfolgen, die wie folgt aussehen:
// gets current timestamp: current := time.Now().Unix() fmt.Println("Current timestamp: ", current)
Wie Sie sehen, unterscheidet sich dies erheblich von den ganzzahligen privaten Schlüsseln, die wir verwendet haben, und die aktuelle Implementierung unseres Algorithmus wird nicht funktionieren, wenn wir zu diesem Format wechseln. Wie k?nnen wir unsere Logik anpassen?
Privater Schlüssel als String
Beginnen wir mit einem einfachen Ansatz: Unser Code l?sst sich aufgrund dieser Zeile nicht kompilieren:
Current timestamp: 1733691162
as pk ist von nun an vom Typ string. Warum wandeln wir es also nicht in eine Ganzzahl um? Es gibt zwar weitaus elegantere und leistungsf?higere M?glichkeiten, dies zu tun, aber hier ist die einfachste Sache, die ich mir ausgedacht habe:
// gets current timestamp current := time.Now().Unix() fmt.Println("Current timestamp: ", current) // gets a number that is stable within 30 seconds base := current / 30 fmt.Println("Base: ", base)
Dies ist stark von der Java-hashCode()-Implementierung für den String-Datentyp inspiriert, was es für unser Szenario gut genug macht.
Hier ist die angepasste Logik:
Current timestamp: 1733691545 Base: 57789718
Hier ist die Terminalausgabe:
Current timestamp: 1733691552 Base: 57789718
Sch?ner 6-stelliger Code, gute Arbeit. Warten wir, bis wir zum n?chsten Zeitfenster gelangen, und führen Sie es erneut aus:
Current timestamp: 1733691571 Base: 57789719
Hm...es funktioniert, aber der Code ist im Grunde die Erh?hung des vorherigen Werts, was nicht gut ist, da OTPs auf diese Weise vorhersehbar sind und es sehr wichtig ist, seinen Wert zu haben und zu wissen, zu welcher Zeit er geh?rt Es ist einfach, mit der Generierung derselben Werte zu beginnen, ohne den privaten Schlüssel kennen zu müssen. Hier ist der einfache Pseudocode für diesen Hack:
// gets current timestamp: current := time.Now().Unix() fmt.Println("Current timestamp: ", current) // gets a number that is stable within 30 seconds: base := current / 30 fmt.Println("Base: ", base) // makes sure it has only 6 digits: code := base % 1_000_000 // adds leading zeros if necessary: formattedCode := fmt.Sprintf("%06d", code) fmt.Println("Code: ", formattedCode)
wobei keepWithinSixDigits dafür sorgt, dass nach 999 999 der n?chste Wert 000 000 ist usw., um den Wert innerhalb der 6-stelligen Grenzm?glichkeiten zu halten.
Wie Sie sehen, handelt es sich um eine schwerwiegende Sicherheitslücke. Warum passiert es? Wenn wir uns die Basisberechnungslogik ansehen, werden wir feststellen, dass sie auf zwei Faktoren beruht:
- aktueller Zeitstempel geteilt durch 30
- Hash des privaten Schlüssels
Der Hash erzeugt denselben Wert für denselben Schlüssel, daher ist sein Wert konstant. Der aktuelle / 30 hat 30 Sekunden lang den gleichen Wert, aber sobald das Fenster verstrichen ist, ist der n?chste Wert die Erh?hung des vorherigen. Dann verh?lt sich Basis % 1_000_000 so, wie wir es sehen. Unsere vorherige Implementierung (mit privaten Schlüsseln als Ganzzahlen) hatte die gleiche Schwachstelle, aber wir haben das nicht bemerkt – schuld daran waren mangelnde Tests.
Wir müssen den Strom / 30 in etwas umwandeln, um die ?nderung seines Wertes deutlicher zu machen.
Verteilte OTP-Werte
Es gibt mehrere M?glichkeiten, dies zu erreichen, und es gibt einige coole Mathe-Tricks, aber zu Bildungszwecken legen wir Wert auf die Lesbarkeit der L?sung, die wir verwenden werden: Extrahieren wir current / 30 in eine separate Variablenbasis und schlie?en wir sie ein es in die Hash-Berechnungslogik:
// gets current timestamp: current := time.Now().Unix() fmt.Println("Current timestamp: ", current)
Auf diese Weise erh?ht sich das Gewicht des Diffs aufgrund der Reihe durchgeführter Multiplikationen, obwohl sich die Basis alle 30 Sekunden um 1 ?ndert, nachdem sie in der Hash()-Funktionslogik verwendet wurde.
Hier ist das aktualisierte Codebeispiel:
Current timestamp: 1733691162
Lass es uns ausführen:
// gets current timestamp current := time.Now().Unix() fmt.Println("Current timestamp: ", current) // gets a number that is stable within 30 seconds base := current / 30 fmt.Println("Base: ", base)
Boom! Wie kommt es, dass wir hier den Minuswert erhalten haben? Nun, es scheint, als w?ren uns die int64-Bereiche ausgegangen, also haben wir die Werte auf das Minus begrenzt und von vorne begonnen – meine Java-Kollegen kennen das aus dem Verhalten von hashCode(). Die L?sung ist einfach: Nehmen wir den absoluten Wert aus dem Ergebnis, dann wird das Minuszeichen ignoriert:
Current timestamp: 1733691545 Base: 57789718
Hier ist das gesamte Codebeispiel mit dem Fix:
Current timestamp: 1733691552 Base: 57789718
Lass es uns ausführen:
Current timestamp: 1733691571 Base: 57789719
Lass es uns noch einmal ausführen, um sicherzustellen, dass die OTP-Werte jetzt verteilt werden:
// gets current timestamp: current := time.Now().Unix() fmt.Println("Current timestamp: ", current) // gets a number that is stable within 30 seconds: base := current / 30 fmt.Println("Base: ", base) // makes sure it has only 6 digits: code := base % 1_000_000 // adds leading zeros if necessary: formattedCode := fmt.Sprintf("%06d", code) fmt.Println("Code: ", formattedCode)
Sch?n, endlich eine vernünftige L?sung!
Eigentlich war das der Moment, in dem ich meinen manuellen Implementierungsprozess beendet habe, da ich viel Spa? hatte und etwas Neues gelernt habe. Es ist jedoch weder die beste L?sung noch die, mit der ich leben würde. Es hat unter anderem einen gro?en Fehler: Wie Sie sehen, verarbeitet unsere Logik aufgrund der Hashing-Logik und der Zeitstempelwerte immer gro?e Zahlen, was bedeutet, dass es h?chst unwahrscheinlich ist, dass wir Ergebnisse generieren k?nnen, die mit 1 oder beginnen mehr Nullen: z. B. 012345, 001234 usw., obwohl sie vollst?ndig gültig sind. Aus diesem Grund fehlen uns 100.000 m?gliche Werte, was 10 % der m?glichen Anzahl von Ergebnissen des Algorithmus entspricht – die Wahrscheinlichkeit von Kollisionen ist auf diese Weise h?her. Nicht cool!
Wohin von hier aus?
Ich werde nicht n?her auf die Implementierung eingehen, die in den realen Anwendungen verwendet wird, aber für diejenigen, die neugierig sind, werde ich zwei RFCs vorstellen, die einen Blick wert sind:
- HOTP: Ein HMAC-basierter Einmalpasswort-Algorithmus
- TOTP: Zeitbasierter Einmalpasswort-Algorithmus
Und hier ist die Pseudocode-Implementierung, die basierend auf den oben genannten RFCs wie beabsichtigt funktioniert:
Current timestamp: 1733692423 Base: 57789747 Code: 789747
Wie Sie sehen, sind wir dem schon sehr nahe gekommen, aber der ursprüngliche Algorithmus verwendet ein fortgeschritteneres Hashing (HMAC-SHA1 in diesem Beispiel) und führt einige bitweise Operationen aus, um die Ausgabe zu normalisieren.
Sicherheitsüberlegungen: Wiederverwenden statt selbst erstellen
Bevor wir Schluss machen, m?chte ich jedoch noch auf eine Sache eingehen: die Sicherheit. Ich würde Ihnen dringend davon abraten, die Logik der Generierung von OTPs selbst zu implementieren, da es viele Bibliotheken gibt, die das für uns erledigen. Der Spielraum für Fehler ist riesig und der Weg zur Schwachstelle, die von den b?sen Akteuren da drau?en entdeckt und ausgenutzt wird, ist kurz.
Selbst wenn Sie die Generierungslogik richtig hinbekommen und sie mit Tests abdecken, gibt es noch andere Dinge, die schief gehen k?nnen. Wie viel wird Ihrer Meinung nach beispielsweise n?tig sein, um den 6-stelligen Code brutal zu erzwingen? Lass uns experimentieren:
// gets current timestamp: current := time.Now().Unix() fmt.Println("Current timestamp: ", current)
Lassen Sie uns diesen Code ausführen:
Current timestamp: 1733691162
Und noch einmal:
// gets current timestamp current := time.Now().Unix() fmt.Println("Current timestamp: ", current) // gets a number that is stable within 30 seconds base := current / 30 fmt.Println("Base: ", base)
Wie Sie sehen, dauert es etwa 70 ms, den Code mithilfe einer einfachen Brute-Forcing-For-Schleife zu erraten. Das ist 400-mal schneller als die Lebensdauer des OTP! Der Server der App/Website, die den OTP-Mechanismus verwendet, muss dies verhindern, indem er beispielsweise nach drei fehlgeschlagenen Versuchen in den n?chsten 5 oder 10 Sekunden keine neuen Codes akzeptiert. Auf diese Weise erh?lt der Angreifer innerhalb des 30-Sekunden-Fensters nur 18 bzw. entsprechend 9 Versuche, was für den Pool von 1 Million m?glichen Werten nicht ausreicht.
Und es gibt noch andere Dinge wie diese, die man leicht übersieht. Lassen Sie mich also wiederholen: Bauen Sie nicht von Grund auf auf, sondern verlassen Sie sich auf die vorhandenen L?sungen.
Wie auch immer, ich hoffe, dass Sie heute etwas Neues gelernt haben und die OTP-Logik von nun an kein R?tsel mehr für Sie sein wird. Auch wenn Sie irgendwann im Leben Ihr Offline-Ger?t dazu bringen müssen, mithilfe eines reproduzierbaren Algorithmus einige Werte zu generieren, wissen Sie, wo Sie anfangen sollen.
Vielen Dank für die Zeit, die Sie mit dem Lesen dieses Beitrags verbracht haben, und viel Spa?! =)
P.S. Erhalten Sie eine E-Mail, sobald ich einen neuen Beitrag ver?ffentliche – abonnieren Sie ihn hier
P.P.S. Wie die anderen coolen Kids habe ich in letzter Zeit ein Bluesky-Konto erstellt, also helfen Sie mir bitte, meinen Feed unterhaltsamer zu gestalten =)
Das obige ist der detaillierte Inhalt vonOTPs entmystifizieren: die Logik hinter der Offline-Generierung von Token. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Hei?e KI -Werkzeuge

Undress AI Tool
Ausziehbilder kostenlos

Undresser.AI Undress
KI-gestützte App zum Erstellen realistischer Aktfotos

AI Clothes Remover
Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

Clothoff.io
KI-Kleiderentferner

Video Face Swap
Tauschen Sie Gesichter in jedem Video mühelos mit unserem v?llig kostenlosen KI-Gesichtstausch-Tool aus!

Hei?er Artikel

Hei?e Werkzeuge

Notepad++7.3.1
Einfach zu bedienender und kostenloser Code-Editor

SublimeText3 chinesische Version
Chinesische Version, sehr einfach zu bedienen

Senden Sie Studio 13.0.1
Leistungsstarke integrierte PHP-Entwicklungsumgebung

Dreamweaver CS6
Visuelle Webentwicklungstools

SublimeText3 Mac-Version
Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

Hei?e Themen

GO kompiliert das Programm standardm??ig in eine eigenst?ndige Bin?rdatei. Der Hauptgrund ist die statische Verknüpfung. 1. Einfacher Bereitstellung: Keine zus?tzliche Installation von Abh?ngigkeitsbibliotheken kann direkt über Linux -Verteilungen ausgeführt werden. 2. Gr??ere bin?re Gr??e: einschlie?lich aller Abh?ngigkeiten führt zu einer Erh?hung der Dateigr??e, kann jedoch durch Erstellen von Flags oder Komprimierungswerkzeugen optimiert werden. 3.. H?here Vorhersagbarkeit und Sicherheit: Vermeiden Sie Risiken, die durch ?nderungen der externen Bibliotheksversionen verursacht werden und die Stabilit?t verbessern; 4. Flexibilit?t begrenzter Betrieb: Kann nicht hei?es Update der gemeinsam genutzten Bibliotheken sowie eine Neukompilien und Bereitstellung erforderlich sind, um Abh?ngigkeitsf?lligkeiten zu beheben. Diese Funktionen sind für CLI-Tools, Microservices und andere Szenarien geeignet. In Umgebungen, in denen die Speicherung eingeschr?nkt ist oder auf zentrales Management beruht, sind Kompromisse erforderlich.

GOENSURSMEMORYSFETTYWITHOUTMANUALMANUMAGETROUGHAUTOMATICGARBAGECOLLECTION, Nopointerarithmetic, SafeConcurrency, Andruntimechecks.First, Go’sgarbageboceColectorAutomaticReclaimsUnusedMemory, Verhinderung von Verhinderung der Verhinderung von Verhinderung der

Um einen Pufferkanal in GO zu erstellen, geben Sie einfach die Kapazit?tsparameter in der Funktion machen. Mit dem Pufferkanal kann der Sendungsvorgang Daten vorübergehend speichern, wenn kein Empf?nger vorliegt, solange die angegebene Kapazit?t nicht überschritten wird. Zum Beispiel erstellt CH: = make (Chanint, 10) einen Pufferkanal, der bis zu 10 Ganzzahlwerte speichern kann. Im Gegensatz zu ungelandten Kan?len werden die Daten beim Senden nicht sofort blockiert, aber die Daten werden vorübergehend im Puffer gespeichert, bis sie vom Empf?nger weggenommen werden. Beachten Sie bitte: 1. Die Kapazit?tseinstellung sollte angemessen sein, um Speicherabf?lle oder h?ufiges Blockieren zu vermeiden. 2. Der Puffer muss verhindern, dass Speicherprobleme im Puffer auf unbestimmte Zeit angesammelt werden. 3. Das Signal kann vom Chanstruct {} -Typ übergeben werden, um Ressourcen zu sparen; Zu den h?ufigen Szenarien geh?rt die Kontrolle der Anzahl der Parallelit?t, Herstellerverbrauchermodelle und Differenzierung

GO ist ideal für die Systemprogrammierung, da es die Leistung von kompilierten Sprachen wie C mit der Benutzerfreundlichkeit und Sicherheit moderner Sprachen kombiniert. 1. In Bezug auf Datei- und Verzeichnisoperationen unterstützt das Betriebssystempaket von Go unterstützt die Erstellung, L?schung, Umbenennung und überprüfung, ob Dateien und Verzeichnisse vorhanden sind. Verwenden Sie OS.ReadFile, um die gesamte Datei in einer Codezeile zu lesen, die zum Schreiben von Sicherungsskripten oder Protokollierungstools geeignet ist. 2. In Bezug auf die Prozessverwaltung kann die Funktion von Exec.Command des OS/EXEC -Pakets externe Befehle ausführen, die Ausgabe erfassen, Umgebungsvariablen festlegen, Eingangs- und Ausgangsflüsse umleiten und die Lebensdauer von Prozesslebenszyklen für Automatisierungstools und Bereitstellungsskripte geeignet sind. 3. In Bezug auf Netzwerk und Parallelit?t unterstützt das NET -Paket TCP/UDP -Programmierung, DNS -Abfrage und Originals?tze.

In der GO -Sprache muss eine Strukturmethode aufgerufen werden, muss zun?chst die Struktur und die Methode definieren, die den Empf?nger bindet, und auf sie zugreift mit einer Punktzahl. Nach der Definition des Strukturrechtecks ??kann die Methode über den Wertempf?nger oder den Zeigerempf?nger deklariert werden. 1. Verwenden Sie den Wertempf?nger wie Func (rrectangle) aa () int und rufen Sie ihn direkt über rect.Area () an; 2. Wenn Sie die Struktur ?ndern müssen, verwenden Sie den Zeigerempf?nger wie Func (R*Rechteck) Setwidth (...) und behandelt automatisch die Umwandlung von Zeigern und Werten. 3. Bei der Einbettung der Struktur wird die Methode der eingebetteten Struktur verbessert und kann direkt durch die ?u?ere Struktur aufgerufen werden. 4..

In Go ist eine Schnittstelle ein Typ, der Verhalten ohne Angabe der Implementierung definiert. Eine Schnittstelle besteht aus Methodensignaturen und jedem Typ, der diese Methoden implementiert, die die Schnittstelle automatisch erfüllt. Wenn Sie beispielsweise eine Lautsprecherschnittstelle definieren, die die Speak () -Methode enth?lt, k?nnen alle Typen, die die Methode implementieren, als Sprecher betrachtet werden. Schnittstellen eignen sich zum Schreiben gemeinsamer Funktionen, Abstrakt -Implementierungsdetails und Verwendung von Scheinobjekten im Testen. Das Definieren einer Schnittstelle verwendet das Schlüsselwort der Schnittstelle und listet Methodensignaturen auf, ohne den Typ ausdrücklich zu deklarieren, um die Schnittstelle zu implementieren. Gemeinsame Anwendungsf?lle umfassen Protokolle, Formatierung, Abstraktionen verschiedener Datenbanken oder Dienste sowie Benachrichtigungssysteme. Zum Beispiel k?nnen sowohl Hund- als auch Robotertypen Sprechmethoden implementieren und an dieselbe Anno weitergeben

In der GO-Sprache werden String-Operationen haupts?chlich über Strings-Pakete und integrierte Funktionen implementiert. 1.Strings.Contains () wird verwendet, um festzustellen, ob eine Zeichenfolge einen Substring enth?lt, und gibt einen booleschen Wert zurück. 2.Strings.index () kann den Ort finden, an dem das Substring zum ersten Mal erscheint und wenn es nicht existiert, gibt es -1 zurück. 3.Strings.ReplaceAll () kann alle übereinstimmenden Substrings ersetzen und auch die Anzahl der Ersetzungen durch Zeichenfolgen steuern. Replace (); 4.Len () Funktion wird verwendet, um die L?nge der Bytes der Zeichenfolge zu erhalten. Bei der Verarbeitung von Unicode müssen Sie jedoch auf den Unterschied zwischen Zeichen und Bytes achten. Diese Funktionen werden h?ufig in Szenarien wie Datenfilterung, Textanalyse und String -Verarbeitung verwendet.

TheGoiopackageProviDEnterFaCesLikeraderAndWritertOhandlei/ooperationsgerafigAcrossSources.1.io.
