Viele Konten fragen nach dem Passwort noch nach einem sechsstelligen Code aus einer App, der alle 30 Sekunden wechselt. Du baust das Verfahren dahinter selbst, in Ruby oder Python: einen Schlüssel in Base32, einen Code aus Schlüssel und Zähler, einen Zähler aus der Uhrzeit. Am Ende scannst du deinen eigenen Schlüssel mit dem Handy, und deine App zeigt dieselben Codes wie dein Programm.
Ein Passwort ist etwas, das du weißt. Das Problem: Andere können es auch erfahren. Es taucht in einem Datenleck auf, weil eine Website es schlecht gespeichert hat, jemand hat dir über die Schulter geschaut, oder du hast es bei mehreren Diensten benutzt. Ein zweiter Faktor ist etwas anderes: etwas, das du hast, zum Beispiel dein Handy. Der Server fragt dann nach dem Passwort und nach einem Code, den nur das Handy kennt.
Wie kann das Handy einen Code kennen, den der Server nachprüfen kann, ohne dass die beiden miteinander reden? Beim Einrichten bekommen beide denselben geheimen Schlüssel, meistens über einen QR-Code. Danach rechnen beide unabhängig voneinander aus dem Schlüssel und der Uhrzeit einen Code aus. Weil sie dieselbe Rechnung mit denselben Zahlen machen, kommt dasselbe heraus. Wer den Schlüssel nicht hat, kann den Code nicht ausrechnen, auch nicht, wenn er hundert frühere Codes gesehen hat.
Jemand hat dein Passwort erfahren. Bei deinem Konto ist ein zweiter Faktor eingeschaltet, eine Authenticator-App auf deinem Handy. Was bräuchte er außerdem, um sich anzumelden?
Das Verfahren heißt TOTP (»Time-based One-Time Password«) und steht in zwei kurzen Standards, RFC 4226 und RFC 6238. Jede Authenticator-App kann es. Deshalb funktioniert dein eigenes Programm am Ende mit ihr zusammen.
Lege einen Ordner an und öffne ihn mit StrgK StrgO:
mkdir /workspace/totp
Lege darin totp.rb an:
# Einmalcodes wie in einer Authenticator-App require 'openssl' require 'securerandom' ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' # Base32 in Bytes: jedes Zeichen sind 5 Bits, je 8 Bits ein Byte. def base32_decode(text) ''.b end # Bytes in Base32, ohne = am Ende. def base32_encode(data) '' end # Der Code für einen Zähler. def hotp(key, counter, digits = 6) '000000' end p base32_decode('JBSWY3DPEHPK3PXP')
Mit ruby totp.rb läuft das Programm unten in der Datei; die Prüfungen der Aufgaben laden nur deine Methoden und rufen sie direkt auf.
Lege darin totp.py an:
# Einmalcodes wie in einer Authenticator-App import hashlib import hmac import os import secrets import time ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' def base32_decode(text): """Base32 in Bytes: jedes Zeichen sind 5 Bits, je 8 Bits ein Byte.""" return b'' def base32_encode(data): """Bytes in Base32, ohne = am Ende.""" return '' def hotp(key, counter, digits=6): """Der Code für einen Zähler.""" return '000000' print(base32_decode('JBSWY3DPEHPK3PXP'))
Mit python3 totp.py läuft das Programm unten in der Datei; die Prüfungen der Aufgaben laden nur deine Funktionen und rufen sie direkt auf.
Der Schlüssel sind 20 zufällige Bytes. Damit man ihn auch abtippen kann, wenn der QR-Code nicht geht, schreiben Apps ihn in Base32: 32 Zeichen, die Großbuchstaben A bis Z und die Ziffern 2 bis 7. Die 0, die 1, die 8 und die 9 fehlen mit Absicht, weil man sie mit O, I und l, B und g verwechseln kann. Und Groß- und Kleinschreibung spielt keine Rolle.
32 Zeichen sind \(2^5\): Jedes Zeichen steht für genau 5 Bits, A für 00000, B für 00001, … 7 für 11111. Zum Lesen hängst du die Bits aller Zeichen aneinander und nimmst je 8 davon als ein Byte. Bleiben am Ende weniger als 8 Bits übrig, gehören sie zu keinem Byte mehr. Zum Schreiben geht es umgekehrt: die Bits aller Bytes aneinander, am Ende mit Nullen auffüllen, bis die Zahl durch 5 teilbar ist, und je 5 Bits ein Zeichen.
Wie viele Bytes stecken in den 32 Zeichen des Base32-Schlüssels GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ?
Am einfachsten rechnest du mit den Bits als Text aus Nullen und Einsen: 5.to_s(2).rjust(5, '0') ist "00101", und "01001000".to_i(2) ist 72. scan(/.{8}/) teilt einen Text in Stücke zu 8 Zeichen, pack('C*') macht aus einer Liste von Zahlen Bytes. Kleinbuchstaben, Leerzeichen und = am Ende, die in echten Schlüsseln vorkommen, sollen dich nicht stören.
Am einfachsten rechnest du mit den Bits als Text aus Nullen und Einsen: format(5, '05b') ist '00101', und int('01001000', 2) ist 72. bytes([72, 105]) macht aus einer Liste von Zahlen Bytes. Kleinbuchstaben, Leerzeichen und = am Ende, die in echten Schlüsseln vorkommen, sollen dich nicht stören. Python hat mit base64.b32decode schon eine fertige Funktion; die darfst du benutzen, um deine zu prüfen.
Schreib base32_decode und base32_encode in totp.rb.
Mach aus jedem Zeichen seine 5 Bits als Text aus Nullen und Einsen, häng alles aneinander und nimm dann je 8 davon.
Schreib base32_decode und base32_encode in totp.py.
Mach aus jedem Zeichen seine 5 Bits als Text aus Nullen und Einsen, häng alles aneinander und nimm dann je 8 davon.
Der Code kommt aus einer HMAC. Das ist eine Prüfsumme wie SHA-1 aus Kryptographie im Terminal, nur mit einem Schlüssel gemischt: Wer den Schlüssel nicht kennt, kann sie nicht ausrechnen, und an der Prüfsumme sieht man dem Schlüssel nichts an. Für einen Zähler, der bei jeder Anmeldung um 1 weiterzählt, geht HOTP so:
00 00 00 00 00 00 00 01.Mit dem Schlüssel 12345678901234567890 aus dem Standard und dem Zähler 1 ist die HMAC 75a48a19d4cbe100644e8ac1397eea747a2d33ab. Das letzte Byte ist ab, seine untersten 4 Bits sind b, also 11. Die 4 Bytes ab Byte 11 sind c1 39 7e ea; ohne das oberste Bit wird aus c1 eine 41, und 41397eea ist 1094287082. Der Code ist 287082.
OpenSSL::HMAC.digest('SHA1', key, data) rechnet die HMAC aus. Den Zähler als 8 Bytes macht [counter].pack('Q>'), und bytes[o, 4].unpack1('N') liest 4 Bytes ab Index o als Zahl, das höchste zuerst. Das oberste Bit löschst du mit & 0x7fffffff.
hmac.new(key, data, hashlib.sha1).digest() rechnet die HMAC aus. Den Zähler als 8 Bytes macht counter.to_bytes(8, 'big'), und int.from_bytes(hash[o:o + 4], 'big') liest 4 Bytes ab Index o als Zahl, das höchste zuerst. Das oberste Bit löschst du mit & 0x7fffffff.
Schreib hotp in totp.rb. Die Codes sind die Testwerte aus RFC 4226.
Der Zähler kommt als 8 Bytes in die HMAC, das höchste zuerst. Vom Ergebnis sagen die untersten 4 Bits des letzten Bytes, wo die 4 Bytes anfangen, aus denen der Code wird.
Schreib hotp in totp.py. Die Codes sind die Testwerte aus RFC 4226.
Der Zähler kommt als 8 Bytes in die HMAC, das höchste zuerst. Vom Ergebnis sagen die untersten 4 Bits des letzten Bytes, wo die 4 Bytes anfangen, aus denen der Code wird.
Ein Zähler, der nur bei jeder Anmeldung weiterzählt, hat einen Haken: Handy und Server müssen sich einig sein, wie weit er schon ist. TOTP nimmt stattdessen die Uhrzeit als Zähler. Computer zählen die Zeit in Sekunden seit dem 1. Januar 1970, der Unix-Zeit; date +%s im Terminal zeigt sie. Geteilt durch 30 und abgerundet ist das der Zähler: Er wird alle 30 Sekunden um 1 größer, und genau so oft wechselt der Code in deiner App.
Schreib totp(key, time), das mit deinem hotp den Code für eine Unix-Zeit ausrechnet; ohne Zeit nimmt es die aktuelle. Die Prüfung vergleicht mit den Testwerten aus RFC 6238.
Schreib totp in totp.rb. Die Codes sind die Testwerte aus RFC 6238.
Der Zähler ist die Unix-Zeit geteilt durch 30, abgerundet. Alles andere macht schon hotp.
Schreib totp in totp.py. Die Codes sind die Testwerte aus RFC 6238.
Der Zähler ist die Unix-Zeit geteilt durch 30, abgerundet. Alles andere macht schon hotp.
Jetzt kommt dein Programm mit einer echten App zusammen. Es braucht dafür einen eigenen Schlüssel und einen QR-Code, den die App scannen kann. Im QR-Code steht eine Adresse, die jede Authenticator-App versteht:
otpauth://totp/Hackschule:Workspace?secret=JBSWY3DPEHPK3PXP&issuer=Hackschule
Den QR-Code macht eine Bibliothek:
gem install rqrcode
Ersetze das Programm unten in der Datei, alles nach der letzten Methode, durch dieses:
require 'rqrcode' path = File.join(__dir__, 'secret.txt') File.write(path, base32_encode(SecureRandom.random_bytes(20)) + "\n") unless File.exist?(path) secret = File.read(path).strip uri = "otpauth://totp/Hackschule:Workspace?secret=#{secret}&issuer=Hackschule" RQRCode::QRCode.new(uri).as_png(size: 300).save(File.join(__dir__, 'qr.png')) puts "Geheimnis: #{secret}" puts 'Scanne qr.png mit deiner Authenticator-App. Beenden mit Strg+C.' begin loop do now = Time.now.to_i print "\rCode: #{totp(base32_decode(secret), now)}, noch #{30 - now % 30} Sekunden " sleep 1 end rescue Interrupt puts end
Beim ersten Start würfelt das Programm einen Schlüssel aus 20 Bytes und speichert ihn in secret.txt; danach nimmt es immer denselben. Starte es mit ruby totp.rb und öffne qr.png in VS Code.

Den QR-Code macht eine Bibliothek:
pip install "qrcode[pil]"
Ersetze das Programm unten in der Datei, alles nach der letzten Funktion, durch dieses:
import qrcode path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'secret.txt') if not os.path.exists(path): with open(path, 'w') as f: f.write(base32_encode(secrets.token_bytes(20)) + '\n') with open(path) as f: secret = f.read().strip() uri = f'otpauth://totp/Hackschule:Workspace?secret={secret}&issuer=Hackschule' qrcode.make(uri).save(os.path.join(os.path.dirname(path), 'qr.png')) print(f'Geheimnis: {secret}') print('Scanne qr.png mit deiner Authenticator-App. Beenden mit Strg+C.') try: while True: now = int(time.time()) print(f'\rCode: {totp(base32_decode(secret), now)}, noch {30 - now % 30} Sekunden ', end='', flush=True) time.sleep(1) except KeyboardInterrupt: print()
Beim ersten Start würfelt das Programm einen Schlüssel aus 20 Bytes und speichert ihn in secret.txt; danach nimmt es immer denselben. Starte es mit python3 totp.py und öffne qr.png in VS Code.

Scanne den QR-Code mit einer Authenticator-App auf deinem Handy, zum Beispiel FreeOTP, Aegis oder der App, die du schon hast. Nach ein paar Sekunden zeigt sie dieselben sechs Ziffern wie dein Programm und wechselt im selben Augenblick. Dein Handy und dein Workspace haben nie miteinander geredet; sie rechnen nur dasselbe.
secret.txt ist ein echter Schlüssel: Wer ihn hat, kann deine Codes ausrechnen. Für dieses Programm schadet das nicht, es schützt ja nichts. Ein echter Server bewahrt solche Schlüssel aber so sorgfältig auf wie Passwörter, und in ein Git-Repository gehören sie nie. Den Eintrag in deiner App kannst du danach wieder löschen.
Der Server hat dieselbe Rechnung und prüft, ob der eingetippte Code stimmt. Ganz so einfach ist es aber nicht.
Die Uhr deines Handys geht 20 Sekunden nach. Was passiert, wenn der Server nur den Code des aktuellen Zeitfensters annimmt?
Server nehmen deshalb auch den Code des Zeitfensters davor und danach an, also insgesamt 90 Sekunden. Viele Apps zeigen den Code außerdem mit einem Leerzeichen in der Mitte, 287 082; auch das soll nicht stören.
Schreib verify in totp.rb.
Rechne die Codes für die Zeit davor, jetzt und danach aus, jeweils 30 Sekunden auseinander, und schau, ob einer passt.
Schreib verify in totp.py.
Rechne die Codes für die Zeit davor, jetzt und danach aus, jeweils 30 Sekunden auseinander, und schau, ob einer passt.
Warum soll ein Server einen Code, mit dem sich jemand gerade angemeldet hat, kein zweites Mal annehmen, obwohl er noch gilt?
Ein zweiter Faktor macht ein Passwort allein wertlos: Taucht es in einem Datenleck auf, kann niemand etwas damit anfangen, solange er dein Handy nicht hat. Das ist der häufigste Fall, und dafür lohnt sich der zweite Faktor bei jedem wichtigen Konto.
Er hat aber Grenzen. Wer dein entsperrtes Handy in der Hand hat, hat beide Faktoren. Und tippst du den Code selbst auf einer gefälschten Seite ein, die genauso aussieht wie die echte, kann sie ihn sofort weiterreichen; der Code weiß nicht, auf welcher Seite er eingetippt wird. Dagegen helfen Passkeys: Dort prüft das Handy selbst, mit welcher Adresse es redet, und antwortet nur der echten. Codes per SMS sind schwächer als eine App, weil eine Telefonnummer leichter auf jemand anderen umgeleitet werden kann als ein Schlüssel auf deinem Handy.
Wovor schützt dich ein zweiter Faktor, und wovor nicht? Denk an ein Passwort, das in einem Datenleck auftaucht, an ein gestohlenes Handy und an eine gefälschte Anmeldeseite, auf der du den Code selbst eintippst.
verify in den Server aus Ein eigener Webserver ein, mit einer Anmeldeseite, die erst das Passwort und dann den Code verlangt, und merk dir benutzte Codes.