💻 Toegepaste cryptografie · Technische deep dive

Hoe TOTP werkt: implementatie van het RFC 6238-algoritme in Python, JavaScript & Go

2F 2FA-Tool beveiligingsonderzoekslab · Bijgewerkt: oktober 2026 · 16 min leestijd (2.700+ woorden)
💡 Architectonische samenvatting & technisch manifest

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.

⏱️ De doorbraak van RFC 6238: gebeurtenisentellers vervangen door Epoch-tijd

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:

1 Stap 1: Base32-geheime sleutel decoderen

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$.

2 Stap 2: Berekening van de tijdstap (teller $T$)

Het huidige Unix-tijdstempel (verstreken seconden sinds 1 januari 1970 UTC) wordt toegewezen aan een geheel getal als teller $T$:

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

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.

3 Stap 3: HMAC-SHA1-hashberekening

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$:

HS = HMAC-SHA-1(Key, T)
4 Stap 4: Dynamische truncatie (Dynamic Truncation, DT)

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 0x0F om 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 0x7F om dubbelzinnigheid door signed integer overflow te voorkomen:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Stap 5: Modulo-reductie naar $N$ cijfers

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:

OTP = BinaryCode mod 10^6

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:

totp_rfc6238.py Python 3.8+ (Geen externe afhankelijkheden)
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: """ 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:

totp_node.js Node.js Native Crypto
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:

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. 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:

totp.go Go 1.20+ Standard Library
package main

import ( “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:

Regel 1 Tolerantievenster voor tijdsafwijking (Clock Drift)

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.

Regel 2 Strikt eenmalig gebruik van tokens (Anti-Replay)

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.

Regel 3 Snelheidsbeperking tegen brute-force & vergelijking in constante tijd

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.

Compatibel met RFC 6238 · Client-side berekening via W3C Web Crypto API

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.