Printre protocoalele criptografice fundamentale din securitatea web modernă, RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) se remarcă prin simplitatea sa matematică. Nu necesită negocieri interactive de rețea, nicio infrastructură centralizată de chei publice (PKI) și niciun apel către servere cloud. Cu doar o cheie secretă Base32 partajată și înaintarea constantă a timpului fizic Unix, un client și un server calculează jetoane de autentificare efemere de 6 cifre identice la fiecare 30 de secunde, cu certitudine matematică de 100%.
În acest ghid cuprinzător, ocolim bibliotecile externe de tip cutie neagră (cum ar fi pyotp sau otplib) pentru a construi de la zero întregul flux criptografic RFC 6238 în Python, JavaScript și Go, verificând fiecare implementare pe baza vectorilor de test oficiali IETF.
01. Eleganța autentificării ghidate de timp
Pentru a înțelege TOTP, trebuie mai întâi să analizăm predecesorul său: RFC 4226 HOTP (HMAC-Based One-Time Password).
În HOTP, clientul și serverul partajează un contor de evenimente $C$. De fiecare dată când utilizatorul generează un jeton, contorul crește ($C = C + 1$), iar jetonul este derivat din $\text{HMAC-SHA-1}(K, C)$. Deși robust din punct de vedere matematic, HOTP suferea de o slăbiciune operațională: desincronizarea contorului. Dacă un utilizator apăsa din greșeală pe butonul tokenului hardware de cinci ori în buzunar, contorul clientului ajungea la $C=6$, în timp ce serverul de autentificare rămânea la $C=1$, ceea ce ducea la respingeri permanente ale autentificării.
Publicat în mai 2011 de N. M'Raihi, S. Machani, M. Pei și J. Rydell, RFC 6238 a rezolvat problema desincronizării contorului înlocuind numărul întreg incremental cu timpul standardizat Unix epoch. Deoarece timpul fizic înaintează uniform pe întreaga planetă, atât clientul, cât și serverul își avansează simultan contoarele interne fără a trimite nici măcar un pachet de rețea.
02. Cei 5 pași matematici criptografici ai TOTP
Transformarea unei semințe Base32 de 16 caractere (de ex., JBSWY3DPEHPK3PXP) într-un cod dinamic de 6 cifre (de ex., 284915) implică cinci etape criptografice precise:
Cheile de înrolare furnizate la configurare sunt codificate conform RFC 4648 Base32 folosind alfabetul A-Z și 2-7 (excluzând caracterele ambigue precum 0, 1, 8, 9). Algoritmul normalizează mai întâi acest șir, eliminând spațiile libere și adăugând caractere de umplutură (=) astfel încât lungimea sa să fie multiplu de 8, înainte de a-l decodifica în octeții cheii binare brute $K$.
Timestamp-ul Unix curent (secunde scurse de la 1 ianuarie 1970 UTC) este mapat într-un contor întreg $T$:
Cu parametrii standard $T_0 = 0$ și durata pasului $X = 30$ secunde. Acest număr întreg $T$ este apoi codificat ca un întreg fără semn pe 64 de biți în format big-endian de 8 octeți.
Folosind octeții cheii brute $K$ ca cheie secretă, calculăm rezumatul HMAC-SHA1 peste bufferul de timp big-endian de 8 octeți, obținând un hash de autentificare a mesajului de 20 de octeți (160 de biți) $HS$:
Pentru a converti 20 de octeți bruți într-un număr întreg, RFC 4226 specifică trunchierea dinamică (Dynamic Truncation):
- Extragerea deplasamentului (Offset): Se inspectează ultimul octet $HS[19]$ și se maschează cu
0x0Fpentru a-i extrage cei 4 biți inferiori, obținând o valoare de deplasament între 0 și 15:Offset = HS[19] & 0x0F; - Extragerea a 4 octeți consecutivi: Se extrag 4 octeți consecutivi începând de la Offset: $P = HS[Offset \dots Offset+3]$;
- Mascarea bitului de semn: Se maschează cel mai semnificativ bit (MSB) cu
0x7Fpentru a preveni ambiguitatea depășirii întregului cu semn:
BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
În final, se reduce BinaryCode modulo $10^6$ (1.000.000) pentru jetoanele standard de 6 cifre. Rezultatul se completează la stânga cu zerouri pentru a asigura un șir de ieșire constant de 6 caractere:
03. Cod executabil 1: Biblioteca standard Python 3
Această implementare completă utilizează modulele native Python base64, struct, hmac și hashlib, fără a necesita niciun pachet extern 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. Cod executabil 2: JavaScript și Node.js Crypto
Implementare în JavaScript pur pentru mediile backend Node.js folosind modulul nativ 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”));
Execuție pe partea de client în browser prin W3C Web Crypto API:
Pentru aplicațiile web moderne care necesită o securitate 100% offline (exact arhitectura care stă la baza 2FA-Tool), W3C Web Crypto API oferă primitive criptografice asincrone, accelerate hardware, direct în sandbox-ul browserului, fără pachete npm terțe:
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. Cod executabil 3: Golang (Go) pentru producție
Microserviciile cu cerințe ridicate de debit beneficiază din plin de biblioteca standard a limbajului Go, testată riguros în medii de producție:
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. Verificarea vectorilor de test din RFC 6238 Anexa B
Pentru a dovedi conformitatea cu standardul, fiecare implementare criptografică trebuie verificată în raport cu vectorii de test oficiali publicați în IETF RFC 6238 Anexa B.
Suita oficială de testare folosește secretul ASCII "12345678901234567890" (20 de octeți), care se codifică în Base32 ca:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (evaluat pentru ieșiri de 8 cifre cu pași de 30 de secunde):
| Timestamp Unix Epoch | Dată și oră UTC | Cod oficial așteptat RFC | Rezultat implementare |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ REUȘIT (Potrivire exactă) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ REUȘIT (Potrivire exactă) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ REUȘIT (Potrivire exactă) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ REUȘIT (Potrivire exactă) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ REUȘIT (Potrivire exactă) |
07. Inginerie de producție: deviația ceasului, atacul prin reluare și limite
La implementarea verificării TOTP în sisteme enterprise de autentificare de înaltă disponibilitate, trebuie respectate trei cerințe stricte de securitate:
Din cauza unor mici inexactități ale ceasului dispozitivelor mobile, serverele ar trebui să evalueze fereastra [T - 1, T, T + 1] (verificând intervalul precedent de 30s, intervalul curent de 30s și următorul de 30s). Nu lărgiți niciodată fereastra dincolo de pasul $\pm 1$, deoarece fiecare pas suplimentar mărește suprafața de atac prin reluare (replay attack) cu încă 30 de secunde.
Un jeton trebuie invalidat imediat după o utilizare reușită. Stocați valoarea Last_Verified_Time_Step în Redis sau într-un cache în memorie. Dacă un client încearcă să transmită același jeton în cadrul aceleiași ferestre de 30 de secunde, respingeți solicitarea pentru a neutraliza atacurile prin reluare de tip man-in-the-middle.
Deoarece codurile de 6 cifre cuprind doar 1.000.000 de permutări, endpoint-urile fără limitare a ratei pot fi compromise rapid prin forță brută (brute-force). Aplicați întârzieri progresive și blocarea contului după 3 eșecuri consecutive. În cod, efectuați comparațiile folosind funcții de egalitate în timp constant (cum ar fi crypto/subtle.ConstantTimeCompare în Go sau crypto.timingSafeEqual în Node.js) pentru a împiedica analiza atacurilor pe canale laterale bazate pe timp (side-channel timing attacks).
08. Întrebări frecvente (FAQ)
Î1: Deoarece SHA-1 este considerat spart criptografic, de ce TOTP folosește în mod implicit HMAC-SHA1? ▼
Atacurile teoretice de coliziune împotriva algoritmului brut SHA-1 nu afectează construcția HMAC (RFC 2104). În HMAC, funcția hash subiacentă este inclusă în structuri cu chei secrete interne și externe, ceea ce neutralizează atacurile bazate pe coliziuni. Până în prezent, HMAC-SHA1 rămâne sigur pentru autentificarea 2FA. În plus, RFC 6238 oferă suport complet pentru HMAC-SHA256 și HMAC-SHA512 dacă sunt necesare cerințe de securitate superioare.
Î2: Fusul orar sau ora de vară (DST) afectează sincronizarea TOTP? ▼
Nu. Timpul Unix Epoch este definit ca numărul de secunde scurse de la 1970-01-01 00:00:00 UTC. Este complet independent de fusurile orare, secundele bisecte și trecerea la ora de vară. În orice moment exact, un server din Tokyo și un smartphone din California au exact același timestamp întreg.
Î3: Prin ce diferă autentificarea Steam Guard cu 5 caractere de RFC 6238? ▼
Steam Guard este o variantă proprietară bazată pe RFC 6238. Folosește exact același pas de timp de 30 de secunde și același calcul HMAC-SHA1, dar în faza finală de reducere mapează numărul întreg extras printr-un tablou alfanumeric personalizat de 26 de caractere (23456789BCDFGHJKMNPQRTVWXY) pentru a produce un șir de 5 caractere.
Experimentați generarea codurilor TOTP 100% pe partea de client în direct
2FA-Tool implementează acest flux criptografic exact direct în browserul dvs. folosind W3C Web Crypto API. Zero transmisii către server, zero stocare de jetoane, validare instantanee.