💻 Alkalmazott kriptográfia · Mélyreható mérnöki elemzés

Hogyan működik a TOTP: Az RFC 6238 algoritmus megvalósítása Python, JavaScript és Go nyelven

2F 2FA-Tool Biztonsági Kutatólaboratórium · Frissítve: 2026. október · 16 perc olvasás (több mint 2700 szó)
💡 Architektúra-összefoglaló és mérnöki kiáltvány

A modern webes biztonság alapvető kriptográfiai protokolljai közül az RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) kiemelkedik matematikai eleganciájával és egyszerűségével. Nem igényel interaktív hálózati kézfogást, központosított nyilvános kulcsú infrastruktúrát (PKI), sem oda-vissza felhőbeli kommunikációt. Csupán egy megosztott Base32 titkos kulcsra és a fizikai Unix-idő megállíthatatlan előrehaladására támaszkodva a kliens és a szerver 100%-os matematikai bizonyossággal azonos, ideiglenes 6 számjegyű hitelesítési tokent számít ki 30 másodpercenként.

Ebben az átfogó útmutatóban elkerüljük a zárt feketedobozos külső csomagokat (mint a pyotp vagy az otplib), és a teljes RFC 6238 kriptográfiai folyamatot az alapoktól építjük fel Python, JavaScript és Go nyelven, minden egyes megvalósítást a hivatalos IETF tesztvektorokkal ellenőrizve.

01. Az idővezérelt hitelesítés eleganciája

A TOTP megértéséhez először elődjét kell megvizsgálnunk: az RFC 4226 HOTP (HMAC-Based One-Time Password) algoritmust.

A HOTP-ban a kliens és a szerver egy közös $C$ eseményszámlálót használ. Valahányszor a felhasználó tokent generál, a számláló eggyel nő ($C = C + 1$), a tokent pedig a $\text{HMAC-SHA-1}(K, C)$ összefüggésből származtatják. Bár matematikailag megalapozott volt, a HOTP egy komoly működési gyengeséggel küzdött: a számlálók deszinkronizációjával. Ha egy felhasználó a hardveres eszköz gombját véletlenül ötször megnyomta a zsebében, a kliens számlálója elérte a $C=6$ értéket, míg a hitelesítő szerver $C=1$ értéken maradt, ami folyamatos sikertelen bejelentkezésekhez vezetett.

⏱️ Az RFC 6238 áttörése: Az eseményszámláló felváltása Epoch-idővel

Az N. M'Raihi, S. Machani, M. Pei és J. Rydell által 2011 májusában közzétett RFC 6238 a számláló deszinkronizációs problémáját úgy oldotta meg, hogy a növekményes egész szám helyére a szabványosított Unix epoch-időt állította. Mivel a fizikai idő világszerte egyenletesen telik, a kliens és a szerver egyetlen hálózati csomag elküldése nélkül, párhuzamosan lépteti előre belső számlálóját.

02. A TOTP 5 kriptográfiai és matematikai lépése

Egy 16 karakteres Base32 mag (pl. JBSWY3DPEHPK3PXP) 6 számjegyű mozgó kóddá (pl. 284915) alakítása öt pontos kriptográfiai fázison keresztül történik:

1 1. lépés: A Base32 titkos kulcs dekódolása

A regisztrációs kulcsok az RFC 4648 Base32 szabvány szerint kódoltak az A-Z és 2-7 ábécé használatával (kihagyva a félreérthető karaktereket, mint a 0, 1, 8, 9). Az algoritmus először normalizálja ezt a karakterláncot: eltávolítja a szóközöket és kitöltést (=) alkalmaz, hogy a hossza 8 többszöröse legyen, mielőtt nyers bináris kulcsbájtokká ($K$) dekódolná.

2 2. lépés: Az időlépés kiszámítása ($T$ számláló)

Az aktuális Unix időbélyeget (az 1970. január 1. UTC óta eltelt másodpercek számát) egy $T$ egész számlálóra képezzük le:

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

A standard paraméterek szerint $T_0 = 0$ és a lépés időtartama $X = 30$ másodperc. Ezt a $T$ egész számot egy 8 bájtos, big-endian formátumú, 64 bites előjel nélküli egész számként kódoljuk.

3 3. lépés: HMAC-SHA1 hash számítás

A nyers $K$ kulcsbájtokat titkos kulcsként használva kiszámítjuk a HMAC-SHA1 lenyomatot a 8 bájtos big-endian időpufferen, ami egy 20 bájtos (160 bites) üzenethitelesítési hash-t ($HS$) eredményez:

HS = HMAC-SHA-1(Key, T)
4 4. lépés: Dinamikus csonkítás (Dynamic Truncation - DT)

A 20 nyers bájt egész számmá alakításához az RFC 4226 a dinamikus csonkítást (Dynamic Truncation) írja elő:

  • Eltolás (Offset) kinyerése: Megvizsgáljuk az utolsó bájtot ($HS[19]$), és egy 0x0F maszkkal kinyerjük az alsó 4 bitjét, ami egy 0 és 15 közötti eltolási értéket ad: Offset = HS[19] & 0x0F;
  • 4 egymást követő bájt kivágása: Kinyerünk 4 egymást követő bájtot az Offset pozíciótól kezdve: $P = HS[Offset \dots Offset+3]$;
  • Előjelbit maszkolása: A legfelső bitet (MSB) 0x7F értékkel maszkoljuk az előjeles egész túlcsordulási kétértelműségének megakadályozására:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 5. lépés: Modulo redukció $N$ számjegyre

Végül a BinaryCode értéket modulo $10^6$ (1 000 000) művelettel csökkentjük a standard 6 számjegyű tokenekhez. Az eredményt balról nullákkal töltjük fel az egységes 6 karakteres kimenet biztosításához:

OTP = BinaryCode mod 10^6

03. 1. futtatható kód: Python 3 szabványos könyvtár

Ez a teljes megvalósítás a Python beépített base64, struct, hmac és hashlib moduljait használja, nulla külső pip csomagfüggőséggel:

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. 2. futtatható kód: JavaScript és Node.js Crypto

Tiszta JavaScript implementáció Node.js háttérkörnyezetekhez a natív crypto.createHmac használatával:

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 < 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”));

Kliensoldali böngészős futtatás a W3C Web Crypto API segítségével:

A 100%-ban offline biztonságra törekvő modern webes alkalmazások számára (pontosan ez az architektúra hajtja a 2FA-Tool-t is) a W3C Web Crypto API aszinkron, hardveresen gyorsított kriptográfiai primitíveket biztosít közvetlenül a böngésző biztonságos környezetében, külső npm csomagok nélkül:

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. 3. futtatható kód: Produkciós Golang (Go)

A nagy átviteli sebességű mikroszolgáltatások a Go alaposan tesztelt szabványos könyvtárának előnyeit élvezik:

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. Az RFC 6238 B függelékének tesztvektoros ellenőrzése

A szabványos megfelelőség igazolásához minden kriptográfiai megvalósítást ellenőrizni kell az IETF RFC 6238 B függelékében közzétett hivatalos tesztvektorokkal szemben.

A hivatalos tesztcsomag az "12345678901234567890" ASCII titkos kulcsot használja (20 bájt), amely Base32 formátumban a következő:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (8 számjegyű kimenetekre és 30 másodperces lépésekre kiértékelve):

Unix Epoch időbélyeg UTC dátum és idő Hivatalos elvárt RFC kód Megvalósítás eredménye
59 1970-01-01 00:00:59 94287082 ✓ SIKERES (Pontos egyezés)
1111111109 2005-03-18 01:58:29 07081804 ✓ SIKERES (Pontos egyezés)
1111111111 2005-03-18 01:58:31 14050471 ✓ SIKERES (Pontos egyezés)
1234567890 2009-02-13 23:31:30 89005924 ✓ SIKERES (Pontos egyezés)
2000000000 2033-05-18 03:33:20 69279037 ✓ SIKERES (Pontos egyezés)

07. Produkciós mérnöki szempontok: időlépés-eltérés, újrajátszás és sebességkorlátozás

Amikor a TOTP ellenőrzést nagy rendelkezésre állású vállalati hitelesítési rendszerekben vezetik be, három kulcsfontosságú biztonsági követelményt kell betartani:

1. szabály Óraeltérési toleranciaablak (Clock Drift)

A mobilkészülékek óráinak kisebb pontatlanságai miatt a szervereknek a [T - 1, T, T + 1] ablakot kell kiértékelniük (az előző 30 másodperc, az aktuális 30 másodperc és a következő 30 másodperc ellenőrzése). Soha ne tágítsa az ablakot $\pm 1$ lépésnél nagyobbra, mivel minden további lépés 30 másodperccel növeli az újrajátszásos támadási felületet.

2. szabály Egyszeri tokenhasználat kikényszerítése (újrajátszás elleni védelem)

A tokent a sikeres felhasználást követően azonnal érvényteleníteni kell. Tárolja a Last_Verified_Time_Step értéket Redisben vagy memóriabeli gyorsítótárban. Ha a kliens ugyanazt a tokent próbálja beküldeni ugyanazon a 30 másodperces ablakon belül, utasítsa el a kérést a közbeékelődéses (man-in-the-middle) újrajátszásos támadások semlegesítésére.

3. szabály Nyers erő elleni sebességkorlátozás és állandó idejű összehasonlítás

Mivel a 6 számjegyű kódok csupán 1 000 000 variációt tesznek lehetővé, a korlátozatlan végpontok nyers erővel (brute-force) gyorsan feltörhetők. Alkalmazzon fokozatos késleltetést és fiókzárolást 3 egymást követő sikertelen kísérlet után. A kódban az összehasonlításokat állandó idejű függvényekkel végezze (pl. crypto/subtle.ConstantTimeCompare Go-ban vagy crypto.timingSafeEqual Node.js-ben) az oldatorcsatornás időzítési elemzések (side-channel timing attacks) kivédésére.

08. Gyakran Ismételt Kérdések (GYIK)

1. kérdés: Mivel az SHA-1 kriptográfiailag feltörtnek számít, miért használ a TOTP alapértelmezésben továbbra is HMAC-SHA1-et? ▼

A nyers SHA-1 elleni elméleti ütközési támadások nem érvényesek a HMAC szerkezetre (RFC 2104). A HMAC esetében a mögöttes hash függvény belső és külső titkos kulcsokba van ágyazva, ami semlegesíti az ütközésalapú támadásokat. A mai napig a HMAC-SHA1 teljesen biztonságos marad a 2FA céljaira. Ezenkívül az RFC 6238 teljes mértékben támogatja a HMAC-SHA256 és HMAC-SHA512 algoritmusokat is, amennyiben szigorúbb garanciákra van szükség.

2. kérdés: Befolyásolják-e az időzónák vagy a nyári időszámítás (DST) a TOTP szinkronizációját? ▼

Nem. A Unix epoch-idő az 1970-01-01 00:00:00 UTC óta eltelt másodpercek száma. Szigorúan független az időzónáktól, a szökőmásodpercektől és a nyári időszámítás váltásaitól. Bármely adott pillanatban egy tokiói szerver és egy kaliforniai okostelefon pontosan ugyanazzal az egész számmal rendelkező időbélyeggel dolgozik.

3. kérdés: Miben különbözik a Steam Guard 5 karakteres hitelesítése az RFC 6238-tól? ▼

A Steam Guard egy szabadalmaztatott, az RFC 6238-on alapuló változat. Pontosan ugyanazt a 30 másodperces időlépést és HMAC-SHA1 számítást alkalmazza, azonban a végső redukciós fázisban a kinyert egész számot egy egyedi, 26 karakteres alfanumerikus karaktertömbön (23456789BCDFGHJKMNPQRTVWXY) keresztül képezi le, így állít elő egy 5 karakteres szöveget.

RFC 6238 szabványnak megfelelő · W3C Web Crypto API kliensoldali számítás

Próbálja ki a 100%-ban kliensoldali TOTP kódgenerálást élőben

A 2FA-Tool pontosan ezt a kriptográfiai folyamatot hajtja végre közvetlenül a böngészőjében a W3C Web Crypto API használatával. Nulla szerverre továbbítás, nulla tokentárolás, azonnali érvényesítés.