Blandt grundlæggende kryptografiske protokoller i moderne websikkerhed skiller RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) sig ud med sin matematiske elegance og enkelhed. Den kræver ingen interaktive netværkshåndtryk, ingen centraliseret offentlig nøgleinfrastruktur (PKI) og ingen forespørgsler til eksterne cloudservere. Udelukkende ved hjælp af en delt hemmelig Base32-nøgle og den uafbrudte fremadskriden af fysisk Unix-tid beregner klienten og serveren identiske, kortlivede 6-cifrede godkendelseskoder hvert 30. sekund med 100 % matematisk sikkerhed.
I denne omfattende vejledning ser vi bort fra uigennemskuelige tredjepartsbiblioteker (som pyotp eller otplib) for at opbygge hele den kryptografiske RFC 6238-pipeline helt fra bunden i Python, JavaScript og Go – og vi verificerer hver eneste implementering mod officielle IETF-testvektorer.
01. Elegancen i tidsstyret godkendelse
For at forstå TOTP må vi først se på dens forgænger: RFC 4226 HOTP (HMAC-Based One-Time Password).
I HOTP deler klienten og serveren en hændelsestæller $C$. Hver gang brugeren genererer en engangskode, tælles tælleren op ($C = C + 1$), og koden udledes af $\text{HMAC-SHA-1}(K, C)$. Selvom HOTP er matematisk solid, har den en operationel svaghed: desynkronisering af tælleren. Hvis en bruger ved et uheld trykker fem gange på knappen på sin hardware-dongle i lommen, når klienttælleren $C=6$, mens godkendelsesserveren forbliver på $C=1$, hvilket resulterer i gentagne login-afvisninger.
RFC 6238 blev udgivet i maj 2011 af N. M'Raihi, S. Machani, M. Pei og J. Rydell og løste desynkroniseringsproblemet ved at erstatte den trinvis optællende heltalsvariabel med standardiseret Unix epoch-tid. Da den fysiske tid forløber ensartet over hele kloden, rykker både klient og server deres interne tællere frem synkront uden at sende en eneste netværkspakke.
02. TOTP's 5 kryptografiske matematiske trin
Omdannelsen af en 16-tegns Base32-nøgle (f.eks. JBSWY3DPEHPK3PXP) til en rullende 6-cifret kode (f.eks. 284915) involverer fem præcise kryptografiske faser:
Oprettelsesnøgler kodes i RFC 4648 Base32 ved hjælp af alfabetet A-Z og 2-7 (hvor tvetydige tegn som 0, 1, 8, 9 udelades). Algoritmen normaliserer først denne streng ved at fjerne mellemrum og tilføje udfyldningstegn (=), så dens længde er et multiplum af 8, før den afkodes til rå binære nøglebytes $K$.
Det aktuelle Unix-tidsstempel (antal sekunder forløbet siden 1. januar 1970 UTC) oversættes til heltalstælleren $T$:
Med standardparametrene $T_0 = 0$ og trinstørrelsen $X = 30$ sekunder. Dette heltal $T$ kodes derefter som et 8-byte big-endian 64-bit unsigned heltal.
Med de rå nøglebytes $K$ som hemmelig nøgle beregnes HMAC-SHA1-digestet over den 8-byte big-endian tidsbuffer, hvilket resulterer i et 20-byte (160-bit) hash-værdi $HS$ til meddelelsesgodkendelse:
For at konvertere de 20 rå bytes til et heltal specificerer RFC 4226 dynamisk trunkering:
- Uddrag offset: Inspicer den sidste byte $HS[19]$ og maskér med
0x0Ffor at udtrække dens laveste 4 bits, hvilket giver en offset-værdi mellem 0 og 15:Offset = HS[19] & 0x0F; - Uddrag 4 bytes: Udtræk 4 på hinanden følgende bytes startende ved Offset: $P = HS[Offset \dots Offset+3]$;
- Maskér fortegnsbit: Maskér den mest betydende bit (MSB) med
0x7Ffor at forhindre signed integer overflow-problemer:
BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
Til sidst reduceres BinaryCode modulo $10^6$ (1.000.000) for standardmæssige 6-cifrede koder. Resultatet foranstilles med nuller (left-pad), så der altid opnås en ensartet 6-tegns streng:
03. Kørbar kode 1: Python 3 standardbibliotek
Denne komplette implementering benytter Pythons indbyggede moduler base64, struct, hmac og hashlib helt uden eksterne pip-afhængigheder:
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: """ Generer en RFC 6238-kompatibel TOTP-kode. :param secret_base32: Delt hemmelig nøgle i Base32-format (f.eks. ‘JBSWY3DPEHPK3PXP’) :param timestamp: Unix epoch-tidsstempel i sekunder. Standard er time.time() :param step: Tidsvindue i sekunder (standard: 30) :param digits: Antal outputcifre (standard: 6) :return: Nul-udfyldt OTP-streng (f.eks. ‘045812’) """ if timestamp is None: timestamp = int(time.time())
# 1. Normaliser Base32-streng og tilføj '=' udfyldningstegn 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. Beregn 64-bit big-endian tidstrinstæller T time_counter = timestamp // step time_bytes = struct.pack(">Q", time_counter) # 8 bytes big-endian unsigned long long # 3. Beregn HMAC-SHA1 digest (20 bytes) hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest() # 4. Udfør dynamisk trunkering offset = hmac_digest[-1] & 0x0F # De laveste 4 bits af den 20. byte # Udpak 4 bytes ved offset og ryd MSB-fortegnsbit binary_code = struct.unpack(">I", hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF # 5. Modulo-aritmetik og formatering med foranstillede nuller otp_code = binary_code % (10 ** digits) return str(otp_code).zfill(digits)
if name == “main”: test_secret = “JBSWY3DPEHPK3PXP” print(f”Genereret TOTP-kode: {generate_totp(test_secret)}”)
04. Kørbar kode 2: JavaScript & Node.js Crypto
Ren JavaScript-implementering til Node.js backend-miljøer ved brug af det native crypto.createHmac-modul:
const crypto = require('crypto');// RFC 4648 Base32-afkoder i ren 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);
// Alloker 8-byte buffer i Big-Endian-format const buffer = Buffer.alloc(8); buffer.writeBigUInt64BE(BigInt(counter), 0);
// Beregn HMAC-SHA1 const hmac = crypto.createHmac(‘sha1’, key).update(buffer).digest();
// Dynamisk trunkering 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(“Beregnet TOTP:”, generateTOTP(“JBSWY3DPEHPK3PXP”));
Klientkørsel i browseren via W3C Web Crypto API:
Til moderne webapplikationer, der ønsker 100 % offline-sikkerhed (den præcise arkitektur, der driver 2FA-Tool), leverer W3C Web Crypto API asynkrone, hardwareaccelererede kryptografiske funktioner direkte i browserens sandbox uden tredjeparts npm-pakker:
async function generateBrowserTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000)) { const keyBytes = base32Decode(secretBase32); const counter = Math.floor(timestamp / 30);// 1. Importer den rå hemmelige nøgle til SubtleCrypto const cryptoKey = await window.crypto.subtle.importKey( “raw”, keyBytes, { name: “HMAC”, hash: { name: “SHA-1” } }, false, [“sign”] );
// 2. Kod 8-byte Big-Endian tællerbuffer const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, BigInt(counter), false); // false = Big-Endian
// 3. Udfør kryptografisk HMAC-signering const signature = await window.crypto.subtle.sign(“HMAC”, cryptoKey, buffer); const hmacResult = new Uint8Array(signature);
// 4. Dynamisk trunkering 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. Kørbar kode 3: Produktionsklar Golang (Go)
Højtydende mikrotjenester med høj gennemstrømning drager fordel af Go's gennemtestede standardbibliotek:
package mainimport ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “math” “strings” “time” )
// GenerateTOTP beregner standardmæssige RFC 6238 tidsbaserede koder func GenerateTOTP(secretBase32 string, timestamp int64, step int64, digits int) (string, error) { // 1. Rens Base32-nøglen og afkod 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. Kod 64-bit Big-Endian tidstrinstæller counter := uint64(timestamp / step) buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, counter) // 3. Beregn HMAC-SHA1 mac := hmac.New(sha1.New, key) mac.Write(buf) hash := mac.Sum(nil) // 4. Dynamisk trunkering offset := hash[len(hash)-1] & 0x0f binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff // 5. Modulo-aritmetik 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-beregnet TOTP: %s\n”, code) }
06. Verifikation mod officielle testvektorer i RFC 6238 bilag B
For at dokumentere standardoverholdelse skal enhver kryptografisk implementering verificeres mod de officielle testvektorer, der er offentliggjort i IETF RFC 6238 bilag B.
Den officielle testsuite anvender ASCII-hemmeligheden "12345678901234567890" (20 bytes), som kodes i Base32 som:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (evalueret for 8-cifrede koder med 30-sekunders trin):
| Unix Epoch-tidsstempel | UTC dato & tid | Forventet kode ifølge RFC | Implementeringsresultat |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ BESTÅET (Eksakt match) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ BESTÅET (Eksakt match) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ BESTÅET (Eksakt match) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ BESTÅET (Eksakt match) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ BESTÅET (Eksakt match) |
07. Produktionsteknik: Tidsforskydning, replay og hastighedsgrænser
Når TOTP-verifikation implementeres i virksomhedssystemer med høje krav til oppetid og sikkerhed, skal tre centrale sikkerhedsregler altid håndhæves:
På grund af mindre unøjagtigheder i mobile enheders ure bør servere evaluere tidsvinduet [T - 1, T, T + 1] (hvilket kontrollerer det foregående trin på 30 sekunder, det aktuelle trin og det efterfølgende trin). Udvid aldrig vinduet ud over $\pm 1$ trin, da hvert ekstra trin øger angrebsfladen for replay-angreb med 30 sekunder.
En kode skal straks gøres ugyldig efter vellykket validering. Gem Sidst_Verificerede_Tidstrin i Redis eller en intern hukommelses-cache. Hvis en klient forsøger at indsende den samme kode inden for det samme 30-sekunders vindue, skal anmodningen afvises for at neutralisere man-in-the-middle replay-angreb.
Da 6-cifrede koder kun rummer 1.000.000 mulige permutationer, kan ubeskyttede endepunkter hurtigt udsættes for brute-force-angreb. Håndhæv progressive forsinkelser og kontospærring efter 3 på hinanden følgende mislykkede forsøg. I koden skal strenge sammenlignes ved hjælp af funktioner i konstant tid (f.eks. crypto/subtle.ConstantTimeCompare i Go eller crypto.timingSafeEqual i Node.js) for at modvirke side-channel timing-angreb.
08. Ofte stillede spørgsmål (FAQ)
Q1: Når SHA-1 er kryptografisk brudt, hvorfor bruger TOTP så stadig HMAC-SHA1 som standard? ▼
De teoretiske kollisionsangreb mod rå SHA-1 bryder ikke HMAC-konstruktionen (RFC 2104). I HMAC indkapsles den underliggende hashfunktion i interne og eksterne hemmelige nøgler, hvilket neutraliserer kollisionsbaserede angreb. Til dato forbliver HMAC-SHA1 sikker til totrinsgodkendelse. Derudover understøtter RFC 6238 fuldt ud HMAC-SHA256 og HMAC-SHA512, hvis der stilles strengere krav til sikkerhedsniveauet.
Q2: Påvirker tidszoner eller sommertid (DST) TOTP-synkroniseringen? ▼
Nej. Unix epoch-tid er defineret som det forløbne antal sekunder siden 1970-01-01 00:00:00 UTC. Den er fuldstændig uafhængig af lokale tidszoner, skudsekunder og sommertidsomlægninger. På ethvert givet tidspunkt har en server i Tokyo og en smartphone i København nøjagtig samme heltalstidsstempel.
Q3: Hvordan adskiller Steam Guards 5-bogstavers godkendelse sig fra RFC 6238? ▼
Steam Guard er en proprietær variant baseret på RFC 6238. Den benytter præcis det samme 30-sekunders tidstrin og samme HMAC-SHA1-beregning, men i den afsluttende reduktionsfase mapper den det udtrukne heltal gennem et tilpasset alfanumerisk tegnsæt på 26 tegn (23456789BCDFGHJKMNPQRTVWXY) for at frembringe en 5-tegns streng.
Oplev 100 % klientbaseret TOTP-kodegenerering live
2FA-Tool implementerer netop denne kryptografiske pipeline direkte i din browser ved hjælp af W3C Web Crypto API. Ingen servertransmissioner, ingen lagring af koder, øjeblikkelig validering.