💻 Angewandte Kryptografie · Technischer Deep Dive

Wie TOTP funktioniert: RFC 6238 Algorithmus in Python, JavaScript & Go implementieren

2F 2FA-Tool Sicherheits-Forschungslabor · Aktualisiert: Oktober 2026 · 16 Min. Lesezeit (2.700+ Wörter)
💡 Architektur-Abstrakt & Entwickler-Manifest

Unter den grundlegenden kryptografischen Protokollen der modernen Websicherheit zeichnet sich RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) durch seine bestechende mathematische Einfachheit aus. Es benötigt keine interaktiven Netzwerk-Handshakes, keine zentrale Public-Key-Infrastruktur (PKI) und keine Server-Roundtrips. Ausschließlich mit einem geteilten Base32-Geheimschlüssel und dem kontinuierlichen Fortschreiten der physischen Unix-Epochenzeit berechnen Client und Server alle 30 Sekunden identische, flüchtige 6-stellige Authentifizierungstoken mit 100 % mathematischer Gewissheit.

In diesem praxisnahen Leitfaden verzichten wir bewusst auf intransparente Drittanbieter-Bibliotheken (wie pyotp oder otplib) und bauen die vollständige kryptografische RFC 6238-Pipeline von Grund auf in Python, JavaScript und Go nach – validiert gegen die offiziellen IETF-Testvektoren.

01. Die Eleganz zeitgesteuerter Authentifizierung

Um TOTP zu verstehen, müssen wir zunächst dessen Vorgänger betrachten: RFC 4226 HOTP (HMAC-Based One-Time Password).

Bei HOTP teilen sich Client und Server einen gemeinsamen Ereigniszähler $C$. Jedes Mal, wenn der Anwender ein Token generiert, erhöht sich der Zähler ($C = C + 1$), und das Token wird aus $\text{HMAC-SHA-1}(K, C)$ abgeleitet. Obwohl mathematisch fundiert, wies HOTP eine gravierende betriebliche Schwachstelle auf: Zähler-Desynchronisation. Betätigte ein Benutzer versehentlich die Taste seines Hardware-Tokens fünfmal in der Hosentasche, stand der Client-Zähler bei $C=6$, während der Authentifizierungsserver bei $C=1$ verharrte – die Folge waren permanente Anmeldefehlschläge.

⏱️ Der Durchbruch von RFC 6238: Zähler durch standardisierte Epochenzeit ersetzen

Im Mai 2011 von N. M'Raihi, S. Machani, M. Pei und J. Rydell veröffentlicht, löste RFC 6238 das Desynchronisationsproblem elegant: Der inkrementierende Ganzzahl-Zähler wurde durch die standardisierte Unix-Epochenzeit ersetzt. Da die physikalische Zeit weltweit einheitlich voranschreitet, erhöhen Client und Server ihren internen Zähler simultan, ohne jemals ein einziges Datenpaket austauschen zu müssen.

02. Die 5 mathematisch-kryptografischen Schritte von TOTP

Die Umwandlung eines 16-stelligen Base32-Seeds (z. B. JBSWY3DPEHPK3PXP) in einen fortlaufenden 6-stelligen Code (z. B. 284915) umfasst exakt fünf kryptografische Phasen:

1 Schritt 1: Base32-Dekodierung des Geheimschlüssels

Die bei der 2FA-Einrichtung angezeigten Schlüssel sind nach RFC 4648 Base32-codiert und nutzen das Alphabet A-Z sowie 2-7 (ohne verwechselbare Zeichen wie 0, 1, 8, 9). Der Algorithmus normalisiert diesen String zunächst (Leerzeichen entfernen, Großschreibung) und ergänzt Padding-Zeichen (=), sodass die Länge ein Vielfaches von 8 ergibt, bevor er ihn in rohe binäre Schlüssel-Bytes $K$ dekodiert.

2 Schritt 2: Zeitschritt-Berechnung (Zähler $T$)

Der aktuelle Unix-Zeitstempel (verstrichene Sekunden seit dem 1. Januar 1970 UTC) wird auf den diskreten Ganzzahl-Zähler $T$ abgebildet:

T = floor( (CurrentTime - T0) / X )

Mit den Standardparametern $T_0 = 0$ und einer Schrittdauer von $X = 30$ Sekunden. Diese Ganzzahl $T$ wird anschließend als 8-Byte Big-Endian 64-Bit vorzeichenlose Ganzzahl formatiert.

3 Schritt 3: HMAC-SHA1 Hash-Berechnung

Unter Verwendung der rohen Schlüssel-Bytes $K$ als kryptografischem Schlüssel berechnen wir den HMAC-SHA1-Digest über den 8-Byte-Zeitpuffer. Das Ergebnis ist ein 20-Byte (160-Bit) langer Nachrichtenauthentifizierungshash $HS$:

HS = HMAC-SHA-1(Key, T)
4 Schritt 4: Dynamisches Abschneiden (Dynamic Truncation, DT)

Um aus den 20 rohen Bytes eine handhabbare Ganzzahl zu extrahieren, definiert RFC 4226 das dynamische Abschneiden (Dynamic Truncation):

  • Offset bestimmen: Das letzte Byte $HS[19]$ wird mit 0x0F bitweise maskiert, um die unteren 4 Bits zu extrahieren. Dies ergibt einen Offset-Wert zwischen 0 und 15: Offset = HS[19] & 0x0F;
  • 4 Bytes ausschneiden: Ausgehend von diesem Offset werden 4 aufeinanderfolgende Bytes entnommen: $P = HS[Offset \dots Offset+3]$;
  • Vorzeichenbit maskieren: Das höchstwertige Bit (MSB) wird mit 0x7F maskiert, um Mehrdeutigkeiten durch vorzeichenbehaftete 32-Bit-Ganzzahlen zu vermeiden:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Schritt 5: Modulo-Reduktion auf $N$ Stellen

Schließlich wird BinaryCode modulo $10^6$ (1.000.000) gerechnet, um das standardmäßige 6-stellige Token zu erhalten. Das Ergebnis wird mit führenden Nullen aufgefüllt, um immer eine exakt 6-stellige Zeichenkette zu garantieren:

OTP = BinaryCode mod 10^6

03. Lauffähiger Code 1: Python 3 Standardbibliothek

Diese vollständige Implementierung nutzt ausschließlich Pythons integrierte Module base64, struct, hmac und hashlib – es sind keinerlei externe Pip-Pakete erforderlich:

totp_rfc6238.py Python 3.8+ (Keine externen Abhängigkeiten)
import base64
import hashlib
import hmac
import struct
import time

def generate_totp(secret_base32: str, timestamp: int = None, step: int = 30, digits: int = 6) -> str: """ Generiert ein RFC 6238-konformes TOTP-Token. :param secret_base32: Geteilter Geheimschlüssel im Base32-Format (z. B. ‘JBSWY3DPEHPK3PXP’) :param timestamp: Unix-Epochenzeitstempel in Sekunden. Standardmäßig aktuelle Zeit :param step: Zeitintervallfenster in Sekunden (Standard: 30) :param digits: Anzahl der Ausgabeziffern (Standard: 6) :return: Mit Nullen links aufgefüllter OTP-String (z. B. ‘045812’) """ if timestamp is None: timestamp = int(time.time())

1. Base32-String normalisieren und mit ’=’ auf Vielfaches von 8 auffüllen

normalized_secret = secret_base32.replace(” ”, "").upper() padding_needed = (8 - len(normalized_secret) % 8) % 8 normalized_secret += ”=” * padding_needed key_bytes = base64.b32decode(normalized_secret)

2. 64-Bit Big-Endian Zeitschritt-Zähler T berechnen

time_counter = timestamp // step time_bytes = struct.pack(“>Q”, time_counter) # 8 Bytes Big-Endian unsigned long long

3. HMAC-SHA1-Digest berechnen (20 Bytes)

hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest()

4. Dynamisches Abschneiden (Dynamic Truncation)

offset = hmac_digest[-1] & 0x0F # Untere 4 Bits des 20. Bytes

4 Bytes ab Offset entpacken und MSB-Vorzeichenbit löschen

binary_code = struct.unpack(“>I”, hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF

5. Modulo-Arithmetik und Formatierung mit führenden Nullen

otp_code = binary_code % (10 ** digits) return str(otp_code).zfill(digits)

if name == “main”: test_secret = “JBSWY3DPEHPK3PXP” print(f”Berechneter TOTP-Code: {generate_totp(test_secret)}”)

04. Lauffähiger Code 2: JavaScript & Node.js Crypto

Reine JavaScript-Implementierung für serverseitige Node.js-Umgebungen unter Verwendung des nativen Moduls crypto.createHmac:

totp_node.js Node.js Natives Crypto
const crypto = require('crypto');

// RFC 4648 Base32-Decoder in nativem JavaScript function base32Decode(base32Str) { const alphabet = ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ234567’; const cleaned = base32Str.replace(/=+$/, ”).toUpperCase().replace(/\s+/g, ”); let bits = 0, value = 0, index = 0; const output = new Uint8Array(Math.floor((cleaned.length * 5) / 8));

for (let i = 0; i < cleaned.length; i++) { const val = alphabet.indexOf(cleaned[i]); if (val === -1) continue; value = (value << 5) | val; bits += 5; if (bits >= 8) { output[index++] = (value >>> (bits - 8)) & 255; bits -= 8; } } return Buffer.from(output.buffer, 0, index); }

function generateTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000), step = 30, digits = 6) { const key = base32Decode(secretBase32); const counter = Math.floor(timestamp / step);

// 8-Byte Puffer im Big-Endian-Format allozieren const buffer = Buffer.alloc(8); buffer.writeBigUInt64BE(BigInt(counter), 0);

// HMAC-SHA1 berechnen const hmac = crypto.createHmac(‘sha1’, key).update(buffer).digest();

// Dynamisches Abschneiden (Dynamic Truncation) const offset = hmac[hmac.length - 1] & 0x0f; const binary = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff);

const otp = binary % (10 ** digits); return otp.toString().padStart(digits, ‘0’); }

console.log(“Berechneter TOTP:”, generateTOTP(“JBSWY3DPEHPK3PXP”));

Clientseitige Ausführung im Browser via W3C Web Crypto API:

Für moderne Webanwendungen, die 100 % offline-fähige Sicherheit garantieren möchten (exakt die Architektur von 2FA-Tool), stellt die standardisierte W3C Web Crypto API asynchrone, hardwarebeschleunigte kryptografische Primitiven direkt im Browser bereit – ganz ohne npm-Pakete:

webcrypto_totp.js Browser W3C Web Crypto API
async function generateBrowserTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000)) {
const keyBytes = base32Decode(secretBase32);
const counter = Math.floor(timestamp / 30);

// 1. Rohe Schlüssel-Bytes in SubtleCrypto importieren const cryptoKey = await window.crypto.subtle.importKey( “raw”, keyBytes, { name: “HMAC”, hash: { name: “SHA-1” } }, false, [“sign”] );

// 2. 8-Byte Big-Endian Zählerpuffer erzeugen const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, BigInt(counter), false); // false = Big-Endian

// 3. Kryptografische HMAC-Signatur berechnen const signature = await window.crypto.subtle.sign(“HMAC”, cryptoKey, buffer); const hmacResult = new Uint8Array(signature);

// 4. Dynamisches Abschneiden const offset = hmacResult[hmacResult.length - 1] & 0x0f; const binary = ((hmacResult[offset] & 0x7f) << 24) | ((hmacResult[offset + 1] & 0xff) << 16) | ((hmacResult[offset + 2] & 0xff) << 8) | (hmacResult[offset + 3] & 0xff);

const otp = binary % 1000000; return otp.toString().padStart(6, ‘0’); }

05. Lauffähiger Code 3: Produktives Golang (Go)

Hochleistungs-Microservices profitieren von Gos robuster Standardbibliothek:

totp.go Go 1.20+ Standardbibliothek
package main

import ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “math” “strings” “time” )

// GenerateTOTP berechnet standardkonforme RFC 6238 zeitbasierte Einmalkennwörter func GenerateTOTP(secretBase32 string, timestamp int64, step int64, digits int) (string, error) { // 1. Base32-Seed bereinigen und dekodieren cleaned := strings.ToUpper(strings.ReplaceAll(secretBase32, ” ”, "")) key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(cleaned) if err != nil { key, err = base32.StdEncoding.DecodeString(cleaned) if err != nil { return "", err } }

// 2. 64-Bit Big-Endian Zeitschritt-Zähler kodieren counter := uint64(timestamp / step) buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, counter)

// 3. HMAC-SHA1 berechnen mac := hmac.New(sha1.New, key) mac.Write(buf) hash := mac.Sum(nil)

// 4. Dynamisches Abschneiden offset := hash[len(hash)-1] & 0x0f binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff

// 5. Modulo-Arithmetik mod := uint32(math.Pow10(digits)) otp := binaryCode % mod

format := fmt.Sprintf(“%%0%dd”, digits) return fmt.Sprintf(format, otp), nil }

func main() { code, err := GenerateTOTP(“JBSWY3DPEHPK3PXP”, time.Now().Unix(), 30, 6) if err != nil { panic(err) } fmt.Printf(“Golang berechneter TOTP: %s\n”, code) }

06. Validierung anhand der Testvektoren aus RFC 6238 Anhang B

Um die strikte Konformität sicherzustellen, muss jede kryptografische Implementierung anhand der offiziellen Testvektoren aus IETF RFC 6238 Anhang B verifiziert werden.

Die offizielle Testsuite verwendet den ASCII-Schlüssel "12345678901234567890" (20 Bytes), welcher in Base32 codiert lautet:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (evaluiert für 8-stellige Ausgaben im 30-Sekunden-Intervall):

Unix-Epochenzeitstempel UTC Datum & Uhrzeit Offizieller RFC-Referenzcode Testergebnis
59 1970-01-01 00:00:59 94287082 ✓ BESTANDEN (Exakte Übereinstimmung)
1111111109 2005-03-18 01:58:29 07081804 ✓ BESTANDEN (Exakte Übereinstimmung)
1111111111 2005-03-18 01:58:31 14050471 ✓ BESTANDEN (Exakte Übereinstimmung)
1234567890 2009-02-13 23:31:30 89005924 ✓ BESTANDEN (Exakte Übereinstimmung)
2000000000 2033-05-18 03:33:20 69279037 ✓ BESTANDEN (Exakte Übereinstimmung)

07. Produktionspraxis: Zeittoleranz, Replay-Schutz & Rate-Limits

Beim Einsatz der TOTP-Verifizierung in hochverfügbaren Authentifizierungssystemen müssen drei fundamentale Sicherheitsanforderungen durchgesetzt werden:

Regel 1 Zeittoleranzfenster für Clock Drift

Aufgrund minimaler Ungenauigkeiten mobiler Smartphone-Uhren sollten Server das Zeitfenster [T - 1, T, T + 1] evaluieren (die vorherigen 30s, aktuellen 30s und nächsten 30s prüfen). Erweitern Sie dieses Fenster keinesfalls über $\pm 1$ Intervall hinaus, da jedes zusätzliche Intervall die Angriffsfläche für Replay-Attacken um weitere 30 Sekunden vergrößert.

Regel 2 Einmalige Token-Nutzung (Anti-Replay)

Ein Token muss unmittelbar nach erfolgreicher Verifizierung entwertet werden. Speichern Sie den Wert Last_Verified_Time_Step in Redis oder einem In-Memory-Cache. Versucht ein Client, denselben Code innerhalb desselben 30-Sekunden-Fensters erneut einzureichen, verwerfen Sie die Anfrage, um Man-in-the-Middle-Replay-Angriffe wirksam abzuwehren.

Regel 3 Brute-Force Rate Limiting & Zeitkonstanter Vergleich

Da 6-stellige Codes nur 1.000.000 Permutationen umfassen, können ungeschützte Endpunkte rasch per Brute-Force überwunden werden. Erzwingen Sie progressive Verzögerungen und Kontosperren nach 3 aufeinanderfolgenden Fehlversuchen. Führen Sie Zeichenkettenvergleiche im Code stets mit zeitkonstanten Funktionen durch (z. B. crypto/subtle.ConstantTimeCompare in Go oder crypto.timingSafeEqual in Node.js), um Seitenkanal-Timing-Angriffe zu verhindern.

08. Häufig gestellte Fragen (FAQ)

F1: Da SHA-1 kryptografisch gebrochen ist, warum setzt TOTP standardmäßig immer noch auf HMAC-SHA1? ▼

Die bekannten Kollisionsangriffe gegen reines SHA-1 brechen nicht die HMAC-Konstruktion (RFC 2104). In HMAC ist die Hashfunktion in innere und äußere Schlüsseloperationen eingebettet, was kollisionsbasierte Angriffe wirksam neutralisiert. Bisher gilt HMAC-SHA1 für Einmalkennwörter als kryptografisch sicher. RFC 6238 unterstützt jedoch uneingeschränkt auch HMAC-SHA256 und HMAC-SHA512 für besonders hohe Sicherheitsanforderungen.

F2: Beeinflussen Zeitzonen oder die Sommer-/Winterzeit (DST) die TOTP-Synchronisation? ▼

Nein. Die Unix-Epochenzeit ist definiert als die verstrichenen Sekunden seit dem 1. Januar 1970 um 00:00:00 UTC. Sie ist vollkommen unabhängig von lokalen Zeitzonen, Schaltsekunden oder Sommerzeitanpassungen. Zu jedem beliebigen Zeitpunkt besitzen ein Server in Tokio und ein Smartphone in Berlin exakt denselben Unix-Ganzzahl-Zeitstempel.

F3: Wie unterscheidet sich die 5-Zeichen-Authentifizierung von Steam Guard von RFC 6238? ▼

Steam Guard ist eine proprietäre Variante auf Basis von RFC 6238. Es nutzt denselben 30-Sekunden-Zeitschritt und dieselbe HMAC-SHA1-Berechnung. Im finalen Reduktionsschritt mappt Steam die extrahierte Ganzzahl jedoch über ein benutzerdefiniertes Alphabet aus 26 Zeichen (23456789BCDFGHJKMNPQRTVWXY), um einen 5-stelligen alphanumerischen Code anstelle von 6 Dezimalziffern zu erzeugen.

RFC 6238 Konform · W3C Web Crypto API Client-Side Computing

Erleben Sie 100 % clientseitige TOTP-Generierung live

2FA-Tool implementiert diese exakte kryptografische Pipeline direkt in Ihrem Browser über die W3C Web Crypto API. Keine Serverübertragung, keine Token-Speicherung, sofortige Validierung.