💻 Uporabna kriptografija · Poglobljen inženirski pregled

Kako deluje TOTP: Implementacija algoritma RFC 6238 v Pythonu, JavaScriptu in Go

2F Varnostni raziskovalni laboratorij 2FA-Tool · Posodobljeno: oktober 2026 · 16 min branja (več kot 2.700 besed)
💡 Arhitekturni povzetek in inženirski manifest

Med temeljnimi kriptografskimi protokoli v sodobni spletni varnosti izstopa RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) po svoji matematični preprostosti. Ne zahteva nobenih interaktivnih omrežnih usklajevanj (handshakes), nobene centralizirane infrastrukture javnih ključev (PKI) in nobenih klicev v oblak. Zgolj s skupnim skrivnim ključem Base32 in nenehnim tekom fizičnega časa Unix odjemalec in strežnik vsakih 30 sekund s 100-odstotno matematično zanesljivostjo izračunata enake, kratkotrajne 6-mestne žetone za preverjanje pristnosti.

V tem celovitem vodniku se izognemo zunanjim knjižnicam tipa črne škatle (kot sta pyotp ali otplib) in od začetka zgradimo celoten kriptografski cevovod RFC 6238 v Pythonu, JavaScriptu in Go, pri čemer vsako izvedbo preverimo z uradnimi testnimi vektorji IETF.

01. Eleganca časovno vodenega preverjanja pristnosti

Če želimo razumeti TOTP, moramo najprej spoznati njegovega predhodnika: RFC 4226 HOTP (HMAC-Based One-Time Password).

Pri HOTP si odjemalec in strežnik delita števec dogodkov $C$. Vsakič, ko uporabnik ustvari žeton, se števec poveča ($C = C + 1$), žeton pa se izpelje iz $\text{HMAC-SHA-1}(K, C)$. Čeprav je bil HOTP matematično brezhiben, je imel operativno pomanjkljivost: desinhronizacijo števca. Če je uporabnik v žepu nevede petkrat pritisnil gumb na svojem strojnem ključku, je števec odjemalca dosegel $C=6$, medtem ko je strežnik za preverjanje pristnosti ostal pri $C=1$, kar je povzročilo stalno zavračanje prijave.

⏱️ Prelomnica RFC 6238: Zamenjava števca dogodkov s časom epohe

RFC 6238, ki so ga maja 2011 objavili N. M'Raihi, S. Machani, M. Pei in J. Rydell, je rešil težavo z desinhronizacijo števca tako, da je naraščajoče celo število nadomestil s standardiziranim časom epohe Unix. Ker fizični čas po vsem svetu teče enakomerno, odjemalec in strežnik hkrati premikata svoja notranja števca, ne da bi poslala en sam omrežni paket.

02. 5 kriptografskih matematičnih korakov TOTP

Pretvorba 16-znakovnega semena Base32 (npr. JBSWY3DPEHPK3PXP) v 6-mestno vrtečo se kodo (npr. 284915) obsega pet natančnih kriptografskih faz:

1 1. korak: Dekodiranje skrivnega ključa Base32

Ključi za registracijo so kodirani v RFC 4648 Base32 z uporabo abecede A-Z in 2-7 (brez zavajajočih znakov, kot so 0, 1, 8, 9). Algoritem najprej normalizira ta niz, odstrani presledke in doda odmike (=), da je njegova dolžina večkratnik števila 8, nato pa ga dekodira v surove binarne bajte ključa $K$.

2 2. korak: Izračun časovnega koraka (Števec $T$)

Trenutni časovni žig Unix (število sekund, pretečenih od 1. januarja 1970 UTC) se preslika v celoštevilski števec $T$:

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

S standardnimi parametri $T_0 = 0$ in trajanjem koraka $X = 30$ sekund. To celo število $T$ se nato kodira kot 8-bajtno nepredznačeno celo število v zapisu z višjim bajtom na začetku (big-endian 64-bit unsigned integer).

3 3. korak: Izračun zgoščene vrednosti HMAC-SHA1

Z uporabo surovih bajtov ključa $K$ kot skrivnega ključa izračunamo povzetek HMAC-SHA1 prek 8-bajtnega časovnega medpomnilnika v zapisu big-endian, s čimer dobimo 20-bajtno (160-bitno) zgoščeno vrednost za preverjanje pristnosti sporočila $HS$:

HS = HMAC-SHA-1(Key, T)
4 4. korak: Dinamično prirezovanje (Dynamic Truncation - DT)

Za pretvorbo 20 surovih bajtov v celo število RFC 4226 določa dinamično prirezovanje (Dynamic Truncation):

  • Izluščenje odmika (Offset): Preglejte zadnji bajt $HS[19]$ in uporabite masko 0x0F, da izluščite njegove spodnje 4 bite, kar da vrednost odmika med 0 in 15: Offset = HS[19] & 0x0F;
  • Izrez 4 bajtov: Izluščite 4 zaporedne bajte z začetkom pri odmiku Offset: $P = HS[Offset \dots Offset+3]$;
  • Maskiranje predznakovnega bita: Maskirajte najbolj pomemben bit (MSB) z 0x7F, da preprečite dvoumnost prekoračitve predznačenega celega števila:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 5. korak: Zmanjšanje po modulu na $N$ števk

Na koncu zmanjšajte BinaryCode po modulu $10^6$ (1.000.000) za standardne 6-mestne žetone. Rezultat z leve dopolnite z ničlami, da zagotovite dosleden 6-znakovni izhodni niz:

OTP = BinaryCode mod 10^6

03. Delujoča koda 1: Standardna knjižnica Python 3

Ta popolna implementacija uporablja vgrajene module Pythona base64, struct, hmac in hashlib brez potrebe po namestitvi dodatnih paketov pip:

totp_rfc6238.py Python 3.8+ (Brez odvisnosti)
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. Delujoča koda 2: JavaScript in Node.js Crypto

Čista implementacija v JavaScriptu za izvajalno okolje Node.js na zaledju z uporabo domorodnega modula crypto.createHmac:

totp_node.js Node.js Domorodni 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);

// 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] & 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(“Calculated TOTP:”, generateTOTP(“JBSWY3DPEHPK3PXP”));

Izvajanje na strani odjemalca v brskalniku prek W3C Web Crypto API:

Za sodobne spletne aplikacije, ki zahtevajo 100-odstotno varnost brez povezave (točno taka arhitektura poganja 2FA-Tool), W3C Web Crypto API ponuja asinhrona, strojno pospešena kriptografska primitiva neposredno v peskovniku brskalnika brez zunanjih paketov npm:

webcrypto_totp.js Brskalniški 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] & 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. Delujoča koda 3: Produkcijski Golang (Go)

Mikrostoritve z visoko prepustnostjo imajo veliko koristi od preizkušene standardne knjižnice jezika Go:

totp.go Standardna knjižnica Go 1.20+
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] & 0x0f
binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 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. Preverjanje testnih vektorjev RFC 6238 Dodatek B

Za dokazovanje skladnosti s standardom je treba vsako kriptografsko izvedbo preveriti z uradnimi testnimi vektorji, objavljenimi v IETF RFC 6238 Dodatek B.

Uradni testni nabor uporablja skrivnost ASCII "12345678901234567890" (20 bajtov), ki se v Base32 kodira kot:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (ocenjeno za 8-mestne izhode s 30-sekundnimi koraki):

Časovni žig epohe Unix Datum in čas UTC Uradna pričakovana koda RFC Rezultat implementacije
59 1970-01-01 00:00:59 94287082 ✓ USPEŠNO (Natančno ujemanje)
1111111109 2005-03-18 01:58:29 07081804 ✓ USPEŠNO (Natančno ujemanje)
1111111111 2005-03-18 01:58:31 14050471 ✓ USPEŠNO (Natančno ujemanje)
1234567890 2009-02-13 23:31:30 89005924 ✓ USPEŠNO (Natančno ujemanje)
2000000000 2033-05-18 03:33:20 69279037 ✓ USPEŠNO (Natančno ujemanje)

07. Produkcijski inženiring: Časovni zamik, ponovni napadi in omejitve

Pri uvajanju preverjanja pristnosti TOTP v poslovne sisteme z visoko razpoložljivostjo je treba uveljaviti tri varnostne zahteve:

Pravilo 1 Okno tolerance za časovni zamik ure (Clock Drift)

Zaradi manjših netočnosti ur mobilnih naprav morajo strežniki oceniti okno [T - 1, T, T + 1] (preverjanje prejšnjih 30 s, trenutnih 30 s in naslednjih 30 s). Nikoli ne razširjajte okna prek $\pm 1$ koraka, saj vsak dodaten korak razširi površino za ponovne napade (replay attack) za 30 sekund.

Pravilo 2 Uveljavitev enkratne uporabe žetona (Anti-Replay)

Žeton mora biti po uspešni porabi nemudoma razveljavljen. Shranite Last_Verified_Time_Step v Redis ali medpomnilnik v pomnilniku (in-memory cache). Če odjemalec poskuša predložiti isti žeton v istem 30-sekundnem oknu, zahtevo zavrnite, da preprečite napade posrednika (man-in-the-middle) s ponovnim predvajanjem.

Pravilo 3 Omejevanje hitrosti pri napadih s surovo silo in primerjava v konstantnem času

Ker 6-mestne kode obsegajo le 1.000.000 permutacij, lahko neomejene končne točke hitro podležejo napadom s surovo silo. Uveljavite progresivne zakasnitve in zaklepanje računa po 3 zaporednih neuspešnih poskusih. V kodi izvajajte primerjave z uporabo funkcij s konstantnim časom (npr. crypto/subtle.ConstantTimeCompare v Go ali crypto.timingSafeEqual v Node.js), da preprečite analizo časovne uskladitve prek stranskih kanalov.

08. Pogosta vprašanja (FAQ)

V1: Ker je SHA-1 kriptografsko zlomljen, zakaj TOTP še vedno privzeto uporablja HMAC-SHA1? ▼

Teoretični napadi s kolizijami proti surovemu SHA-1 ne ogrožajo konstrukcije HMAC (RFC 2104). V HMAC je osnovna funkcija zgoščevanja vdelana v notranje in zunanje skrivne ključe, kar nevtralizira napade na podlagi kolizij. Do danes ostaja HMAC-SHA1 popolnoma varen za 2FA. Poleg tega RFC 6238 v celoti podpira HMAC-SHA256 in HMAC-SHA512, če je zahtevana višja raven varnosti.

V2: Ali časovni pasovi ali poletni čas (DST) vplivajo na sinhronizacijo TOTP? ▼

Ne. Čas epohe Unix je definiran kot število pretečenih sekund od 1970-01-01 00:00:00 UTC. Je popolnoma neodvisen od časovnih pasov, prestopnih sekund in premikov na poletni čas. V katerem koli natančnem trenutku imata strežnik v Tokiu in pametni telefon v Kaliforniji popolnoma enak celoštevilski časovni žig.

V3: Kako se 5-znakovno preverjanje pristnosti Steam Guard razlikuje od RFC 6238? ▼

Steam Guard je lastniška različica, ki temelji na RFC 6238. Uporablja popolnoma enak 30-sekundni časovni korak in izračun HMAC-SHA1, vendar v končni fazi zmanjšanja preslika izluščeno celo število prek lastnega 26-znakovnega alfanumeričnega polja (23456789BCDFGHJKMNPQRTVWXY), s čimer ustvari 5-znakovni alfanumerični niz.

Skladno z RFC 6238 · Izračun na strani odjemalca z W3C Web Crypto API

Izkusite ustvarjanje kod TOTP v živo 100 % na strani odjemalca

2FA-Tool izvaja ta natančen kriptografski cevovod neposredno v vašem brskalniku z uporabo W3C Web Crypto API. Brez prenosa na strežnik, brez shranjevanja žetonov, takojšnje preverjanje.