Tra i protocolli crittografici fondamentali della moderna sicurezza web, RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) si distingue per la sua semplicità matematica. Non richiede handshake di rete interattivi, nessuna infrastruttura a chiave pubblica (PKI) centralizzata e nessun passaggio attraverso il cloud. Con una sola chiave segreta Base32 condivisa e l'avanzamento costante del tempo fisico Unix, client e server calcolano token di autenticazione temporanei a 6 cifre identici ogni 30 secondi con una certezza matematica del 100%.
In questa guida completa, superiamo le librerie di terze parti a scatola chiusa (come pyotp o otplib) per costruire da zero l'intera pipeline crittografica RFC 6238 in Python, JavaScript e Go, verificando ogni implementazione rispetto ai vettori di test ufficiali IETF.
01. L'eleganza dell'autenticazione basata sul tempo
Per comprendere il funzionamento di TOTP, dobbiamo prima analizzare il suo predecessore: RFC 4226 HOTP (HMAC-Based One-Time Password).
In HOTP, il client e il server condividono un contatore di eventi $C$. Ogni volta che l'utente genera un token, il contatore viene incrementato ($C = C + 1$) e il token viene ricavato da $\text{HMAC-SHA-1}(K, C)$. Pur essendo matematicamente solido, HOTP soffriva di una debolezza operativa: la desincronizzazione del contatore. Se un utente premeva inavvertitamente il pulsante del proprio token hardware cinque volte nella tasca, il contatore del client saliva a $C=6$ mentre il server di autenticazione rimaneva fermo a $C=1$, provocando ripetuti rifiuti di accesso.
Pubblicato nel maggio 2011 da N. M'Raihi, S. Machani, M. Pei e J. Rydell, RFC 6238 ha risolto il problema della desincronizzazione del contatore sostituendo l'intero incrementale con il tempo Unix epoch standardizzato. Poiché il tempo fisico scorre uniformemente in tutto il mondo, sia il client sia il server fanno avanzare i propri contatori interni simultaneamente senza trasmettere un singolo pacchetto di rete.
02. I 5 passaggi matematici crittografici di TOTP
La trasformazione di un seed Base32 da 16 caratteri (ad es. JBSWY3DPEHPK3PXP) in un codice dinamico a 6 cifre (ad es. 284915) prevede cinque precise fasi crittografiche:
Le chiavi di registrazione dell'account sono codificate in Base32 secondo RFC 4648 utilizzando l'alfabeto A-Z e 2-7 (omettendo caratteri ambigui come 0, 1, 8, 9). L'algoritmo normalizza innanzitutto la stringa, rimuovendo gli spazi bianchi e applicando il padding (=) in modo che la sua lunghezza sia multiplo di 8, prima di decodificarla nei byte binari grezzi della chiave $K$.
Il timestamp Unix corrente (secondi trascorsi dal 1° gennaio 1970 UTC) viene mappato sul contatore intero $T$:
Con i parametri standard $T_0 = 0$ e durata del passo $X = 30$ secondi. Questo intero $T$ viene quindi codificato come intero senza segno a 64 bit big-endian di 8 byte.
Utilizzando i byte grezzi della chiave $K$ come chiave segreta, calcoliamo il digest HMAC-SHA1 sul buffer temporale big-endian da 8 byte, ottenendo un hash di autenticazione del messaggio $HS$ da 20 byte (160 bit):
Per convertire 20 byte grezzi in un intero, RFC 4226 specifica il troncamento dinamico (Dynamic Truncation):
- Estrai l'Offset: Esamina l'ultimo byte $HS[19]$ e applica la maschera con
0x0Fper estrarne i 4 bit inferiori, ottenendo un valore di offset compreso tra 0 e 15:Offset = HS[19] & 0x0F; - Estrai 4 byte consecutivi: Preleva 4 byte consecutivi a partire da Offset: $P = HS[Offset \dots Offset+3]$;
- Maschera il bit di segno: Maschera il bit più significativo (MSB) con
0x7Fper prevenire ambiguità di overflow degli interi con segno:
BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
Infine, riduci BinaryCode con operazione modulo $10^6$ (1.000.000) per i token standard a 6 cifre. Aggiungi zeri a sinistra per garantire una stringa di output sempre coerente a 6 caratteri:
03. Codice eseguibile 1: Libreria standard di Python 3
Questa implementazione completa utilizza i moduli integrati di Python base64, struct, hmac e hashlib, senza richiedere alcun pacchetto pip esterno:
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. Codice eseguibile 2: JavaScript e Node.js Crypto
Implementazione in puro JavaScript per runtime backend Node.js che utilizza il modulo nativo 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”));
Esecuzione lato client nel browser tramite W3C Web Crypto API:
Per le moderne applicazioni web che richiedono una sicurezza offline al 100% (la stessa architettura alla base di 2FA-Tool), la W3C Web Crypto API fornisce primitive crittografiche asincrone con accelerazione hardware direttamente nella sandbox del browser, senza dipendenze npm di terze parti:
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. Codice eseguibile 3: Golang (Go) pronto per la produzione
I microservizi ad alto throughput traggono grande vantaggio dalla collaudata libreria standard di 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. Verifica dei vettori di test dell'Appendice B di RFC 6238
Per dimostrare la conformità agli standard, ogni implementazione crittografica deve essere verificata rispetto ai vettori di test ufficiali pubblicati nell'Appendice B di IETF RFC 6238.
La suite di test ufficiale utilizza il segreto ASCII "12345678901234567890" (20 byte), che in Base32 si codifica come:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (valutato per output a 8 cifre con intervallo di 30 secondi):
| Timestamp Unix Epoch | Data e ora UTC | Codice atteso ufficiale RFC | Risultato dell'implementazione |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ SUPERATO (Corrispondenza esatta) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ SUPERATO (Corrispondenza esatta) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ SUPERATO (Corrispondenza esatta) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ SUPERATO (Corrispondenza esatta) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ SUPERATO (Corrispondenza esatta) |
07. Ingegneria di produzione: Skew, Replay e Limiti di frequenza
Quando si implementa la verifica TOTP in sistemi di autenticazione aziendali ad alta disponibilità, è necessario applicare tre requisiti di sicurezza fondamentali:
A causa di lievi imprecisioni dell'orologio sui dispositivi mobili, i server dovrebbero valutare la finestra [T - 1, T, T + 1] (verificando i 30 secondi precedenti, i 30 secondi correnti e i 30 secondi successivi). Non allargare mai la finestra oltre $\pm 1$ intervallo, poiché ogni intervallo aggiuntivo espande la superficie di attacco di replay di 30 secondi.
Un token deve essere invalidato immediatamente dopo il suo utilizzo riuscito. Registra Last_Verified_Time_Step in Redis o in una cache in memoria. Se un client tenta di inviare lo stesso token all'interno della stessa finestra di 30 secondi, rifiuta la richiesta per neutralizzare attacchi di replay man-in-the-middle.
Poiché i codici a 6 cifre coprono solo 1.000.000 di permutazioni, gli endpoint non protetti possono essere forzati rapidamente. Applica ritardi progressivi e blocco dell'account dopo 3 tentativi falliti consecutivi. Nel codice, esegui i confronti usando funzioni di uguaglianza a tempo costante (ad es. crypto/subtle.ConstantTimeCompare in Go o crypto.timingSafeEqual in Node.js) per neutralizzare l'analisi temporale dei canali laterali (side-channel).
08. Domande frequenti (FAQ)
Q1: Poiché SHA-1 è crittograficamente vulnerabile alle collisioni, perché TOTP usa ancora per impostazione predefinita HMAC-SHA1? ▼
Gli attacchi di collisione teorici contro SHA-1 puro non compromettono la costruzione HMAC (RFC 2104). In HMAC, la funzione hash sottostante è annidata all'interno di chiavi segrete interne ed esterne, neutralizzando gli attacchi basati su collisione. A oggi, HMAC-SHA1 rimane sicuro per il 2FA. Inoltre, RFC 6238 supporta pienamente HMAC-SHA256 e HMAC-SHA512 se è richiesto un livello di sicurezza superiore.
Q2: I fusi orari o l'ora legale influiscono sulla sincronizzazione del TOTP? ▼
No. Il tempo Unix epoch è definito come il numero di secondi trascorsi dal 1970-01-01 00:00:00 UTC. È del tutto indipendente da fusi orari, secondi intercalari e variazioni dell'ora legale. In qualsiasi momento preciso, un server a Tokyo e uno smartphone in California registrano lo stesso identico timestamp intero.
Q3: In cosa differisce l'autenticazione a 5 lettere di Steam Guard da RFC 6238? ▼
Steam Guard è una variante proprietaria basata su RFC 6238. Utilizza lo stesso identico intervallo di 30 secondi e lo stesso calcolo HMAC-SHA1, ma nella fase di riduzione finale mappa l'intero estratto attraverso un array di 26 caratteri alfanumerici personalizzato (23456789BCDFGHJKMNPQRTVWXY) per generare una stringa di 5 caratteri.
Prova la generazione di codici TOTP al 100% lato client in tempo reale
2FA-Tool implementa questa pipeline crittografica direttamente all'interno del browser utilizzando la W3C Web Crypto API. Nessuna trasmissione a server remoti, nessuna archiviazione di token, convalida istantanea.