💻 Primijenjena kriptografija · Detaljni inženjerski vodič

Kako radi TOTP: Implementacija algoritma RFC 6238 u Pythonu, JavaScriptu i Gou

2F Sigurnosni istraživački laboratorij 2FA-Tool · Ažurirano: listopad 2026. · 15 min čitanja (više od 2.700 riječi)
💡 Arhitektonski sažetak i inženjerski manifest

Među temeljnim kriptografskim protokolima u modernoj web sigurnosti, RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) ističe se svojom matematičkom jednostavnošću. Ne zahtijeva interaktivna mrežna rukovanja, centraliziranu infrastrukturu javnih ključeva (PKI) niti slanje upita prema poslužiteljima u oblaku. Uz samo zajednički Base32 tajni ključ i neprekidni tijek fizičkog Unix vremena, klijent i poslužitelj izračunavaju identične, kratkotrajne 6-znamenkaste autentifikacijske tokene svakih 30 sekundi sa 100% matematičkom točnošću.

U ovom sveobuhvatnom vodiču zaobilazimo gotove vanjske biblioteke (poput pyotp ili otplib) te gradimo cjelokupni kriptografski cjevovod RFC-a 6238 od nule u Pythonu, JavaScriptu i Gou, provjeravajući svaku implementaciju prema službenim testnim vektorima IETF-a.

01. Elegancija autentifikacije temeljene na vremenu

Kako bismo razumjeli TOTP, moramo prvo proučiti njegovog prethodnika: RFC 4226 HOTP (HMAC-Based One-Time Password).

Kod HOTP-a klijent i poslužitelj dijele brojač događaja $C$. Svaki put kada korisnik generira token, brojač se povećava ($C = C + 1$), a token se izvodi iz $\text{HMAC-SHA-1}(K, C)$. Iako matematički besprijekoran, HOTP je patio od operativne slabosti: desinkronizacije brojača. Ako je korisnik slučajno pritisnuo tipku na svom hardverskom tokenu pet puta u džepu, brojač klijenta dosegao bi $C=6$, dok bi autentifikacijski poslužitelj ostao na $C=1$, što je dovodilo do neuspješnih prijava.

⏱️ Prekretnica RFC-a 6238: Zamjena brojača događaja vremenom epohe

Objavljen u svibnju 2011. od strane autora N. M'Raihi, S. Machani, M. Pei i J. Rydell, RFC 6238 riješio je problem desinkronizacije brojača zamjenom rastućeg cijelog broja standardiziranim Unix vremenom epohe. Budući da fizičko vrijeme jednako teče diljem svijeta, i klijent i poslužitelj istodobno pomiču svoje interne brojače bez slanja ijednog mrežnog paketa.

02. 5 kriptografskih matematičkih koraka TOTP-a

Pretvaranje Base32 početnog niza od 16 znakova (npr. JBSWY3DPEHPK3PXP) u promjenjivi 6-znamenkasti kod (npr. 284915) obuhvaća pet preciznih kriptografskih faza:

1 Korak 1: Dekodiranje Base32 tajnog ključa

Ključevi za registraciju kodirani su u formatu RFC 4648 Base32 koristeći abecedu A-Z i 2-7 (pri čemu se izostavljaju dvosmisleni znakovi poput 0, 1, 8, 9). Algoritam prvo normalizira ovaj niz, uklanjajući razmake i dodajući znakove dopune (=) tako da mu duljina bude višekratnik broja 8, prije nego što ga dekodira u sirove bajtove binarnog ključa $K$.

2 Korak 2: Izračun vremenskog koraka (Brojač $T$)

Trenutna Unix vremenska oznaka (sekunde protekle od 1. siječnja 1970. UTC) preslikava se u cjelobrojni brojač $T$:

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

Uz standardne parametre $T_0 = 0$ i trajanje koraka $X = 30$ sekundi. Ovaj cijeli broj $T$ potom se kodira kao 8-bajtni neoznačeni 64-bitni cijeli broj u big-endian poretku bajtova.

3 Korak 3: Izračun HMAC-SHA1 sažetka

Koristeći sirove bajtove ključa $K$ kao tajni ključ, računamo HMAC-SHA1 sažetak preko 8-bajtnog big-endian vremenskog međuspremnika, što daje sažetak za autentifikaciju poruke od 20 bajtova (160 bita) $HS$:

HS = HMAC-SHA-1(Key, T)
4 Korak 4: Dinamičko skraćivanje (Dynamic Truncation - DT)

Za pretvaranje 20 sirovih bajtova u cijeli broj, RFC 4226 propisuje dinamičko skraćivanje:

  • Izdvajanje pomaka (Offset): Pregledava se posljednji bajt $HS[19]$ i maskira s 0x0F kako bi se izdvojila njegova 4 donja bita, čime se dobiva vrijednost pomaka između 0 i 15: Offset = HS[19] & 0x0F;
  • Izrezivanje 4 bajta: Izdvajaju se 4 uzastopna bajta počevši od Offseta: $P = HS[Offset \dots Offset+3]$;
  • Maskiranje bita predznaka: Maskira se najviši bit (MSB) s 0x7F kako bi se spriječila dvosmislenost preljeva označenog cijelog broja:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Korak 5: Modulo redukcija na $N$ znamenki

Na kraju, reduciramo BinaryCode operacijom modulo $10^6$ (1.000.000) za standardne 6-znamenkaste tokene. Rezultat dopunjavamo vodećim nulama slijeva kako bismo osigurali ujednačen izlazni niz od 6 znakova:

OTP = BinaryCode mod 10^6

03. Pokretljivi kod 1: Standardna biblioteka Pythona 3

Ova cjelovita implementacija koristi ugrađene module Pythona base64, struct, hmac i hashlib bez potrebe za instaliranjem vanjskih paketa putem pip-a:

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. Pokretljivi kod 2: JavaScript i Node.js Crypto

Čista JavaScript implementacija za poslužiteljsko okruženje Node.js koja koristi izvorni modul 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”));

Izvršavanje u pregledniku na strani klijenta putem W3C Web Crypto API-ja:

Za moderne web aplikacije koje teže 100% izvanmrežnoj sigurnosti (upravo onakva arhitektura koja pokreće 2FA-Tool), W3C Web Crypto API pruža asinkrone, hardverski ubrzane kriptografske funkcije izravno u sigurnosnom okruženju preglednika bez vanjskih npm paketa:

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] & 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. Pokretljivi kod 3: Produkcijski Golang (Go)

Mikroservisi s visokim opterećenjem imaju koristi od Go-ove provjerene i optimizirane standardne biblioteke:

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] & 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. Provjera službenih testnih vektora iz Dodatka B RFC-a 6238

Kako bi se dokazala usklađenost sa standardom, svaka kriptografska implementacija mora se verificirati prema službenim testnim vektorima objavljenim u Dodatku B specifikacije IETF RFC 6238.

Službeni testni skup koristi ASCII tajni ključ "12345678901234567890" (20 bajtova), koji se u Base32 formatu kodira kao:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (evaluirano za 8-znamenkaste vrijednosti uz vremenske korake od 30 sekundi):

Vremenska oznaka Unix epohe UTC datum i vrijeme Službeni očekivani RFC kod Rezultat implementacije
59 1970-01-01 00:00:59 94287082 ✓ PROŠLO (Točno podudaranje)
1111111109 2005-03-18 01:58:29 07081804 ✓ PROŠLO (Točno podudaranje)
1111111111 2005-03-18 01:58:31 14050471 ✓ PROŠLO (Točno podudaranje)
1234567890 2009-02-13 23:31:30 89005924 ✓ PROŠLO (Točno podudaranje)
2000000000 2033-05-18 03:33:20 69279037 ✓ PROŠLO (Točno podudaranje)

07. Produkcijsko inženjerstvo: Pomak sata, ponavljanje i ograničenja

Pri implementaciji TOTP provjere u visoko dostupnim korporativnim sustavima autentifikacije moraju se primijeniti tri ključna sigurnosna pravila:

Pravilo 1 Prozor tolerancije na pomak sata (Clock Drift)

Zbog manjih netočnosti na satovima mobilnih telefona, poslužitelji bi trebali provjeravati prozor [T - 1, T, T + 1] (provjeravajući prethodnih 30s, trenutnih 30s i sljedećih 30s). Nikada nemojte širiti prozor izvan $\pm 1$ koraka, jer svaki dodatni korak proširuje površinu za napad ponavljanjem za dodatnih 30 sekundi.

Pravilo 2 Zabrana ponovnog korištenja tokena (Anti-Replay)

Token mora biti poništen odmah nakon uspješnog korištenja. Pohranite Last_Verified_Time_Step u Redis ili priručnu memoriju. Ako klijent pokuša poslati isti token unutar istog prozora od 30 sekundi, odbijte zahtjev kako biste neutralizirali napade posrednikom (Man-in-the-Middle replay).

Pravilo 3 Ograničavanje broja pokušaja i usporedba u konstantnom vremenu

Budući da 6-znamenkasti kodovi pokrivaju samo 1.000.000 kombinacija, krajnje točke bez zaštite mogu se brzo probiti napadom grubom silom. Uvedite progresivne vremenske odgode i privremeno zaključavanje računa nakon 3 uzastopna neuspjela pokušaja. U programskom kodu usporedbu provodite funkcijama u konstantnom vremenu (npr. crypto/subtle.ConstantTimeCompare u Gou ili crypto.timingSafeEqual u Node.js) kako biste spriječili analizu vremenskih odstupanja putem bočnih kanala.

08. Često postavljana pitanja (FAQ)

P1: Budući da je SHA-1 kompromitiran, zašto TOTP i dalje standardno koristi HMAC-SHA1? ▼

Teorijski napadi kolizijom na izvorni algoritam SHA-1 ne narušavaju sigurnost HMAC konstrukcije (RFC 2104). U HMAC-u je temeljna funkcija sažimanja ugniježđena unutar internih i vanjskih tajnih ključeva, što u potpunosti neutralizira napade temeljene na kolizijama. Do danas HMAC-SHA1 ostaje siguran za 2FA. Osim toga, RFC 6238 u potpunosti podržava HMAC-SHA256 i HMAC-SHA512 ako je potrebna viša razina sigurnosti.

P2: Utječu li vremenske zone ili ljetno/zimsko računanje vremena (DST) na TOTP sinkronizaciju? ▼

Ne. Unix vrijeme epohe definirano je kao broj sekundi proteklih od 1970-01-01 00:00:00 UTC. Ono je potpuno neovisno o vremenskim zonama, prijestupnim sekundama i pomicanju sata zbog ljetnog računanja vremena. U bilo kojem točnom trenutku, poslužitelj u Tokiju i pametni telefon u Zagrebu imaju potpuno istu cjelobrojnu vremensku oznaku.

P3: Po čemu se Steam Guard autentifikacija s 5 znakova razlikuje od RFC-a 6238? ▼

Steam Guard je vlasnička inačica temeljena na RFC-u 6238. Koristi potpuno isti 30-sekundni vremenski korak i HMAC-SHA1 izračun, ali u završnoj fazi redukcije preslikava izvučeni cijeli broj kroz prilagođeni niz od 26 alfanumeričkih znakova (23456789BCDFGHJKMNPQRTVWXY) kako bi proizveo niz od 5 znakova.

Usklađeno s RFC 6238 · Izračun na strani klijenta putem W3C Web Crypto API-ja

Doživite 100% klijentsko generiranje TOTP kodova uživo

2FA-Tool implementira ovaj identičan kriptografski cjevovod izravno u vašem pregledniku pomoću W3C Web Crypto API-ja. Nula slanja na poslužitelje, nula pohrane tokena, trenutačna provjera.