Medzi základnými kryptografickými protokolmi modernej webovej bezpečnosti vyniká RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) svojou matematickou jednoduchosťou. Nevyžaduje žiadne interaktívne sieťové handshake protokoly, žiadnu centralizovanú infraštruktúru verejných kľúčov (PKI) ani volania do cloudu. Iba so zdieľaným tajným kľúčom Base32 a neustálym plynutím fyzického unixového času klient a server každých 30 sekúnd vypočítajú identické, efemérne 6-ciferné autentifikačné tokeny so 100 % matematickou istotou.
V tomto komplexnom sprievodcovi obídeme externé knižnice fungujúce ako čierne skrinky (ako pyotp alebo otplib) a vytvoríme kompletný kryptografický reťazec RFC 6238 od nuly v Pythone, JavaScripte a Go, pričom každú implementáciu overíme voči oficiálnym testovacím vektorom IETF.
01. Elegancia autentifikácie riadenej časom
Aby sme pochopili TOTP, musíme sa najprv pozrieť na jeho predchodcu: RFC 4226 HOTP (HMAC-Based One-Time Password).
V HOTP klient a server zdieľajú počítadlo udalostí $C$. Zakaždým, keď používateľ vygeneruje token, počítadlo sa zvýši ($C = C + 1$) a token sa odvodí z $\text{HMAC-SHA-1}(K, C)$. Hoci bol tento prístup matematicky spoľahlivý, trpel prevádzkovou slabinou: desynchronizáciou počítadla. Ak používateľ nechtiac stlačil tlačidlo na hardvérovom tokene päťkrát vo vrecku, počítadlo klienta dosiahlo $C=6$, zatiaľ čo autentifikačný server zostal na hodnote $C=1$, čo viedlo k trvalému odmietaniu prihlásenia.
Štandard RFC 6238, ktorý v máji 2011 publikovali N. M'Raihi, S. Machani, M. Pei a J. Rydell, vyriešil problém desynchronizácie počítadla nahradením prírastkového celého čísla štandardizovaným unixovým časom epochy. Keďže fyzický čas plynie na celom svete rovnomerne, klient aj server posúvajú svoje interné počítadlá súčasne bez odoslania jediného sieťového paketu.
02. 5 kryptografických matematických krokov TOTP
Transformácia 16-znakového semienka Base32 (napr. JBSWY3DPEHPK3PXP) na 6-ciferný rotujúci kód (napr. 284915) zahŕňa päť presných kryptografických fáz:
Registračné kľúče z burzy či služby sú kódované v štandarde RFC 4648 Base32 pomocou abecedy A-Z a 2-7 (s vynechaním nejednoznačných znakov ako 0, 1, 8, 9). Algoritmus najprv tento reťazec normalizuje, odstráni medzery a doplní zarovnanie (=), aby bola jeho dĺžka násobkom 8, a následne ho dekóduje na surové binárne bajty kľúča $K$.
Aktuálna unixová časová pečiatka (počet sekúnd uplynutých od 1. januára 1970 UTC) sa mapuje na celočíselné počítadlo $T$:
So štandardnými parametrami $T_0 = 0$ a trvaním kroku $X = 30$ sekúnd. Toto celé číslo $T$ sa následne zakóduje ako 8-bajtové celé číslo bez znamienka vo formáte Big-Endian (64-bit unsigned integer).
So surovými bajtmi kľúča $K$ ako tajným kľúčom vypočítame digest HMAC-SHA1 cez 8-bajtový big-endian časový buffer, čím získame 20-bajtový (160-bitový) autentifikačný hash správy $HS$:
Na konverziu 20 surových bajtov na celé číslo definuje RFC 4226 dynamické orezanie (Dynamic Truncation):
- Extrakcia posunu (Offset): Skontrolujte posledný bajt $HS[19]$ a aplikujte masku
0x0Fna extrahovanie jeho spodných 4 bitov, čo poskytne hodnotu posunu medzi 0 a 15:Offset = HS[19] & 0x0F; - Výrez 4 bajtov: Extrahujte 4 po sebe idúce bajty začínajúce na Offset: $P = HS[Offset \dots Offset+3]$;
- Maskovanie znamienkového bitu: Maskujte najvýznamnejší bit (MSB) hodnotou
0x7F, aby sa predišlo nejednoznačnosti pretečenia celého čísla so znamienkom:
BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
Nakoniec redukujte BinaryCode modulo $10^6$ (1 000 000) pre štandardné 6-ciferné tokeny. Výsledok zarovnajte zľava nulami, aby sa zabezpečil konzistentný 6-znakový výstupný reťazec:
03. Spustiteľný kód 1: Štandardná knižnica Python 3
Táto kompletná implementácia využíva vstavané moduly Pythonu base64, struct, hmac a hashlib bez nutnosti inštalovať akékoľvek balíky tretích strán cez pip:
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: """ 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. Spustiteľný kód 2: JavaScript a Node.js Crypto
Čistá implementácia v JavaScripte pre runtime Node.js na backende s využitím natívneho crypto.createHmac:
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”));
Klientske vykonávanie v prehliadači cez W3C Web Crypto API:
Pre moderné webové aplikácie vyžadujúce 100 % offline bezpečnosť (presne taká architektúra poháňa 2FA-Tool) poskytuje W3C Web Crypto API asynchrónne, hardvérovo akcelerované kryptografické primitíva priamo v sandboxe prehliadača bez externých balíkov z npm:
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. Spustiteľný kód 3: Produkčný Golang (Go)
Vysoko priepustné mikroslužby ťažia z rokmi overenej štandardnej knižnice jazyka Go:
package mainimport ( “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. Overenie testovacích vektorov RFC 6238 Príloha B
Na preukázanie zhody so štandardom musí byť každá kryptografická implementácia overená voči oficiálnym testovacím vektorom publikovaným v IETF RFC 6238 Príloha B.
Oficiálna testovacia sada používa ASCII tajomstvo "12345678901234567890" (20 bajtov), ktoré sa v Base32 kóduje ako:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (vyhodnotené pre 8-ciferné výstupy s 30-sekundovými krokmi):
| Unixová časová pečiatka epochy | Dátum a čas UTC | Oficiálny očakávaný kód RFC | Výsledok implementácie |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ SPLNENÉ (Presná zhoda) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ SPLNENÉ (Presná zhoda) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ SPLNENÉ (Presná zhoda) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ SPLNENÉ (Presná zhoda) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ SPLNENÉ (Presná zhoda) |
07. Produkčné inžinierstvo: Časový posun, opakované útoky a limity
Pri nasadzovaní overovania TOTP do podnikových autentifikačných systémov s vysokou dostupnosťou je potrebné presadzovať tri bezpečnostné pravidlá:
Vzhľadom na drobné nepresnosti hodín mobilných zariadení by servery mali vyhodnocovať okno [T - 1, T, T + 1] (kontrola predchádzajúcich 30 s, aktuálnych 30 s a nasledujúcich 30 s). Nikdy nerozširujte okno nad rámec $\pm 1$ kroku, pretože každý ďalší krok zväčšuje útočnú plochu pre útok opakovaním (replay attack) o 30 sekúnd.
Token musí byť po úspešnom overení okamžite zneplatnený. Uložte Last_Verified_Time_Step do Redis alebo do vyrovnávacej pamäte v pamäti (in-memory cache). Ak sa klient pokúsi odoslať rovnaký token v rámci toho istého 30-sekundového okna, požiadavku odmietnite, aby ste zmarili man-in-the-middle replay útoky.
Pretože 6-ciferné kódy pokrývajú len 1 000 000 permutácií, neobmedzené koncové body (endpoints) možno rýchlo prelomiť hrubou silou. Zaveďte progresívne oneskorenia a uzamknutie účtu po 3 po sebe idúcich neúspešných pokusoch. V kóde vykonávajte porovnávania pomocou funkcií s konštantným časom (napr. crypto/subtle.ConstantTimeCompare v Go alebo crypto.timingSafeEqual v Node.js), aby ste zabránili analýze časovania cez postranné kanály (side-channel timing attacks).
08. Často kladené otázky (FAQ)
Otázka 1: Keďže SHA-1 je kryptograficky prelomený, prečo TOTP stále predvolene používa HMAC-SHA1? ▼
Teoretické kolízne útoky proti samotnému SHA-1 nenarúšajú konštrukciu HMAC (RFC 2104). V HMAC je základná hashovacia funkcia vnorená v rámci interných a externých tajných kľúčov, čo neutralizuje útoky založené na kolíziách. Dodnes zostáva HMAC-SHA1 plne bezpečný pre 2FA. Okrem toho RFC 6238 plne podporuje HMAC-SHA256 a HMAC-SHA512, ak sa vyžaduje vyššia úroveň záruky.
Otázka 2: Ovplyvňujú časové pásma alebo letný čas (DST) synchronizáciu TOTP? ▼
Nie. Unixový čas epochy je definovaný ako počet uplynutých sekúnd od 1970-01-01 00:00:00 UTC. Je striktne nezávislý od časových pásiem, prestupných sekúnd a posunov letného času. V ktoromkoľvek presnom časovom okamihu majú server v Tokiu a smartfón v Kalifornii úplne rovnakú celočíselnú časovú pečiatku.
Otázka 3: Ako sa 5-znaková autentifikácia Steam Guard líši od RFC 6238? ▼
Steam Guard je proprietárny variant založený na RFC 6238. Používa úplne rovnaký 30-sekundový časový krok a výpočet HMAC-SHA1, ale v konečnej fáze redukcie mapuje extrahované celé číslo cez vlastné 26-znakové alfanumerické pole znakov (23456789BCDFGHJKMNPQRTVWXY), čím vytvára 5-znakový alfanumerický reťazec.
Vyskúšajte si generovanie kódov TOTP čisto na strane klienta naživo
2FA-Tool implementuje tento presný kryptografický reťazec priamo vo vašom prehliadači pomocou W3C Web Crypto API. Žiadne odosielanie na server, žiadne ukladanie tokenov, okamžité overenie.