Onder de fundamentele cryptografische protocollen in moderne webbeveiliging onderscheidt RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) zich door zijn wiskundige eenvoud. Het vereist geen interactieve netwerk-handshakes, geen centrale Public Key Infrastructure (PKI) en geen retourtjes naar de cloud. Met slechts een gedeelde geheime Base32-sleutel en de onafgebroken voorwaartse voortgang van de fysieke Unix-tijd, berekenen client en server met 100% wiskundige zekerheid elke 30 seconden identieke, kortstondige 6-cijferige authenticatietokens.
In deze uitgebreide handleiding slaan we ondoorzichtige externe bibliotheken (zoals pyotp of otplib) over om de volledige cryptografische RFC 6238-pijplijn vanaf nul op te bouwen in Python, JavaScript en Go, waarbij we elke implementatie verifiëren aan de hand van officiële IETF-testvectoren.
01. De elegantie van tijdgestuurde authenticatie
Om TOTP te begrijpen, moeten we eerst kijken naar zijn voorloper: RFC 4226 HOTP (HMAC-Based One-Time Password).
Bij HOTP delen de client en server een gebeurtenisenteller $C$. Telkens wanneer de gebruiker een token genereert, wordt de teller verhoogd ($C = C + 1$), en het token wordt afgeleid van $\text{HMAC-SHA-1}(K, C)$. Hoewel wiskundig solide, kende HOTP een operationele zwakte: tellersynchronisatieverlies. Als een gebruiker per ongeluk vijf keer op de knop van zijn hardwaretoken in zijn broekzak drukte, bereikte de clientteller $C=6$ terwijl de authenticatieserver op $C=1$ bleef staan, wat leidde tot aanhoudende weigeringen bij het inloggen.
Gepubliceerd in mei 2011 door N. M'Raihi, S. Machani, M. Pei en J. Rydell, loste RFC 6238 het synchronisatieprobleem op door het oplopende gehele getal te vervangen door de gestandaardiseerde Unix epoch-tijd. Omdat de fysieke tijd wereldwijd uniform voortschrijdt, verhogen zowel client als server hun interne teller gelijktijdig zonder dat er ook maar één netwerkpakket wordt verstuurd.
02. De 5 cryptografische stappen van TOTP
Het transformeren van een Base32-seed van 16 tekens (bijv. JBSWY3DPEHPK3PXP) naar een wisselende 6-cijferige code (bijv. 284915) omvat vijf nauwkeurige cryptografische fasen:
Aanmeldingssleutels van diensten worden gecodeerd in RFC 4648 Base32 met behulp van het alfabet A-Z en 2-7 (waarbij dubbelzinnige tekens zoals 0, 1, 8, 9 worden weggelaten). Het algoritme normaliseert eerst deze tekenreeks, verwijdert spaties en voegt opvulling (=) toe zodat de lengte een veelvoud van 8 is, voordat deze wordt gedecodeerd naar ruwe binaire sleutelbytes $K$.
Het huidige Unix-tijdstempel (verstreken seconden sinds 1 januari 1970 UTC) wordt toegewezen aan een geheel getal als teller $T$:
Met standaardparameters $T_0 = 0$ en stapduur $X = 30$ seconden. Dit gehele getal $T$ wordt vervolgens gecodeerd als een 8-byte big-endian 64-bit unsigned integer.
Met de ruwe sleutelbytes $K$ als geheime sleutel berekenen we de HMAC-SHA1-digest over de 8-byte big-endian tijdbuffer, wat resulteert in een 20-byte (160-bit) hash voor berichtauthenticatie $HS$:
Om 20 ruwe bytes om te zetten in een geheel getal, specificeert RFC 4226 dynamische truncatie:
- Offset extraheren: Inspecteer de laatste byte $HS[19]$ en maskeer met
0x0Fom de laagste 4 bits te extraheren, wat een offsetwaarde tussen 0 en 15 oplevert:Offset = HS[19] & 0x0F; - 4 opeenvolgende bytes selecteren: Extraheer 4 opeenvolgende bytes vanaf Offset: $P = HS[Offset \dots Offset+3]$;
- Tekenbit maskeren: Maskeer de meest significante bit (MSB) met
0x7Fom dubbelzinnigheid door signed integer overflow te voorkomen:
BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
Tot slot wordt BinaryCode gereduceerd modulo $10^6$ (1.000.000) voor standaard 6-cijferige tokens. Vul het resultaat links aan met nullen om een consistente string van 6 tekens te garanderen:
03. Werkende code 1: Python 3 Standard Library
Deze volledige implementatie maakt uitsluitend gebruik van de ingebouwde Python-modules base64, struct, hmac en hashlib, zonder externe pip-pakketten:
import base64 import hashlib import hmac import struct import timedef generate_totp(secret_base32: str, timestamp: int = None, step: int = 30, digits: int = 6) -> str: """ Genereer een RFC 6238-compatibel TOTP-token. :param secret_base32: Gedeelde geheime sleutel in Base32-formaat (bijv. ‘JBSWY3DPEHPK3PXP’) :param timestamp: Unix epoch-tijdstempel in seconden. Standaard time.time() :param step: Tijdstapvenster in seconden (standaard: 30) :param digits: Aantal uitvoercijfers (standaard: 6) :return: Links met nullen aangevulde OTP-string (bijv. ‘045812’) """ if timestamp is None: timestamp = int(time.time())
# 1. Base32-string normaliseren en '='-opvultekens toevoegen 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 tijdstapteller T berekenen time_counter = timestamp // step time_bytes = struct.pack(">Q", time_counter) # 8 bytes big-endian unsigned long long # 3. HMAC-SHA1-digest berekenen (20 bytes) hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest() # 4. Dynamische truncatie uitvoeren offset = hmac_digest[-1] & 0x0F # Laagste 4 bits van de 20e byte # 4 bytes bij offset uitpakken en MSB-tekenbit wissen binary_code = struct.unpack(">I", hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF # 5. Modulo-berekening en formattering met voorloopnullen otp_code = binary_code % (10 ** digits) return str(otp_code).zfill(digits)
if name == “main”: test_secret = “JBSWY3DPEHPK3PXP” print(f”Gegenereerde TOTP-code: {generate_totp(test_secret)}”)
04. Werkende code 2: JavaScript & Node.js Crypto
Pure JavaScript-implementatie voor Node.js backend runtimes met behulp van de native module crypto.createHmac:
const crypto = require('crypto');// RFC 4648 Base32-decoder in pure 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 buffer toewijzen in Big-Endian-formaat const buffer = Buffer.alloc(8); buffer.writeBigUInt64BE(BigInt(counter), 0);
// HMAC-SHA1 berekenen const hmac = crypto.createHmac(‘sha1’, key).update(buffer).digest();
// Dynamische truncatie 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(“Berekende TOTP:”, generateTOTP(“JBSWY3DPEHPK3PXP”));
Client-side browseruitvoering via de W3C Web Crypto API:
Voor moderne webapplicaties die 100% offline beveiliging nastreven (de exacte architectuur achter 2FA-Tool), biedt de W3C Web Crypto API asynchrone, hardwareversnelde cryptografische primitieven direct in de browser-sandbox zonder externe npm-pakketten:
async function generateBrowserTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000)) { const keyBytes = base32Decode(secretBase32); const counter = Math.floor(timestamp / 30);// 1. Ruwe geheime sleutel importeren in SubtleCrypto const cryptoKey = await window.crypto.subtle.importKey( “raw”, keyBytes, { name: “HMAC”, hash: { name: “SHA-1” } }, false, [“sign”] );
// 2. 8-byte Big-Endian tellersbuffer coderen const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, BigInt(counter), false); // false = Big-Endian
// 3. Cryptografische HMAC-ondertekening uitvoeren const signature = await window.crypto.subtle.sign(“HMAC”, cryptoKey, buffer); const hmacResult = new Uint8Array(signature);
// 4. Dynamische truncatie 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. Werkende code 3: Productie-klare Golang (Go)
Microservices met een hoge doorvoer profiteren van de beproefde standaardbibliotheek van Go:
package mainimport ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “math” “strings” “time” )
// GenerateTOTP berekent standaard RFC 6238 tijdgebaseerde tokens func GenerateTOTP(secretBase32 string, timestamp int64, step int64, digits int) (string, error) { // 1. Base32-seed opschonen en decoderen 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 tijdstapteller coderen counter := uint64(timestamp / step) buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, counter) // 3. HMAC-SHA1 berekenen mac := hmac.New(sha1.New, key) mac.Write(buf) hash := mac.Sum(nil) // 4. Dynamische truncatie offset := hash[len(hash)-1] & 0x0f binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff // 5. Modulo-berekening 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 berekende TOTP: %s\n”, code) }
06. RFC 6238 Appendix B testvectorverificatie
Om naleving van de standaard aan te tonen, moet elke cryptografische implementatie worden geverifieerd aan de hand van de officiële testvectoren die zijn gepubliceerd in IETF RFC 6238 Appendix B.
De officiële testsuite gebruikt het ASCII-geheim "12345678901234567890" (20 bytes), wat in Base32 codeert als:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (geëvalueerd voor 8-cijferige uitvoer met stappen van 30 seconden):
| Unix Epoch-tijdstempel | UTC Datum & Tijd | Officieel verwachte RFC-code | Resultaat van implementatie |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ GESLAAGD (Exacte overeenkomst) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ GESLAAGD (Exacte overeenkomst) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ GESLAAGD (Exacte overeenkomst) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ GESLAAGD (Exacte overeenkomst) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ GESLAAGD (Exacte overeenkomst) |
07. Productie-engineering: tijdsafwijking, replay & limieten
Bij het implementeren van TOTP-verificatie in zakelijke authenticatiesystemen met hoge beschikbaarheid moeten drie beveiligingseisen worden gehandhaafd:
Vanwege kleine klokafwijkingen op mobiele apparaten moeten servers het venster [T - 1, T, T + 1] evalueren (waarbij de voorgaande 30s, de huidige 30s en de volgende 30s worden gecontroleerd). Verruim het venster nooit verder dan $\pm 1$ stap, aangezien elke extra stap het aanvalsoppervlak voor replay-aanvallen met 30 seconden vergroot.
Een token moet direct na succesvol gebruik ongeldig worden gemaakt. Sla Last_Verified_Time_Step op in Redis of een in-memory cache. Als een client binnen hetzelfde venster van 30 seconden hetzelfde token opnieuw probeert in te dienen, weiger dan het verzoek om man-in-the-middle replay-aanvallen te neutraliseren.
Omdat 6-cijferige codes slechts 1.000.000 permutaties omvatten, kunnen onbeveiligde endpoints snel via brute-force worden aangevallen. Hanteer progressieve vertragingen en accountblokkades na 3 opeenvolgende mislukte pogingen. Voer in uw code vergelijkingen uit met constant-time functies (bijv. crypto/subtle.ConstantTimeCompare in Go of crypto.timingSafeEqual in Node.js) om timingaanvallen via side-channels te dwarsbomen.
08. Veelgestelde vragen (FAQ)
V1: Aangezien SHA-1 cryptografisch gebroken is, waarom gebruikt TOTP dan standaard nog steeds HMAC-SHA1? ▼
De theoretische botsingsaanvallen tegen ruwe SHA-1 breken de HMAC-constructie (RFC 2104) niet. In HMAC wordt de onderliggende hashfunctie genest met interne en externe geheime sleutels, wat botsingsaanvallen neutraliseert. Tot op heden blijft HMAC-SHA1 veilig voor 2FA. Bovendien ondersteunt RFC 6238 volledig HMAC-SHA256 en HMAC-SHA512 als een hoger veiligheidsniveau vereist is.
V2: Hebben tijdzones of zomertijd (DST) invloed op TOTP-synchronisatie? ▼
Nee. De Unix epoch-tijd is gedefinieerd als het aantal verstreken seconden sinds 1970-01-01 00:00:00 UTC. Het is strikt onafhankelijk van tijdzones, schrikkelseconden en zomertijdwisselingen. Op ieder exact moment in de tijd hebben een server in Tokio en een smartphone in Amsterdam exact hetzelfde gehele tijdstempel.
V3: Waarin verschilt Steam Guard 5-letterige authenticatie van RFC 6238? ▼
Steam Guard is een bedrijfseigen variant gebaseerd op RFC 6238. Het gebruikt exact dezelfde tijdstap van 30 seconden en HMAC-SHA1-berekening, maar in de uiteindelijke reductiefase koppelt het het geëxtraheerde gehele getal via een aangepaste array van 26 alfanumerieke tekens (23456789BCDFGHJKMNPQRTVWXY) om een code van 5 tekens te genereren.
Ervaar live 100% client-side TOTP-codegeneratie
2FA-Tool implementeert deze exacte cryptografische pijplijn rechtstreeks in uw browser met behulp van de W3C Web Crypto API. Geen serveroverdracht, geen tokenopslag, directe validatie.