Projekt (Ruby)
image: totp-app.webp kind: projekt language: Ruby / Python languages: [ruby, python] duration: 2 Doppelstunden requires: [crypto]

Zwei Faktoren: Einmalcodes selbst gebaut

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.

1. Etwas, das du weißt, und etwas, das du hast

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.

Vermutung 1Wenn das Passwort bekannt ist

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?

Wenn das Passwort bekannt ist

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.

2. Einrichten

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.

3. Der Schlüssel in Base32

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.

Aufgabe 2Wie viele Bytes?

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.

Aufgabe 3Base32

Schreib base32_decode und base32_encode in totp.rb.

Tipp

Mach aus jedem Zeichen seine 5 Bits als Text aus Nullen und Einsen, häng alles aneinander und nimm dann je 8 davon.

Aufgabe 3Base32

Schreib base32_decode und base32_encode in totp.py.

Tipp

Mach aus jedem Zeichen seine 5 Bits als Text aus Nullen und Einsen, häng alles aneinander und nimm dann je 8 davon.

4. Ein Code aus Schlüssel und Zähler

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:

  1. Schreib den Zähler als 8 Bytes, das höchste zuerst. Zähler 1 sind die Bytes 00 00 00 00 00 00 00 01.
  2. Rechne die HMAC mit SHA-1 aus Schlüssel und diesen 8 Bytes. Heraus kommen 20 Bytes.
  3. Die untersten 4 Bits des letzten Bytes sind eine Zahl von 0 bis 15, der Offset. Ab dort nimmst du 4 Bytes als eine Zahl, das höchste zuerst, und löschst ihr oberstes Bit, damit sie nie negativ ist.
  4. Der Code sind die letzten 6 Ziffern dieser Zahl, also der Rest beim Teilen durch \(10^6\), vorn mit Nullen aufgefüllt.

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.

Aufgabe 4HOTP

Schreib hotp in totp.rb. Die Codes sind die Testwerte aus RFC 4226.

Tipp

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.

Aufgabe 4HOTP

Schreib hotp in totp.py. Die Codes sind die Testwerte aus RFC 4226.

Tipp

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.

5. Der Zähler ist die Uhr

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.

Aufgabe 5TOTP

Schreib totp in totp.rb. Die Codes sind die Testwerte aus RFC 6238.

Tipp

Der Zähler ist die Unix-Zeit geteilt durch 30, abgerundet. Alles andere macht schon hotp.

Aufgabe 5TOTP

Schreib totp in totp.py. Die Codes sind die Testwerte aus RFC 6238.

Tipp

Der Zähler ist die Unix-Zeit geteilt durch 30, abgerundet. Alles andere macht schon hotp.

6. Mit dem Handy

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.

VS Code mit einem QR-Code in qr.png; im Terminal darunter zeigt totp.rb den Schlüssel in Base32 und den aktuellen Code mit den Sekunden, die er noch gilt.

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.

VS Code mit einem QR-Code in qr.png; im Terminal darunter zeigt totp.py den Schlüssel in Base32 und den aktuellen Code mit den Sekunden, die er noch gilt.

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.

7. Prüfen wie ein Server

Der Server hat dieselbe Rechnung und prüft, ob der eingetippte Code stimmt. Ganz so einfach ist es aber nicht.

Vermutung 6Wenn die Uhr nachgeht

Die Uhr deines Handys geht 20 Sekunden nach. Was passiert, wenn der Server nur den Code des aktuellen Zeitfensters annimmt?

Wenn die Uhr nachgeht

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.

Aufgabe 7Einen Code prüfen

Schreib verify in totp.rb.

Tipp

Rechne die Codes für die Zeit davor, jetzt und danach aus, jeweils 30 Sekunden auseinander, und schau, ob einer passt.

Aufgabe 7Einen Code prüfen

Schreib verify in totp.py.

Tipp

Rechne die Codes für die Zeit davor, jetzt und danach aus, jeweils 30 Sekunden auseinander, und schau, ob einer passt.

Aufgabe 8Nur einmal

Warum soll ein Server einen Code, mit dem sich jemand gerade angemeldet hat, kein zweites Mal annehmen, obwohl er noch gilt?

Nur einmal

8. Wovor der zweite Faktor schützt

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.

Aufgabe 9Wovor schützt der zweite Faktor?

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.

Mindestens 25 Wörter. Nur du und deine Lehrkraft können die Antwort lesen.

9. Wie geht es weiter?

  • Backup-Codes: Was passiert, wenn das Handy verloren geht? Dienste geben beim Einrichten eine Liste von Codes, die je einmal gelten. Wie würdest du sie speichern, damit auch ein Datenleck sie nicht verrät? Kryptographie im Terminal hat dazu die Idee.
  • Ein eigener Server: Bau 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.
  • HOTP mit Zähler: Manche Schlüsselanhänger zählen statt der Zeit bei jedem Knopfdruck weiter. Wie bleiben Anhänger und Server im Gleichschritt, wenn jemand den Knopf drückt, ohne sich anzumelden?