Mezi základními kryptografickými protokoly moderní webové bezpečnosti vyniká RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) svou matematickou elegancí a přímočarostí. Nevyžaduje žádné interaktivní síťové výměny, žádnou centralizovanou infrastrukturu veřejných klíčů (PKI) ani komunikaci s cloudem. Pouze se sdíleným tajným klíčem v kódování Base32 a neúprosným tokem fyzického unixového času dokáže klient i server se stoprocentní matematickou jistotou vypočítat identické, krátkodobé 6místné autentizační tokeny každých 30 sekund.
V tomto komplexním průvodci odložíme knihovny třetích stran (jako pyotp nebo otplib), které fungují jako černé skříňky, a sestavíme kompletní kryptografickou pipeline podle RFC 6238 od základu v Pythonu, JavaScriptu a Go, přičemž každou implementaci ověříme proti oficiálním testovacím vektorům IETF.
01. Elegance autentizace řízené časem
Abychom plně pochopili TOTP, musíme se nejprve vrátit k jeho přímému předchůdci: RFC 4226 HOTP (HMAC-Based One-Time Password).
V HOTP klient a server sdílejí čítač událostí $C$. Pokaždé, když uživatel vygeneruje token, čítač se inkrementuje ($C = C + 1$) a token se odvodí z $\text{HMAC-SHA-1}(K, C)$. Ačkoli byl HOTP matematicky bezchybný, trpěl zásadní provozní nevýhodou: desynchronizací čítače. Pokud uživatel v kapse nechtěně stiskl tlačítko na svém hardwarovém tokenu pětkrát, čítač v zařízení dosáhl hodnoty $C=6$, zatímco autentizační server zůstal na $C=1$, což vedlo k trvalému selhání přihlašování.
Standard RFC 6238, který v květnu 2011 publikovali N. M'Raihi, S. Machani, M. Pei a J. Rydell, vyřešil problém s desynchronizací čítače tím, že rostoucí celé číslo nahradil standardizovaným unixovým časem (Unix epoch). Vzhledem k tomu, že fyzický čas plyne rovnoměrně po celé planetě, klient i server posouvají své interní stavy synchronně a současně, aniž by si museli vyměnit jediný síťový paket.
02. 5 kryptografických kroků algoritmu TOTP
Převod 16znakového Base32 seedu (např. JBSWY3DPEHPK3PXP) na 6místný rotující kód (např. 284915) zahrnuje pět přesných kryptografických fází:
Registrační klíče jsou kódovány v RFC 4648 Base32 pomocí abecedy A-Z a 2-7 (s vynecháním snadno zaměnitelných znaků jako 0, 1, 8, 9). Algoritmus nejprve tento řetězec normalizuje, odstraní mezery a doplní zarovnání (=), aby délka byla násobkem 8, a poté jej dekóduje na surové binární bajty klíče $K$.
Aktuální unixové časové razítko (počet sekund uplynulých od 1. ledna 1970 UTC) je namapováno na celočíselný čítač $T$:
Se standardními parametry $T_0 = 0$ a délkou časového okna $X = 30$ sekund. Toto celé číslo $T$ je následně serializováno jako 8bajtové bezznaménkové 64bitové číslo ve formátu big-endian.
S použitím surových bajtů klíče $K$ jako tajného klíče spočítáme HMAC-SHA1 otisk přes 8bajtový big-endian časový buffer, čímž vznikne 20bajtový (160bitový) autentizační otisk $HS$:
Pro převod 20 surových bajtů na celé číslo definuje specifikace RFC 4226 dynamické zkrácení:
- Získání offsetu (posunu): Přečte se poslední bajt $HS[19]$ a provede se bitový součin s
0x0Fpro extrakci jeho spodních 4 bitů, což dává hodnotu posunu od 0 do 15:Offset = HS[19] & 0x0F; - Vyříznutí 4 bajtů: Z otisku se vyjmou 4 po sobě jdoucí bajty počínaje daným posunem: $P = HS[Offset \dots Offset+3]$;
- Vynulování znaménkového bitu: Nejvyšší platný bit (MSB) se maskuje pomocí
0x7F, aby se zabránilo nejednoznačnostem se znaménkovým přetečením:
BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
Nakonec zredukujeme BinaryCode operací modulo $10^6$ (1 000 000) pro standardní 6místné tokeny. Výsledek doplníme úvodními nulami zleva, aby měl výstup vždy přesně 6 znaků:
03. Spustitelný kód 1: Standardní knihovna Python 3
Tato kompletní implementace využívá vestavěné moduly Pythonu base64, struct, hmac a hashlib bez nutnosti instalovat jakékoli balíčky přes 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: """ Vygeneruje TOTP token vyhovující standardu RFC 6238. :param secret_base32: Sdílený tajný klíč ve formátu Base32 (např. ‘JBSWY3DPEHPK3PXP’) :param timestamp: Unixové časové razítko v sekundách. Výchozí hodnota je time.time() :param step: Časový krok v sekundách (výchozí: 30) :param digits: Počet výstupních číslic (výchozí: 6) :return: 6místný OTP řetězec zarovnaný nulami (např. ‘045812’) """ if timestamp is None: timestamp = int(time.time())
# 1. Normalizace řetězce Base32 a doplnění znaků '=' 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. Výpočet 64bitového big-endian čítače časových kroků T time_counter = timestamp // step time_bytes = struct.pack(">Q", time_counter) # 8 bajtů big-endian unsigned long long # 3. Výpočet otisku HMAC-SHA1 (20 bajtů) hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest() # 4. Provedení dynamického zkrácení (Dynamic Truncation) offset = hmac_digest[-1] & 0x0F # Spodní 4 bity 20. bajtu # Rozbalení 4 bajtů na daném offsetu a vynulování znaménkového bitu (MSB) binary_code = struct.unpack(">I", hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF # 5. Modulo operace a doplnění nulami otp_code = binary_code % (10 ** digits) return str(otp_code).zfill(digits)
if name == “main”: test_secret = “JBSWY3DPEHPK3PXP” print(f”Vygenerovaný TOTP kód: {generate_totp(test_secret)}”)
04. Spustitelný kód 2: JavaScript a Node.js Crypto
Čistá implementace v JavaScriptu pro backendové běhové prostředí Node.js s využitím nativního modulu crypto.createHmac:
const crypto = require('crypto');// Dekodér RFC 4648 Base32 v čistém JavaScriptu 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);
// Alokace 8bajtového bufferu ve formátu Big-Endian const buffer = Buffer.alloc(8); buffer.writeBigUInt64BE(BigInt(counter), 0);
// Výpočet HMAC-SHA1 const hmac = crypto.createHmac(‘sha1’, key).update(buffer).digest();
// Dynamické zkrácení (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(“Vypočítaný TOTP kód:”, generateTOTP(“JBSWY3DPEHPK3PXP”));
Spuštění v prohlížeči přes standardní W3C Web Crypto API:
Pro moderní webové aplikace vyžadující 100% offline bezpečnost (přesně taková architektura pohání nástroj 2FA-Tool) nabízí W3C Web Crypto API asynchronní, hardwarově akcelerované kryptografické funkce přímo v sandboxu prohlížeče bez nutnosti stahovat externí npm balíčky:
async function generateBrowserTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000)) { const keyBytes = base32Decode(secretBase32); const counter = Math.floor(timestamp / 30);// 1. Import surových bajtů klíče do SubtleCrypto const cryptoKey = await window.crypto.subtle.importKey( “raw”, keyBytes, { name: “HMAC”, hash: { name: “SHA-1” } }, false, [“sign”] );
// 2. Kódování 8bajtového big-endian čítače const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, BigInt(counter), false); // false = Big-Endian
// 3. Kryptografický výpočet HMAC podpisu const signature = await window.crypto.subtle.sign(“HMAC”, cryptoKey, buffer); const hmacResult = new Uint8Array(signature);
// 4. Dynamické zkrácení (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. Spustitelný kód 3: Produkční kód v Go (Golang)
Vysoce propustné mikroslužby těží z prověřené a efektivní standardní knihovny jazyka Go:
package mainimport ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “math” “strings” “time” )
// GenerateTOTP vypočítá standardní časové tokeny dle RFC 6238 func GenerateTOTP(secretBase32 string, timestamp int64, step int64, digits int) (string, error) { // 1. Očištění Base32 řetězce a dekódování 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. Kódování 64bitového big-endian čítače časového kroku counter := uint64(timestamp / step) buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, counter) // 3. Výpočet HMAC-SHA1 mac := hmac.New(sha1.New, key) mac.Write(buf) hash := mac.Sum(nil) // 4. Dynamické zkrácení (Dynamic Truncation) offset := hash[len(hash)-1] & 0x0f binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff // 5. Modulo aritmetika 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(“Golangem vypočítaný TOTP kód: %s\n”, code) }
06. Ověření testovacích vektorů podle RFC 6238 Příloha B
K prokázání plné shody se standardem musí být každá kryptografická implementace ověřena proti oficiálním testovacím vektorům publikovaným v IETF RFC 6238 Příloha B.
Oficiální sada testů používá ASCII tajný klíč "12345678901234567890" (20 bajtů), který má v Base32 následující podobu:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (testováno pro 8místné výstupy s 30sekundovými kroky):
| Unixové časové razítko | UTC Datum a čas | Očekávaný kód dle RFC | Výsledek implementace |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ ÚSPĚŠNĚ OVĚŘENO (Přesná shoda) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ ÚSPĚŠNĚ OVĚŘENO (Přesná shoda) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ ÚSPĚŠNĚ OVĚŘENO (Přesná shoda) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ ÚSPĚŠNĚ OVĚŘENO (Přesná shoda) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ ÚSPĚŠNĚ OVĚŘENO (Přesná shoda) |
07. Produkční inženýrství: Posun, replay a limity
Při nasazování ověřování TOTP ve vysoce dostupných podnikových autentizačních systémech je nutné striktně vymáhat tři základní bezpečnostní pravidla:
V důsledku drobných nepřesností hodin na mobilních zařízeních by servery měly vyhodnocovat okno [T - 1, T, T + 1] (tedy kontrolovat předchozích 30 s, aktuálních 30 s a následujících 30 s). Nikdy nerozšiřujte okno za hranici $\pm 1$ kroku, protože každý další krok rozšiřuje prostor pro útoky přehráním (replay attack) o dalších 30 sekund.
Jakmile je token úspěšně ověřen, musí být okamžitě zneplatněn. Uložte Last_Verified_Time_Step do mezipaměti Redis nebo do paměti aplikace. Pokud se klient pokusí odeslat stejný token znovu ve stejném 30sekundovém okně, požadavek okamžitě zamítněte, což neutralizuje útoky typu man-in-the-middle replay.
Jelikož 6místné kódy pokrývají pouze 1 000 000 možností, nechráněné koncové body lze velmi rychle prolomit útokem hrubou silou. Zaveďte progresivní zpoždění a dočasné zablokování účtu po 3 po sobě jdoucích neúspěšných pokusech. V kódu provádějte porovnání řetězců pomocí funkcí běžících v konstantním čase (např. crypto/subtle.ConstantTimeCompare v Go nebo crypto.timingSafeEqual v Node.js), abyste zabránili postranním časovým útokům (timing attacks).
08. Často kladené otázky (FAQ)
Otázka 1: Proč TOTP stále používá HMAC-SHA1, když je samostatný hash SHA-1 kryptograficky prolomený? ▼
Teoretické kolizní útoky proti samostatnému hashi SHA-1 nenarušují konstrukci HMAC (RFC 2104). V HMAC je podkladová hashovací funkce zanořena mezi interní a externí tajné klíče, což zcela neutralizuje útoky založené na kolizích. Dodnes zůstává HMAC-SHA1 pro účely 2FA plně bezpečný. Navíc standard RFC 6238 plně podporuje i HMAC-SHA256 a HMAC-SHA512, pokud jsou vyžadovány vyšší bezpečnostní záruky.
Otázka 2: Mají časová pásma nebo letní čas (DST) vliv na synchronizaci TOTP? ▼
Nikoli. Unixový čas (Epoch Time) je definován jako počet sekund uplynulých od 1970-01-01 00:00:00 UTC. Je zcela nezávislý na časových pásmech, přestupných sekundách i posunech letního času. V libovolném okamžiku mají server v Tokiu i smartphone v Praze naprosto identické celočíselné časové razítko.
Otázka 3: V čem se liší pětimístné ověřování Steam Guard od standardu RFC 6238? ▼
Steam Guard je proprietární varianta založená na RFC 6238. Využívá naprosto stejný 30sekundový časový krok i výpočet HMAC-SHA1, ale v závěrečné fázi redukce mapuje vyextrahované celé číslo pomocí vlastní 26znakové alfanumerické abecedy (23456789BCDFGHJKMNPQRTVWXY), čímž vytváří pětimístný textový kód.
Vyzkoušejte si generování kódů TOTP čistě na straně klienta naživo
2FA-Tool implementuje tuto přesnou kryptografickou pipeline přímo ve vašem prohlížeči pomocí W3C Web Crypto API. Žádné odesílání dat na server, nulové ukládání tokenů, okamžité ověření.