💻 Anvendt kryptografi · Teknisk dybdegående guide

Sådan fungerer TOTP: Implementering af RFC 6238-algoritmen i Python, JavaScript og Go

2F 2FA-Tool sikkerhedsforskningslaboratorium · Opdateret oktober 2026 · 16 min læsning (2.700+ ord)
💡 Arkitektonisk abstrakt & teknisk manifest

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's gennembrud: Erstat hændelsestællere med Unix-tid

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:

1 Trin 1: Base32-afkodning af den hemmelige nøgle

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

2 Trin 2: Beregning af tidstrin (tæller $T$)

Det aktuelle Unix-tidsstempel (antal sekunder forløbet siden 1. januar 1970 UTC) oversættes til heltalstælleren $T$:

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

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.

3 Trin 3: HMAC-SHA1-hashberegning

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:

HS = HMAC-SHA-1(Nøgle, T)
4 Trin 4: Dynamisk trunkering (Dynamic Truncation, DT)

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 0x0F for 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 0x7F for at forhindre signed integer overflow-problemer:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Trin 5: Moduloreduktion til $N$ cifre

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:

OTP = BinaryCode mod 10^6

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:

totp_rfc6238.py Python 3.8+ (Ingen afhængigheder)
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: """ 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:

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

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

totp.go Go 1.20+ standardbibliotek
package main

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

Regel 1 Tolerancevindue for ur-drift (Clock Drift)

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.

Regel 2 Håndhævelse af éngangskoder (Anti-Replay)

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.

Regel 3 Ratebegrænsning mod brute-force og sammenligning i konstant tid

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.

RFC 6238-kompatibel · Klientberegning med W3C Web Crypto API

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.