💻 Crittografia Applicata · Analisi Tecnica Approfondita

Come funziona TOTP: Implementazione dell'algoritmo RFC 6238 in Python, JavaScript e Go

2F Laboratorio di Ricerca sulla Sicurezza 2FA-Tool · Aggiornato a ottobre 2026 · 16 min di lettura (oltre 2.700 parole)
💡 Abstract architetturale e manifesto ingegneristico

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.

⏱️ La svolta di RFC 6238: Sostituire i contatori di eventi con il tempo Epoch

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:

1 Passo 1: Decodifica della chiave segreta Base32

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$.

2 Passo 2: Calcolo dell'intervallo temporale (Contatore $T$)

Il timestamp Unix corrente (secondi trascorsi dal 1° gennaio 1970 UTC) viene mappato sul contatore intero $T$:

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

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.

3 Passo 3: Calcolo dell'hash HMAC-SHA1

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):

HS = HMAC-SHA-1(Key, T)
4 Passo 4: Troncamento dinamico (Dynamic Truncation - DT)

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 0x0F per 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 0x7F per prevenire ambiguità di overflow degli interi con segno:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Passo 5: Riduzione modulo a $N$ cifre

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:

OTP = BinaryCode mod 10^6

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:

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. Codice eseguibile 2: JavaScript e Node.js Crypto

Implementazione in puro JavaScript per runtime backend Node.js che utilizza il modulo nativo crypto.createHmac:

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

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:

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. Codice eseguibile 3: Golang (Go) pronto per la produzione

I microservizi ad alto throughput traggono grande vantaggio dalla collaudata libreria standard di Go:

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. 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:

Regola 1 Finestra di tolleranza per il disallineamento temporale (Clock Drift)

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.

Regola 2 Utilizzo singolo del token (Anti-Replay)

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.

Regola 3 Limitazione della frequenza contro attacchi brute-force e confronto a tempo costante

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.

Conforme a RFC 6238 · Calcolo lato client con W3C Web Crypto API

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.