Hoe TOTP werkt: het algoritme achter je authenticator-codes
Een paar keer per week tik je een 6-cijferige code over uit een authenticator-app. Toch klopt die code precies met wat de server verwacht, ook al praten je telefoon en de server nooit met elkaar. Hoe werkt TOTP dan? Achter elk vers getal van 30 seconden zit een klein, deterministisch algoritme dat beide kanten los van elkaar uitvoeren op dezelfde gedeelde geheime sleutel. Er gaat geen code over het netwerk, en geen centrale server deelt het getal uit.
TOTP (Time-based One-Time Password), gedefinieerd in RFC 6238, zet een gedeelde geheime sleutel plus de huidige tijd om in een korte numerieke code: het berekent een HMAC over de tijd en kort het resultaat in. Tweefactorauthenticatie (2FA) draait erop dat beide kanten dezelfde waarde berekenen zonder die uit te wisselen, en dus rust het hele vertrouwensmodel op dat algoritme.
Deze gids loopt het algoritme van begin tot eind door met concrete getallen, en behandelt daarna de helft die de meeste uitleg overslaat: hoe een server een code echt verifieert, en wat 2FA wel en niet tegenhoudt. Je kunt een live code berekenen in onze TOTP-generator terwijl je leest.
Wat is TOTP nou eigenlijk?
TOTP (Time-based One-Time Password), gedefinieerd in RFC 6238, is een algoritme dat een gedeelde geheime sleutel combineert met de huidige tijd om een korte code te maken die op een vast interval roteert. Zowel de authenticator-app als de server bewaren dezelfde geheime sleutel, lezen dezelfde klok af en doen dezelfde rekensom, dus komen ze op dezelfde code uit zonder die ooit te versturen.
Dat laatste is het stuk dat de meeste mensen verrast. De code zelf gaat nooit over de lijn; bij de setup wordt alleen de geheime sleutel verstuurd, en daarna leidt elke kant de codes zelf af. Er valt niets te onderscheppen behalve de geheime sleutel bij de inschrijving en de 6 cijfers die de gebruiker bij het inloggen intikt. Drie invoeren vallen samen tot één uitvoer:
| Invoer | Rol | Typische waarde |
|---|---|---|
| Gedeelde geheime sleutel | De langlevende sleutel, één keer afgesproken bij inschrijving | JBSWY3DPEHPK3PXP (Base32) |
| Tijdstap | De teller die vooruit tikt | venster van 30 seconden |
| Uitvoer | De korte code afgeleid uit de twee | 324550 |
De geheime sleutel staat vrijwel altijd in Base32 (de letters A–Z en de cijfers 2–7), omdat dat alfabet hoofdletterongevoelig is en niet sneuvelt bij afdrukken, overtikken of inpakken in een QR-code. Je schrijft een geheime sleutel in door een otpauth://-URI te scannen, die je kunt tonen als een authenticator-QR, of door de Base32-string met de hand in te tikken.
TOTP vs HOTP vs SMS vs Passkeys: het 2FA-landschap
TOTP is één van de opties, en om goed te kiezen moet je het hele veld overzien. De relatie die je in één zin moet onthouden: TOTP is gewoon HOTP, waarbij de teller is vervangen door het aantal tijdstappen sinds het Unix-epoch. Al het andere is een afweging tussen phishingbestendigheid, gemak en welke infrastructuur je nodig hebt.
| Mechanisme | Aandrijving | Levensduur code | Phishingbestendig? | Netwerk nodig? | Typisch gebruik |
|---|---|---|---|---|---|
| HOTP (RFC 4226) | Oplopende teller | Tot gebruikt | Nee | Nee | Hardwaretokens, legacy |
| TOTP (RFC 6238) | Huidige tijd | ~30 seconden | Nee | Nee (na inschrijving) | Authenticator-apps |
| SMS OTP | Server stuurt een code | Een paar minuten | Nee | Ja (mobiel netwerk) | Consumententerugval |
| Push-goedkeuring | Servervraag aan een apparaat | Per verzoek | Gedeeltelijk | Ja | App-gebaseerde 2FA |
| Passkey / FIDO2 | Publiekesleutel-challenge | Per verzoek | Ja (origin-gebonden) | Ja | Moderne accounts |
Let op het patroon in die tabel. TOTP en HOTP draaien offline zodra ze zijn ingeschreven, wat ze robuust en privé maakt, maar geen van beide is op zichzelf phishingbestendig: een overtuigende neppagina kan om de code vragen en die doorgeven. SMS voegt een netwerkkanaal toe, met een eigen aanvalsoppervlak erbij. Passkeys dichten het phishinggat door de credential te binden aan de origin van de site, en daar gaat de branche dan ook naartoe. TOTP blijft populair omdat het sterk is, overal beschikbaar en gratis.
Hoe het TOTP-algoritme werkt, stap voor stap
Het hele algoritme bestaat uit vier stappen. We rekenen elke stap voor met de RFC-testsleutel JBSWY3DPEHPK3PXP en een vaste Unix-tijd van 1700000000, zodat je elk getal zelf kunt nalopen.
- Decodeer de Base32-geheime sleutel naar ruwe sleutelbytes.
- Bereken de tijdstapteller uit de huidige Unix-tijd.
- Bereken een HMAC over de teller met de geheime sleutel.
- Kort de digest in tot een 6-cijferige code.
Stap 1 — Decodeer de Base32-geheime sleutel naar bytes
Base32 stopt 5 bits in elk teken, en de decoder hergroepeert die tekens tot bytes van 8 bits. De geheime sleutel JBSWY3DPEHPK3PXP decodeert naar de 10 ruwe bytes 48 65 6c 6c 6f 21 de ad be ef. Die byte-array is de HMAC-sleutel, niet de afdrukbare string.
Stap 2 — Bereken de tijdstapteller
De teller is het aantal volledige tijdstappen sinds een startpunt: T = floor((unixTime − T0) / period). De RFC-standaardwaarden zijn T0 = 0 (het Unix-epoch) en period = 30. Met unixTime = 1700000000 krijg je T = floor(1700000000 / 30) = 56666666. Dat gehele getal codeer je vervolgens als een 8-byte big-endian-waarde: 00 00 00 00 03 60 aa 2a. De teller verandert pas zodra een nieuw venster van 30 seconden begint, dus blijft elke code een heel venster lang gelijk en springt daarna.
Stap 3 — Bereken de HMAC over de teller met de geheime sleutel
Het algoritme draait HMAC-SHA1 over de 8-byte teller, met de geheime-sleutelbytes als sleutel. HMAC is een gesleutelde eenrichtingsfunctie: zonder de geheime sleutel kun je de digest niet terugrekenen of een geldige vervalsen, en daarom is de code onvervalsbaar. Voor onze invoer is de digest de 20 bytes 1d 70 6e 94 1a c7 6b 6d 4a 46 dd 6f af a4 5f e3 35 11 bf 86.
Stap 4 — Dynamische inkorting tot een 6-cijferige code (RFC 4226)
Een digest van 20 bytes is te lang om in te tikken, dus de dynamische inkorting van RFC 4226 trekt er een getal uit. De offset is de lage nibble van de laatste byte: de laatste byte is 0x86, de lage nibble daarvan is 6, dus de offset is 6. Lees 4 bytes vanaf die offset (6b 6d 4a 46), maskeer de bovenste bit van de eerste weg om het getal positief te houden, en je krijgt het gehele getal 1802324550. Reduceer het modulo 10^6 en vul aan met voorloopnullen: 1802324550 % 1000000 = 324550. Dat is de code die je app op dit moment voor deze geheime sleutel toont.
Hieronder staat het algoritme in JavaScript met de native Web Crypto API van de browser, zonder afhankelijkheden. Elke comment koppelt een blok aan een van de vier stappen hierboven:
// TOTP per RFC 6238 — SHA-1, 6 cijfers, periode van 30s (de standaardwaarden).
async function generateTotp(base32Secret, unixTime = Date.now() / 1000) {
// Stap 1: decodeer de Base32-geheime sleutel (A-Z, 2-7) naar ruwe sleutelbytes.
const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = '';
for (const ch of base32Secret.replace(/=+$/, '').toUpperCase()) {
bits += alpha.indexOf(ch).toString(2).padStart(5, '0');
}
const keyBytes = new Uint8Array(
bits.match(/.{8}/g).map((b) => parseInt(b, 2)));
// Stap 2: teller = aantal stappen van 30s sinds het epoch (8-byte big-endian).
let counter = Math.floor(unixTime / 30);
const msg = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
msg[i] = counter & 0xff;
counter = Math.floor(counter / 256);
}
// Stap 3: bereken HMAC-SHA1 over de teller met de geheime sleutel.
const key = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
const hmac = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg));
// Stap 4: dynamische inkorting (RFC 4226) -> 6-cijferige code.
const offset = hmac[hmac.length - 1] & 0x0f;
const binary = ((hmac[offset] & 0x7f) << 24) | (hmac[offset + 1] << 16) |
(hmac[offset + 2] << 8) | hmac[offset + 3];
return (binary % 1_000_000).toString().padStart(6, '0');
}
const code = await generateTotp('JBSWY3DPEHPK3PXP', 1700000000);
console.log(code); // -> "324550"
Hetzelfde algoritme in Python, alleen met de standaardbibliotheek (hmac en struct):
import base64, hmac, hashlib, struct, time
def totp(secret, for_time=None, period=30, digits=6, digest='sha1'):
if for_time is None:
for_time = time.time()
# Stap 1: Base32-decodeer de geheime sleutel naar ruwe sleutelbytes.
key = base64.b32decode(secret.upper())
# Stap 2: teller = aantal tijdstappen sinds het epoch (8-byte big-endian).
counter = int(for_time // period)
msg = struct.pack(">Q", counter)
# Stap 3: bereken HMAC over de teller met de geheime sleutel.
h = hmac.new(key, msg, digest).digest()
# Stap 4: dynamische inkorting (RFC 4226) -> N-cijferige code.
offset = h[-1] & 0x0f
binary = ((h[offset] & 0x7f) << 24 |
(h[offset + 1] & 0xff) << 16 |
(h[offset + 2] & 0xff) << 8 |
(h[offset + 3] & 0xff))
return str(binary % (10 ** digits)).zfill(digits)
print(totp('JBSWY3DPEHPK3PXP', 1700000000)) # -> 324550
Beide implementaties printen 324550 voor onze vaste tijd, en beide geven de officiële RFC 6238-testvectoren terug (de SHA-1-vector bij T = 59 levert bijvoorbeeld 94287082 op). Vervang je SHA-1 door SHA-256 of SHA-512, of wijzig je het aantal cijfers, dan moet de verificateur aan de andere kant precies dezelfde keuzes maken, anders matchen de codes nooit.
Een TOTP-code aan de serverkant verifiëren
Een code genereren is maar de helft van het systeem. De andere helft is dat de server beslist of hij de 6 cijfers accepteert die een gebruiker zojuist heeft ingetikt, en in die stap zitten alle beveiligingsgevoelige afwegingen.
De server slaat geen codes op. Hij bewaart de geheime sleutel, en bij het inloggen herberekent hij de verwachte code uit die sleutel en de huidige tijd, en vergelijkt dan. De lastigheid is klokafwijking: het apparaat van de gebruiker en de server zijn het zelden op de seconde eens, dus een strikte gelijkheidscontrole zou codes vlak bij een venstergrens afwijzen. De oplossing is een klein validatievenster. Accepteer de huidige stap plus één stap aan weerszijden, oftewel: controleer de codes voor de tellers T−1, T en T+1. Een breder venster verdraagt meer afwijking, maar vergroot ook het raadoppervlak, dus venster 1 (een tolerantie van ±30 seconden) is de gangbare balans. Dezelfde ±1-stap-tolerantie zie je terug op het tabblad Verifiëren van de tool.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyTotp(secret, code, { window = 1, period = 30, digits = 6 } = {}) {
const counter = Math.floor(Date.now() / 1000 / period);
const submitted = Buffer.from(code);
// Controleer de huidige stap en ±window stappen voor klokafwijking.
for (let i = -window; i <= window; i++) {
const expected = Buffer.from(totpAt(secret, counter + i, digits));
// Vergelijking in constante tijd zodat timing geen gedeeltelijke match lekt.
if (expected.length === submitted.length &&
timingSafeEqual(expected, submitted)) {
return counter + i; // gematchte stap — sla op om replay te blokkeren
}
}
return false;
}
Twee extra details maken hier het verschil tussen “werkt” en “veilig”. Het eerste is replaypreventie: sla per gebruiker de laatste teller op die je hebt geaccepteerd en wijs elke code af die hoort bij een stap op of onder die teller, zodat een eenmaal afgeluisterde code niet binnen hetzelfde venster nog eens werkt. Daarom geeft verifyTotp de gematchte stap terug in plaats van een kale true. Het tweede is snelheidsbeperking: een 6-cijferige code is één van een miljoen waarden, en een ±1-venster maakt er op elk moment drie geldig, dus zonder rem kan een aanvaller die ruimte brute-forcen. Vergrendel het account of bouw backoff in na een handvol mislukte pogingen. En de geheime sleutel is een langlevende sleutel, dus versleutel die in rust, houd die uit versiebeheer en behandel die net als een wachtwoord. Genereer er meteen sterke herstelcodes bij, voor de dag dat een apparaat zoekraakt.
Waartegen TOTP beschermt — en waartegen niet
TOTP is een echte verbetering ten opzichte van alleen een wachtwoord, maar het is geen toverij, en marketingpagina’s strijken de gaten graag glad. Hier is de eerlijke verdeling.
| TOTP houdt tegen | TOTP houdt NIET tegen |
|---|---|
| Gelekte of hergebruikte wachtwoorden | Realtime phishing / adversary-in-the-middle |
| Credential stuffing | Malware die de geheime sleutel van een apparaat afleest |
| Brute force op wachtwoord op afstand | Zwakke accountherstel-flows die 2FA overslaan |
| Een DB-lek dat alleen wachtwoord-hashes blootlegt | (hiervoor zijn andere verdedigingen nodig) |
De winst is fors. Inloggen vereist nu een code die alleen de geheime sleutel kan opleveren, dus een gelekt wachtwoord is niet meer genoeg, en dat haalt de wind uit credential stuffing en brute force op afstand. Lekt je database maar zijn de TOTP-geheime sleutels in rust versleuteld, dan kan de aanvaller alsnog geen codes maken.
De gaten zijn net zo reëel. Een realtime phishingproxy (een adversary-in-the-middle-pagina) kan de gebruiker een perfecte replica voorschotelen, de live code afvangen en die binnen hetzelfde venster doorspelen naar de echte site. TOTP kan niet zien dat de code op de verkeerde plek is ingetikt. Malware op het apparaat die de geheime sleutel naar buiten smokkelt verslaat het helemaal, en een slordige “ik ben mijn 2FA vergeten”-flow kan het volledig omzeilen. Eén misverstand is het rechtzetten waard: SIM-swap-aanvallen verslaan eenmalige SMS-codes, niet TOTP. TOTP heeft geen telefoonnummerkanaal, dus er is niets dat een aanvaller kan omleiden.
Waar ga je daarna heen? Passkeys en FIDO2/WebAuthn zijn origin-gebonden en daardoor phishingbestendig van constructie: de credential weigert gewoon te authenticeren tegen het verkeerde domein. Zie TOTP als een sterke, overal beschikbare stap omhoog vanaf wachtwoorden, niet als eindstation. Het sluit prima aan op de rest van je auth-stack: lees aanbevolen aanpak voor JWT-beveiliging voor de session-token-laag boven op een geverifieerde login, en wachtwoorden hashen (bcrypt vs Argon2) voor de wachtwoord-in-rust-laag die 2FA aanvult.
Veelgemaakte valkuilen bij het implementeren van TOTP
De meeste TOTP-bugs zitten niet in het algoritme, want dat ligt vast in de RFC, maar in de bedrading eromheen. Dit zijn de bugs waar implementeerders op stuklopen.
- Klokafwijking van de server. Draait de server geen NTP, dan loopt zijn idee van “nu” weg van het apparaat van de gebruiker en matchen de codes voor iedereen niet meer. Zet netwerktijdsynchronisatie aan op elke host.
- Geheime sleutels in platte tekst of in versiebeheer. Een geheime sleutel in een configuratiebestand dat in git zit, is een permanente achterdeur. Bewaar die versleuteld in een secrets manager, nooit in versiebeheer.
- Geen replaybescherming. Accepteer je een code zonder de bijbehorende stap vast te leggen, dan werkt diezelfde code binnen zijn venster nog een keer. Bewaar de laatst gebruikte stap per gebruiker en weiger hergebruik.
- Een venster dat te breed of te smal is. Te breed verveelvoudigt het aantal raadbare codes en verzwakt de beveiliging; te smal wijst legitieme gebruikers af bij de minste afwijking. Venster 1 is de gebruikelijke balans.
- Parametermismatch. Codeert de inschrijving SHA-256 en 8 cijfers in de
otpauth://-URI, maar gaat de verificateur uit van SHA-1 en 6 cijfers, dan valideert geen enkele code ooit. Lees het algoritme, het aantal cijfers en de periode uit de URI en gebruik die aan beide kanten. - Geen back-up- of herstelcodes. Raakt een telefoon kwijt, dan is een herstelpad de enige weg terug. Geef herstelcodes mee bij de setup en maak ze zo sterk als het account verdient; dezelfde logica achter wachtwoord-entropie geldt ook voor herstelgeheimen.
FAQ
Is TOTP phishingbestendig?
Nee. TOTP houdt gelekte wachtwoorden en brute force op afstand tegen, maar een realtime phishingproxy kan een nep-login tonen, de live code afvangen en die binnen hetzelfde venster van 30 seconden doorspelen naar de echte site. Passkeys en FIDO2 zijn de phishingbestendige upgrade, want zij binden de credential aan de origin van de site.
Is TOTP veiliger dan SMS-2FA?
Ja. SMS-codes reizen over het mobiele netwerk, zijn te onderscheppen via SIM-swap- of SS7-aanvallen en leunen op de beveiliging van je provider. TOTP heeft geen telefoonnummerkanaal en verstuurt de code nooit, dus er valt onderweg niets te onderscheppen. De geheime sleutel wisselt maar één keer van hand, bij de setup.
Wat gebeurt er als ik mijn telefoon of authenticator-app kwijtraak?
Je hebt een back-up nodig die je vooraf hebt geregeld. Dat kunnen herstelcodes zijn die je opslaat bij het instellen van 2FA, een tweede apparaat dat met dezelfde geheime sleutel is ingeschreven, of de oorspronkelijke Base32-geheime sleutel die je ergens veilig bewaart. Heb je niets daarvan, dan betekent een verloren apparaat dat je buiten je account staat.
Hoe verifieert een server een TOTP-code?
Hij herberekent de verwachte code uit de gedeelde geheime sleutel en de huidige tijd, en houdt de ingediende code naast de huidige tijdstap plus één stap aan weerszijden om klokafwijking op te vangen. Hij legt ook vast welke stap matchte, zodat dezelfde code niet kan worden herhaald, en knijpt het aantal pogingen af om raden te blokkeren.
Waarom verversen TOTP-codes elke 30 seconden?
Dertig seconden is de standaardperiode uit RFC 6238: lang genoeg om de code rustig af te lezen en in te tikken, kort genoeg dat een code die een aanvaller afvangt vrijwel meteen verloopt. Sommige systemen gebruiken 60 seconden; die periode staat in de otpauth://-URI zodat de verificateur erop afstemt.
Kunnen twee apparaten één TOTP-geheime sleutel delen?
Ja. Elk apparaat met dezelfde Base32-geheime sleutel en een gelijklopende klok genereert identieke codes, omdat het algoritme deterministisch is. Zo werken multi-device-back-ups van authenticators, en precies daarom moet de geheime sleutel privé blijven: wie hem kopieert, kan elke toekomstige code maken.
Is TOTP hetzelfde als Google Authenticator?
Nee. TOTP is het open algoritme uit RFC 6238. Google Authenticator, Authy en 1Password zijn apps die het implementeren. Omdat de standaard gedeeld is, werkt elke conforme app met elke dienst die TOTP gebruikt, zonder vendor lock-in.
Conclusie
De kernideeën zijn kort genoeg om te onthouden:
- TOTP zet een gedeelde geheime sleutel plus de huidige tijd om in een code via HMAC en inkorting.
- Beide kanten berekenen de code los van elkaar; die gaat nooit over het netwerk.
- Verifieer met een ±1-stap-venster, plus replaybescherming en snelheidsbeperking.
- Het houdt wachtwoordaanvallen tegen, maar geen realtime phishing; dat gat dichten passkeys.
- Houd serverklokken gelijk met NTP en de geheime sleutel versleuteld en privé.
Wil je het algoritme echte getallen zien opleveren en je eigen verificatievenster uitproberen? Open de TOTP / 2FA-generator om codes te berekenen en te verifiëren, volledig in je browser, waarbij de geheime sleutel je apparaat nooit verlaat.