Tarp pamatinių šiuolaikinio žiniatinklio saugumo kriptografinių protokolų RFC 6238 (TOTP: Time-Based One-Time Password algoritmas) išsiskiria savo matematiniu paprastumu. Jam nereikia interaktyvių tinklo rankos paspaudimų, centralizuotos viešųjų raktų infrastruktūros (PKI) ar kreipimosi į debesis. Turėdami tik bendrą Base32 slaptąjį raktą ir nuolatinį fizinio Unix laiko judėjimą į priekį, klientas ir serveris kas 30 sekundžių su 100 % matematiniu tikrumu apskaičiuoja identiškus, trumpalaikius 6 skaitmenų autentifikavimo žetonus.
Šiame išsamiame gide mes apeiname uždaros dėžutės trečiųjų šalių bibliotekas (tokias kaip pyotp ar otplib) ir nuo nulio sukuriame visą RFC 6238 kriptografinę grandinę Python, JavaScript ir Go kalbomis, patikrindami kiekvieną įgyvendinimą pagal oficialius IETF testavimo vektorius.
01. Laiku pagrįsto autentifikavimo elegancija
Norint suprasti TOTP, pirmiausia privalome išnagrinėti jo pirmtaką: RFC 4226 HOTP (HMAC-Based One-Time Password).
HOTP protokole klientas ir serveris dalijasi bendru įvykių skaitikliu $C$. Kiekvieną kartą, kai naudotojas sugeneruoja žetoną, skaitiklis padidinamas ($C = C + 1$), o žetonas išvedamas iš $\text{HMAC-SHA-1}(K, C)$. Nors matematiškai patikimas, HOTP turėjo operacinę silpnybę: skaitiklių desinchronizaciją. Jei naudotojas netyčia penkis kartus kišenėje paspausdavo aparatinio rakto mygtuką, kliento skaitiklis pasiekdavo $C=6$, o autentifikavimo serveryje likdavo $C=1$, todėl visi vėlesni bandymai prisijungti būdavo atmetami.
Paskelbtas 2011 m. gegužę (autoriai N. M'Raihi, S. Machani, M. Pei ir J. Rydell), RFC 6238 išsprendė skaitiklių desinchronizacijos problemą, pakeisdamas didėjantį sveikąjį skaičių standartizuotu Unix epochos laiku. Kadangi fizinis laikas visame pasaulyje slenka tolygiai, tiek klientas, tiek serveris sinchroniškai atnaujina savo vidinius skaitiklius neišsiųsdami nė vieno tinklo paketo.
02. 5 TOTP kriptografiniai matematiniai žingsniai
16 simbolių Base32 pradinio kodo (pvz., JBSWY3DPEHPK3PXP) pavertimas besikeičiančiu 6 skaitmenų kodu (pvz., 284915) apima penkias griežtas kriptografines fazes:
Paslaugų teikėjų registracijos raktai koduojami pagal RFC 4648 Base32 standartą, naudojant abėcėlę A-Z ir 2-7 (atmetant dviprasmiškus simbolius, tokius kaip 0, 1, 8, 9). Algoritmas pirmiausia normalizuoja šią eilutę, pašalina tarpus ir prideda užpildymo simbolius (=), kad ilgis taptų kartotinis 8, o tada dekoduoja ją į neapdorotus binarinius rakto baitus $K$.
Dabartinė Unix laiko žyma (sekundės, praėjusios nuo 1970 m. sausio 1 d. UTC) susiejama su sveikuoju skaitikliu $T$:
Taikant standartinius parametrus $T_0 = 0$ ir žingsnio trukmę $X = 30$ sekundžių. Šis sveikasis skaičius $T$ po to užkoduojamas kaip 8 baitų didžiojo galo (big-endian) 64 bitų beženklis sveikasis skaičius.
Naudojant neapdorotus rakto baitus $K$ kaip slaptąjį raktą, apskaičiuojama HMAC-SHA1 maiša per 8 baitų didžiojo galo laiko buferį, sukuriant 20 baitų (160 bitų) pranešimo autentifikavimo maišą $HS$:
Norint 20 neapdorotų baitų paversti sveikuoju skaičiumi, RFC 4226 nurodo dinaminį apkarpymą:
- Poslinkio išskyrimas: Tikrinamas paskutinis baitas $HS[19]$ ir taikoma kaukė su
0x0F, siekiant išgauti jo žemiausius 4 bitus. Taip gaunama poslinkio reikšmė nuo 0 iki 15:Offset = HS[19] & 0x0F; - 4 baitų išpjovimas: Išskiriami 4 iš eilės einantys baitai, prasidedantys nuo poslinkio: $P = HS[Offset \dots Offset+3]$;
- Ženklo bito maskavimas: Vyriausiasis bitas (MSB) maskuojamas su
0x7F, kad būtų išvengta ženklinio sveikojo skaičiaus perpildymo dviprasmybių:
BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
Galiausiai BinaryCode redukuojamas moduliu $10^6$ (1 000 000) standartiniams 6 skaitmenų žetonams gauti. Gautas skaičius iš kairės papildomas nuliais, užtikrinant pastovų 6 simbolių išvesties ilgį:
03. Veikiantis kodas 1: Python 3 standartinė biblioteka
Šis pilnas įgyvendinimas naudoja integruotus Python modulius base64, struct, hmac ir hashlib, nereikalaudamas jokių papildomų pip paketų:
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. Veikiantis kodas 2: JavaScript ir Node.js Crypto
Grynas JavaScript įgyvendinimas serverio Node.js aplinkoms, naudojantis vietinį 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”));
Kliento naršyklės vykdymas per W3C Web Crypto API:
Šiuolaikinėms žiniatinklio programoms, reikalaujančioms 100 % autonominio saugumo (būtent tokia architektūra naudojama 2FA-Tool), W3C Web Crypto API suteikia asinchroninius, aparatūriškai spartinamus kriptografinius primityvus tiesiog naršyklės smėliadėžėje be jokių trečiųjų šalių npm paketų:
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. Veikiantis kodas 3: Gamybinis Golang (Go)
Didelio pralaidumo mikropaslaugos pasinaudoja laiko patikrinta Go standartine biblioteka:
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. RFC 6238 B priedo testavimo vektorių patikra
Siekiant įrodyti atitiktį standartui, kiekvienas kriptografinis įgyvendinimas turi būti patikrintas pagal oficialius testavimo vektorius, paskelbtus IETF RFC 6238 B priede.
Oficialus testų rinkinys naudoja ASCII raktą "12345678901234567890" (20 baitų), kuris Base32 koduotėje užrašomas taip:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (vertinama 8 skaitmenų išvestims su 30 sekundžių žingsniais):
| Unix epochos laiko žyma | UTC data ir laikas | RFC oficialus laukiamas kodas | Įgyvendinimo rezultatas |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ IŠLAIKYTA (Tikslus atitikimas) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ IŠLAIKYTA (Tikslus atitikimas) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ IŠLAIKYTA (Tikslus atitikimas) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ IŠLAIKYTA (Tikslus atitikimas) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ IŠLAIKYTA (Tikslus atitikimas) |
07. Gamybinė inžinerija: nuokrypis, atakos pakartojimu ir ribojimai
Diegiant TOTP tikrinimą didelio pasiekiamumo įmonių autentifikavimo sistemose, būtina užtikrinti tris esminius saugumo reikalavimus:
Dėl nedidelių mobiliųjų įrenginių laikrodžio netikslumų serveriai turėtų tikrinti langą [T - 1, T, T + 1] (vertinant ankstesnes 30 s, dabartines 30 s ir kitas 30 s). Niekada neišplėskite lango labiau nei $\pm 1$ žingsnis, nes kiekvienas papildomas žingsnis padidina pakartojimo atakos rizikos langą 30 sekundžių.
Žetonas turi tapti negaliojantis iškart po sėkmingo panaudojimo. Išsaugokite Last_Verified_Time_Step „Redis“ ar operatyviosios atminties talpykloje. Jei klientas bando pateikti tą patį žetoną per tą patį 30 sekundžių langą, užklausa turi būti atmetama, taip neutralizuojant priešo viduryje (MitM) atakas pakartojimu.
Kadangi 6 skaitmenų kodai turi tik 1 000 000 galimų derinių, neapsaugotos prieigos gali būti greitai apeitos grubia jėga. Nustatykite progresinius vėlinimus ir paskyros blokavimą po 3 nesėkmingų bandymų iš eilės. Kode palyginimus atlikite naudodami pastovaus laiko lygybės funkcijas (pvz., crypto/subtle.ConstantTimeCompare Go kalboje arba crypto.timingSafeEqual Node.js aplinkoje), kad užkirstumėte kelią šoninio kanalo laiko analizei.
08. Dažniausiai užduodami klausimai (DUK)
1 klausimas: Kadangi SHA-1 yra kriptografiškai pažeidžiamas, kodėl TOTP vis dar naudoja HMAC-SHA1 kaip numatytąjį? ▼
Teorinės kolizijų atakos prieš gryną SHA-1 nepažeidžia HMAC konstrukcijos (RFC 2104). HMAC maišos funkcija yra įterpta tarp vidinių ir išorinių slaptųjų raktų, kas visiškai neutralizuoja kolizijomis pagrįstas atakas. Iki šiol HMAC-SHA1 išlieka saugus 2FA paskirtims. Be to, RFC 6238 palaiko HMAC-SHA256 ir HMAC-SHA512, jei reikalingas aukštesnis saugumo lygis.
2 klausimas: Ar laiko juostos arba vasaros laikas (DST) daro įtaką TOTP sinchronizacijai? ▼
Ne. Unix epochos laikas apibrėžiamas kaip sekundžių skaičius, praėjęs nuo 1970-01-01 00:00:00 UTC. Jis nepriklauso nuo laiko juostų, keliamųjų sekundžių ar vasaros laiko pokyčių. Bet kurią konkrečią akimirką Tokijo serveris ir Kalifornijos išmanusis telefonas turi visiškai vienodą sveikojo skaičiaus laiko žymą.
3 klausimas: Kuo Steam Guard 5 raidžių autentifikavimas skiriasi nuo RFC 6238? ▼
Steam Guard yra patentuotas variantas, sukurtas RFC 6238 pagrindu. Jis naudoja tą patį 30 sekundžių laiko žingsnį ir HMAC-SHA1 skaičiavimą, tačiau galutinėje redukcijos fazėje gautas skaičius susiejamas per individualų 26 simbolių raidinį-skaitmeninį masyvą (23456789BCDFGHJKMNPQRTVWXY), sugeneruojant 5 simbolių kodą.
Išbandykite 100 % kliento pusėje veikiantį TOTP kodų generavimą gyvai
2FA-Tool įgyvendina šią kriptografinę grandinę tiesiog jūsų naršyklėje, naudodamas W3C Web Crypto API. Jokių duomenų siuntimų į serverį, jokio žetonų saugojimo, momentinis patikrinimas.