💻 Anvendt kryptografi · Teknisk dypdykk

Slik fungerer TOTP: Implementering av RFC 6238-algoritmen i Python, JavaScript og Go

2F 2FA-Tool Sikkerhetsforskningslab · Oppdatert oktober 2026 · 16 min lesing (2 700+ ord)
💡 Arkitektonisk sammendrag og ingeniørmanifest

Blant grunnleggende kryptografiske protokoller i moderne nettsikkerhet utmerker RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) seg med sin matematiske enkelhet. Den krever ingen interaktive nettverkshåndtrykk, ingen sentralisert offentlig nøkkelinfrastruktur (PKI) og ingen skysynkronisering. Kun med en delt Base32-hemmelighet og den jevne fremdriften av fysisk Unix-tid, beregner klient og server identiske, kortlivede 6-sifrede autentiseringstokener hvert 30. sekund med 100 % matematisk sikkerhet.

I denne omfattende veiledningen hopper vi over ugjennomsiktige tredjepartsbiblioteker (som pyotp eller otplib) for å bygge hele RFC 6238-kryptorørledningen fra bunnen av i Python, JavaScript og Go, og verifiserer hver implementering mot offisielle IETF-testvektorer.

01. Elegansen i tidsdrevet autentisering

For å forstå TOTP må vi først se på forgjengeren: RFC 4226 HOTP (HMAC-Based One-Time Password).

I HOTP deler klienten og serveren en hendelsesteller $C$. Hver gang brukeren genererer en kode, økes telleren ($C = C + 1$), og koden utledes fra $\text{HMAC-SHA-1}(K, C)$. Selv om den var matematisk solid, hadde HOTP en operativ svakhet: tellerdesynkronisering. Hvis en bruker utilsiktet trykket på knappen på maskinvarebrikken fem ganger i lommen, nådde klienttelleren $C=6$ mens autentiseringsserveren sto igjen på $C=1$, noe som førte til vedvarende avviste pålogginger.

⏱️ RFC 6238s gjennombrudd: Erstatte hendelsestellere med Unix-tid

RFC 6238 ble publisert i mai 2011 av N. M'Raihi, S. Machani, M. Pei og J. Rydell, og løste problemet med tellerdesynkronisering ved å erstatte det inkrementerende heltallet med standardisert Unix-tid (Unix epoch time). Fordi fysisk tid går likt over hele kloden, oppdaterer både klient og server sine interne tellere samtidig uten å sende en eneste nettverkspakke.

02. De 5 kryptografiske matematiske trinnene i TOTP

Å transformere en 16-tegns Base32-frøverdi (f.eks. JBSWY3DPEHPK3PXP) til en 6-sifret rullerende kode (f.eks. 284915) involverer fem presise kryptografiske faser:

1 Trinn 1: Base32-dekoding av hemmelig nøkkel

Tjenestenes oppsettsnøkler kodes i RFC 4648 Base32 ved hjelp av alfabetet A-Z og 2-7 (som utelater tvetydige tegn som 0, 1, 8, 9). Algoritmen normaliserer først denne strengen, fjerner mellomrom og legger til utfylling (=) slik at lengden er et multiplum av 8, før den dekodes til rå binære nøkkelbyte $K$.

2 Trinn 2: Beregning av tidstrinn (teller $T$)

Gjeldende Unix-tidsstempel (antall sekunder siden 1. januar 1970 UTC) tilordnes til et heltall $T$:

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

Med standardparametrene $T_0 = 0$ og trinnlengde $X = 30$ sekunder. Dette heltallet $T$ kodes deretter som et 8-byte Big-Endian 64-bit usignert heltall.

3 Trinn 3: HMAC-SHA1-hashberegning

Ved å bruke de rå nøkkelbytene $K$ som hemmelig nøkkel, beregner vi HMAC-SHA1-oppsummeringen over den 8-byte Big-Endian tidsbufferen, noe som gir en 20-byte (160-bit) meldingsautentiseringshash $HS$:

HS = HMAC-SHA-1(Key, T)
4 Trinn 4: Dynamisk avkorting (DT)

For å konvertere 20 rå byte til et heltall spesifiserer RFC 4226 dynamisk avkorting:

  • Hent forskyvning (offset): Inspiser den siste byten $HS[19]$ og maskér med 0x0F for å hente ut de laveste 4 bitene, noe som gir en offsetverdi mellom 0 og 15: Offset = HS[19] & 0x0F;
  • Kutt ut 4 byte: Trekk ut 4 sammenhengende byte fra og med Offset: $P = HS[Offset \dots Offset+3]$;
  • Maskér fortegnsbit: Maskér den mest signifikante biten (MSB) med 0x7F for å forhindre tvetydighet ved heltallsoverflyt:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Trinn 5: Moduloreduksjon til $N$ sifre

Til slutt reduseres BinaryCode modulo $10^6$ (1 000 000) for standard 6-sifrede koder. Fyll ut resultatet med ledende nuller til venstre for å sikre en konsistent streng på 6 tegn:

OTP = BinaryCode mod 10^6

03. Kjørbar kode 1: Python 3 standardbibliotek

Denne komplette implementeringen bruker Pythons innebygde moduler base64, struct, hmac og hashlib, helt uten eksterne pip-avhengigheter:

totp_rfc6238.py Python 3.8+ (Zero Dependencies)
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: """ Generate an RFC 6238 compliant TOTP token. :param secret_base32: Shared secret key in Base32 format (e.g., ‘JBSWY3DPEHPK3PXP’) :param timestamp: Unix epoch timestamp in seconds. Defaults to time.time() :param step: Time-step window in seconds (default: 30) :param digits: Number of output digits (default: 6) :return: Left-padded OTP string (e.g., ‘045812’) """ if timestamp is None: timestamp = int(time.time())

# 1. Normalize Base32 string and append '=' padding characters
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. Compute 64-bit big-endian time-step counter T
time_counter = timestamp // step
time_bytes = struct.pack(">Q", time_counter)  # 8 bytes big-endian unsigned long long

# 3. Calculate HMAC-SHA1 digest (20 bytes)
hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest()

# 4. Perform Dynamic Truncation
offset = hmac_digest[-1] & 0x0F  # Lower 4 bits of the 20th byte
# Unpack 4 bytes at offset and clear MSB sign bit
binary_code = struct.unpack(">I", hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF

# 5. Modulo arithmetic and zero-fill formatting
otp_code = binary_code % (10 ** digits)
return str(otp_code).zfill(digits)

if name == “main”: test_secret = “JBSWY3DPEHPK3PXP” print(f”Generated TOTP Code: {generate_totp(test_secret)}”)

04. Kjørbar kode 2: JavaScript og Node.js Crypto

Ren JavaScript-implementering for Node.js-kjøretidsmiljøer ved hjelp av native 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 &lt; cleaned.length; i++) {
    const val = alphabet.indexOf(cleaned[i]);
    if (val === -1) continue;
    value = (value &lt;&lt; 5) | val;
    bits += 5;
    if (bits &gt;= 8) {
        output[index++] = (value &gt;&gt;&gt; (bits - 8)) &amp; 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);

// Allocate 8-byte buffer in Big-Endian format
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64BE(BigInt(counter), 0);

// Compute HMAC-SHA1
const hmac = crypto.createHmac('sha1', key).update(buffer).digest();

// Dynamic Truncation
const offset = hmac[hmac.length - 1] &amp; 0x0f;
const binary = ((hmac[offset] &amp; 0x7f) &lt;&lt; 24) |
               ((hmac[offset + 1] &amp; 0xff) &lt;&lt; 16) |
               ((hmac[offset + 2] &amp; 0xff) &lt;&lt; 8) |
               (hmac[offset + 3] &amp; 0xff);

const otp = binary % (10 ** digits);
return otp.toString().padStart(digits, '0');

}

console.log(“Calculated TOTP:”, generateTOTP(“JBSWY3DPEHPK3PXP”));

Klientside-kjøring i nettleser via W3C Web Crypto API:

For moderne webapplikasjoner som krever 100 % offline-sikkerhet (den eksakte arkitekturen som driver 2FA-Tool), gir W3C Web Crypto API asynkrone, maskinvareakselererte kryptografiske primitiver direkte i nettlesersandkassen uten 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. Import raw secret key into SubtleCrypto
const cryptoKey = await window.crypto.subtle.importKey(
    "raw",
    keyBytes,
    { name: "HMAC", hash: { name: "SHA-1" } },
    false,
    ["sign"]
);

// 2. Encode 8-byte Big-Endian counter buffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setBigUint64(0, BigInt(counter), false); // false = Big-Endian

// 3. Perform cryptographic HMAC sign
const signature = await window.crypto.subtle.sign("HMAC", cryptoKey, buffer);
const hmacResult = new Uint8Array(signature);

// 4. Dynamic Truncation
const offset = hmacResult[hmacResult.length - 1] &amp; 0x0f;
const binary = ((hmacResult[offset] &amp; 0x7f) &lt;&lt; 24) |
               ((hmacResult[offset + 1] &amp; 0xff) &lt;&lt; 16) |
               ((hmacResult[offset + 2] &amp; 0xff) &lt;&lt; 8) |
               (hmacResult[offset + 3] &amp; 0xff);

const otp = binary % 1000000;
return otp.toString().padStart(6, '0');

}

05. Kjørbar kode 3: Produksjonsklar Golang (Go)

Mikrotjenester med høy gjennomstrømning drar stor nytte av Gos gjennomtestede standardbibliotek:

totp.go Go 1.20+ Standard Library
package main

import ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “math” “strings” “time” )

// GenerateTOTP computes standard RFC 6238 time-based tokens func GenerateTOTP(secretBase32 string, timestamp int64, step int64, digits int) (string, error) { // 1. Sanitize Base32 seed and decode 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. Encode 64-bit Big-Endian time-step counter
counter := uint64(timestamp / step)
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, counter)

// 3. Compute HMAC-SHA1
mac := hmac.New(sha1.New, key)
mac.Write(buf)
hash := mac.Sum(nil)

// 4. Dynamic Truncation
offset := hash[len(hash)-1] &amp; 0x0f
binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) &amp; 0x7fffffff

// 5. Modulo arithmetic
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 Computed TOTP: %s\n”, code) }

06. Verifisering mot RFC 6238 Tillegg B testvektorer

For å bevise standardoverholdelse må enhver kryptografisk implementering verifiseres mot de offisielle testvektorene publisert i IETF RFC 6238 Tillegg B.

Den offisielle testpakken bruker ASCII-hemmeligheten "12345678901234567890" (20 byte), som kodes i Base32 som:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (evaluert for 8-sifrede utdata med 30-sekunders trinn):

Unix-tidsstempel UTC dato og klokkeslett RFC offisiell forventet kode Implementeringsresultat
59 1970-01-01 00:00:59 94287082 ✓ BESTÅTT (Nøyaktig samsvar)
1111111109 2005-03-18 01:58:29 07081804 ✓ BESTÅTT (Nøyaktig samsvar)
1111111111 2005-03-18 01:58:31 14050471 ✓ BESTÅTT (Nøyaktig samsvar)
1234567890 2009-02-13 23:31:30 89005924 ✓ BESTÅTT (Nøyaktig samsvar)
2000000000 2033-05-18 03:33:20 69279037 ✓ BESTÅTT (Nøyaktig samsvar)

07. Produksjonsutvikling: Tidsavvik, gjenbruk og grenser

Ved implementering av TOTP-verifisering i bedriftsmiljøer med høye krav til oppetid, må tre sikkerhetskrav håndheves:

Regel 1 Toleransevindu for klokkeavvik

På grunn av mindre klokkeavvik på mobilenheter bør servere evaluere vinduet [T - 1, T, T + 1] (sjekke de foregående 30 s, gjeldende 30 s og neste 30 s). Utvid aldri vinduet utover $\pm 1$ trinn, da hvert ekstra trinn utvider angrepsflaten for gjenbruksangrep med 30 sekunder.

Regel 2 Håndheving av engangskode (anti-gjenbruk)

Et token må gjøres ugyldig umiddelbart etter vellykket verifisering. Lagre Last_Verified_Time_Step i Redis eller et hurtiglager. Hvis en klient prøver å sende inn samme token innenfor samme 30-sekundersvindu, avvises forespørselen for å nøytralisere Man-in-the-Middle-gjenbruksangrep.

Regel 3 Hastighetsbegrensning mot brute-force og sammenligning i konstant tid

Fordi 6-sifrede koder kun dekker 1 000 000 permutasjoner, kan ubeskyttede endepunkter raskt angripes med brute-force. Håndhev progressive forsinkelser og kontosperring etter 3 påfølgende feilforsøk. I koden må sammenligninger gjøres med funksjoner som opererer i konstant tid (f.eks. crypto/subtle.ConstantTimeCompare i Go eller crypto.timingSafeEqual i Node.js) for å forhindre tidsbaserte sidekanalsangrep.

08. Ofte stilte spørsmål (FAQ)

Q1: Siden SHA-1 regnes som kryptografisk brutt, hvorfor bruker TOTP fortsatt HMAC-SHA1 som standard? ▼

De teoretiske kollisjonsangrepene mot ren SHA-1 rammer ikke HMAC-konstruksjonen (RFC 2104). I HMAC er den underliggende hash-funksjonen kapslet inn med interne og eksterne hemmelige nøkler, noe som nøytraliserer kollisjonsbaserte angrep. Frem til i dag forblir HMAC-SHA1 sikkert for 2FA. I tillegg støtter RFC 6238 fullt ut HMAC-SHA256 og HMAC-SHA512 dersom det kreves høyere sikkerhetsnivå.

Q2: Påvirker tidssoner eller sommertid (DST) synkroniseringen av TOTP? ▼

Nei. Unix-tid er definert som antall forløpte sekunder siden 1970-01-01 00:00:00 UTC. Den er fullstendig uavhengig av tidssoner, skuddsekunder og sommertidsjusteringer. Til enhver tid har en server i Tokyo og en smarttelefon i California nøyaktig samme heltallstidsstempel.

Q3: Hvordan skiller Steam Guards 5-bokstavers autentisering seg fra RFC 6238? ▼

Steam Guard er en proprietær variant basert på RFC 6238. Den bruker nøyaktig samme 30-sekunders tidstrinn og HMAC-SHA1-beregning, men i den siste reduksjonsfasen mapper den det utpakkede heltallet gjennom en egendefinert tabell med 26 alfanumeriske tegn (23456789BCDFGHJKMNPQRTVWXY) for å produsere en streng på 5 tegn.

RFC 6238-kompatibel · Klientsideberegning med W3C Web Crypto API

Opplev 100 % klientside TOTP-kodegenerering direkte

2FA-Tool implementerer denne eksakte kryptografiske rørledningen direkte i nettleseren din ved hjelp av W3C Web Crypto API. Ingen serveroverføring, ingen lagring av tokens, umiddelbar validering.