Come funziona il TOTP: l’algoritmo dietro i codici dell’app di autenticazione
Digiti un codice di 6 cifre da un’app di autenticazione un paio di volte a settimana e, in qualche modo, quel codice compare sul tuo telefono e coincide con quello che il server si aspetta, anche se i due non si parlano mai. Allora, come funziona il TOTP? La stessa chiave segreta condivisa produce un numero nuovo ogni 30 secondi, e la risposta sta in un piccolo algoritmo deterministico che entrambe le parti eseguono per conto proprio. Nessun codice viaggia sulla rete, e nessun server centrale distribuisce il numero.
Il TOTP (Time-based One-Time Password), definito in RFC 6238, trasforma una chiave segreta condivisa più l’ora corrente in un breve codice numerico calcolando un HMAC sull’ora e troncandone il risultato. Poiché l’autenticazione a due fattori (2FA) si regge sul fatto che entrambe le parti calcolino lo stesso valore senza scambiarselo, l’algoritmo è l’intero modello di fiducia.
Questa guida percorre l’algoritmo con numeri concreti, poi affronta la metà che la maggior parte delle spiegazioni salta: come un server verifica davvero un codice, e un resoconto onesto di cosa la 2FA ferma e cosa no. Mentre leggi puoi calcolare un codice dal vivo nel nostro generatore TOTP.
Cos’è davvero il TOTP?
Il TOTP (Time-based One-Time Password), definito in RFC 6238, è un algoritmo che combina una chiave segreta condivisa con l’ora corrente per produrre un breve codice che ruota a intervalli fissi. Sia l’app di autenticazione sia il server custodiscono la stessa chiave segreta, leggono lo stesso orologio ed eseguono la stessa matematica, così arrivano allo stesso codice senza mai trasmetterlo.
Soffermati su quest’ultimo punto. Il codice non viene mai inviato durante la configurazione, solo la chiave segreta lo è, e dopo ciascuna parte deriva i codici per conto proprio. Sulla rete non c’è nulla da intercettare, tranne la chiave segreta al momento dell’iscrizione e le 6 cifre che l’utente digita al login. Pensa a tre input che collassano in un unico output:
| Input | Ruolo | Valore tipico |
|---|---|---|
| Chiave segreta condivisa | La chiave a lunga durata, concordata una volta all’iscrizione | JBSWY3DPEHPK3PXP (Base32) |
| Time step | Il contatore che avanza | Finestra di 30 secondi |
| Output | Il breve codice derivato dai due | 324550 |
La chiave segreta è quasi sempre scritta in Base32 (le lettere A–Z e le cifre 2–7) perché quell’alfabeto non distingue maiuscole e minuscole e resiste a essere stampato, digitato o impacchettato in un codice QR. Iscrivi una chiave segreta scansionando una URI otpauth://, che puoi visualizzare come un QR per l’app di autenticazione, oppure digitando a mano la stringa Base32.
TOTP, HOTP, SMS e Passkey: il panorama della 2FA
Il TOTP è una delle varie opzioni, e per scegliere bene conviene avere sott’occhio tutte le alternative. La relazione chiave è semplice: il TOTP è HOTP con il contatore sostituito dal numero di time step trascorsi dall’epoch Unix. Tutto il resto è un compromesso tra resistenza al phishing, comodità e l’infrastruttura di cui hai bisogno.
| Meccanismo | Driver | Durata del codice | Resistente al phishing? | Serve la rete? | Uso tipico |
|---|---|---|---|---|---|
| HOTP (RFC 4226) | Contatore incrementale | Fino all’uso | No | No | Token hardware, sistemi legacy |
| TOTP (RFC 6238) | Ora corrente | ~30 secondi | No | No (dopo l’iscrizione) | App di autenticazione |
| SMS OTP | Il server invia un codice | Qualche minuto | No | Sì (cellulare) | Ripiego per il consumatore |
| Approvazione push | Richiesta del server a un dispositivo | Per ogni richiesta | In parte | Sì | 2FA basata su app |
| Passkey / FIDO2 | Sfida a chiave pubblica | Per ogni richiesta | Sì (vincolata all’origin) | Sì | Account moderni |
Conta lo schema in quella tabella. TOTP e HOTP funzionano offline una volta completata l’iscrizione, il che li rende resilienti e riservati, ma nessuno dei due resiste al phishing da solo: una pagina falsa convincente può chiedere il codice e ritrasmetterlo. L’SMS aggiunge un canale di rete con una sua superficie d’attacco. Le Passkey chiudono la falla del phishing vincolando la credenziale all’origin del sito, ed è la direzione verso cui sta andando il settore. Il TOTP resta robusto, disponibile ovunque e gratuito, ed è per questo che continua a essere così diffuso.
Come funziona l’algoritmo TOTP, passo dopo passo
Ecco l’intero algoritmo in quattro passaggi. Eseguiremo ciascuno con la chiave segreta di test della RFC JBSWY3DPEHPK3PXP e un’ora Unix fissa pari a 1700000000, così ogni numero è riproducibile.
- Decodifica la chiave segreta Base32 in byte grezzi della chiave.
- Calcola il contatore del time step dall’ora Unix corrente.
- Calcola l’HMAC del contatore con la chiave segreta.
- Tronca il digest fino a un codice di 6 cifre.
Passo 1 — Decodifica la chiave segreta Base32 in byte
Il Base32 impacchetta 5 bit in ciascun carattere. Il decodificatore raggruppa i caratteri di nuovo in byte da 8 bit. La chiave segreta JBSWY3DPEHPK3PXP si decodifica nei 10 byte grezzi 48 65 6c 6c 6f 21 de ad be ef. Questo array di byte, non la stringa stampabile, è la chiave HMAC.
Passo 2 — Calcola il contatore del time step
Il contatore è il numero di time step interi trascorsi da un punto di partenza: T = floor((unixTime − T0) / period). I valori predefiniti della RFC sono T0 = 0 (l’epoch Unix) e period = 30. Con unixTime = 1700000000, si ottiene T = floor(1700000000 / 30) = 56666666. Questo intero viene poi codificato come valore big-endian di 8 byte: 00 00 00 00 03 60 aa 2a. Poiché il contatore cambia solo quando inizia una nuova finestra di 30 secondi, ogni codice è stabile per la durata di una finestra e poi salta.
Passo 3 — Calcola l’HMAC del contatore con la chiave segreta
L’algoritmo calcola un HMAC-SHA1 sul contatore di 8 byte usando i byte della chiave segreta come chiave. L’HMAC è una funzione unidirezionale con chiave: senza la chiave segreta non puoi invertire il digest né forgiarne uno valido, ed è questo che rende il codice infalsificabile. Per i nostri input il digest è composto dai 20 byte 1d 70 6e 94 1a c7 6b 6d 4a 46 dd 6f af a4 5f e3 35 11 bf 86.
Passo 4 — Troncamento dinamico a un codice di 6 cifre (RFC 4226)
Un digest di 20 byte è troppo lungo da digitare, quindi il troncamento dinamico della RFC 4226 ne estrae un numero. Prendi il nibble basso dell’ultimo byte come offset: l’ultimo byte è 0x86, il suo nibble basso è 6, quindi l’offset è 6. Leggi 4 byte a partire da quell’offset (6b 6d 4a 46), azzera il bit più alto del primo per mantenere il numero positivo, e ottieni l’intero 1802324550. Riducilo modulo 10^6 e aggiungi gli zeri iniziali: 1802324550 % 1000000 = 324550. Questo è il codice che la tua app mostra per questa chiave segreta in questo istante.
Ecco l’algoritmo in JavaScript usando la Web Crypto API nativa del browser, senza dipendenze. Ogni commento associa un blocco a uno dei quattro passaggi sopra:
// TOTP per RFC 6238 — SHA-1, 6 cifre, periodo di 30s (i valori predefiniti).
async function generateTotp(base32Secret, unixTime = Date.now() / 1000) {
// Passo 1: decodifica la chiave segreta Base32 (A-Z, 2-7) in byte grezzi della chiave.
const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = '';
for (const ch of base32Secret.replace(/=+$/, '').toUpperCase()) {
bits += alpha.indexOf(ch).toString(2).padStart(5, '0');
}
const keyBytes = new Uint8Array(
bits.match(/.{8}/g).map((b) => parseInt(b, 2)));
// Passo 2: counter = numero di step da 30s dall'epoch (big-endian da 8 byte).
let counter = Math.floor(unixTime / 30);
const msg = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
msg[i] = counter & 0xff;
counter = Math.floor(counter / 256);
}
// Passo 3: calcola l'HMAC-SHA1 del contatore con la chiave segreta.
const key = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
const hmac = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg));
// Passo 4: troncamento dinamico (RFC 4226) -> codice di 6 cifre.
const offset = hmac[hmac.length - 1] & 0x0f;
const binary = ((hmac[offset] & 0x7f) << 24) | (hmac[offset + 1] << 16) |
(hmac[offset + 2] << 8) | hmac[offset + 3];
return (binary % 1_000_000).toString().padStart(6, '0');
}
const code = await generateTotp('JBSWY3DPEHPK3PXP', 1700000000);
console.log(code); // -> "324550"
Lo stesso algoritmo in Python, usando solo la libreria standard (hmac e struct):
import base64, hmac, hashlib, struct, time
def totp(secret, for_time=None, period=30, digits=6, digest='sha1'):
if for_time is None:
for_time = time.time()
# Passo 1: decodifica in Base32 la chiave segreta in byte grezzi della chiave.
key = base64.b32decode(secret.upper())
# Passo 2: counter = numero di time step dall'epoch (big-endian da 8 byte).
counter = int(for_time // period)
msg = struct.pack(">Q", counter)
# Passo 3: calcola l'HMAC del contatore con la chiave segreta.
h = hmac.new(key, msg, digest).digest()
# Passo 4: troncamento dinamico (RFC 4226) -> codice di N cifre.
offset = h[-1] & 0x0f
binary = ((h[offset] & 0x7f) << 24 |
(h[offset + 1] & 0xff) << 16 |
(h[offset + 2] & 0xff) << 8 |
(h[offset + 3] & 0xff))
return str(binary % (10 ** digits)).zfill(digits)
print(totp('JBSWY3DPEHPK3PXP', 1700000000)) # -> 324550
Entrambe le implementazioni stampano 324550 per la nostra ora fissa, ed entrambe riproducono i vettori di test ufficiali della RFC 6238 (per esempio, il vettore SHA-1 a T = 59 produce 94287082). Se sostituisci SHA-1 con SHA-256 o SHA-512, oppure cambi il numero di cifre, il verificatore dall’altra parte deve corrispondere esattamente alle stesse scelte, altrimenti i codici non coincideranno mai.
Verifica di un codice TOTP lato server
Generare un codice è metà del sistema. L’altra metà è il server che decide se accettare le 6 cifre appena digitate da un utente, e quel passaggio porta con sé tutti i compromessi delicati per la sicurezza.
Il server non memorizza i codici. Memorizza la chiave segreta e, al momento del login, ricalcola il codice atteso da quella chiave segreta e dall’ora corrente, poi confronta. Il problema è la deriva dell’orologio: il dispositivo dell’utente e il server raramente concordano sull’esatto secondo, quindi un controllo di uguaglianza rigida rifiuterebbe i codici vicini al confine di una finestra. Il rimedio è una piccola finestra di validazione. Accetta lo step corrente e uno step da entrambi i lati, il che significa controllare i codici per i contatori T−1, T e T+1. Una finestra più ampia tollera più deriva ma allarga la superficie di tentativo, perciò la finestra 1 (una tolleranza di ±30 secondi) è il compromesso comune. È la stessa tolleranza di ±1 step che trovi nella scheda Verifica del nostro strumento.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyTotp(secret, code, { window = 1, period = 30, digits = 6 } = {}) {
const counter = Math.floor(Date.now() / 1000 / period);
const submitted = Buffer.from(code);
// Controlla lo step corrente e ±window step per la deriva dell'orologio.
for (let i = -window; i <= window; i++) {
const expected = Buffer.from(totpAt(secret, counter + i, digits));
// Confronto a tempo costante così il timing non può rivelare una corrispondenza parziale.
if (expected.length === submitted.length &&
timingSafeEqual(expected, submitted)) {
return counter + i; // step corrispondente — memorizzalo per bloccare il replay
}
}
return false;
}
Altri due dettagli trasformano tutto questo da “funziona” a “sicuro”. Primo, la prevenzione del replay: memorizza l’ultimo contatore che hai accettato per ciascun utente e rifiuta qualsiasi codice proveniente da uno step pari o inferiore, così un codice intercettato una volta non può essere riutilizzato all’interno della stessa finestra. È per questo che verifyTotp restituisce lo step corrispondente invece di un semplice true. Secondo, il rate limiting: un codice di 6 cifre è uno di un milione di valori, e una finestra di ±1 ne rende validi tre in qualsiasi momento, quindi senza limitazioni un attaccante può violare lo spazio con la forza bruta. Blocca l’account o aggiungi un backoff dopo una manciata di tentativi falliti. Infine, la chiave segreta è una chiave a lunga durata, perciò cifrala quando è a riposo, tienila fuori dal controllo di versione e trattala esattamente come una password. Genera codici di recupero robusti insieme ad essa per il giorno in cui un dispositivo andrà perso.
Da cosa protegge il TOTP — e da cosa no
Il TOTP è un vero passo avanti rispetto alle sole password, ma ha i suoi limiti, e le pagine di marketing tendono a sorvolare sulle lacune. Ecco la divisione onesta.
| Il TOTP ferma | Il TOTP NON ferma |
|---|---|
| Password trapelate o riutilizzate | Phishing in tempo reale / adversary-in-the-middle |
| Credential stuffing | Malware che legge la chiave segreta da un dispositivo |
| Brute force remoto della password | Flussi di recupero account deboli che saltano la 2FA |
| Una violazione del DB che espone solo gli hash delle password | (questi richiedono altre difese) |
I vantaggi sono notevoli. Poiché il login ora richiede un codice che solo la chiave segreta può produrre, una password trapelata non basta più, il che annulla del tutto il credential stuffing e il brute force remoto. Se il tuo database trapela ma le chiavi segrete TOTP sono cifrate a riposo, l’attaccante comunque non può coniare codici.
Le lacune sono altrettanto reali. Un proxy di phishing in tempo reale (una pagina adversary-in-the-middle) può mostrare all’utente una replica perfetta, catturare il codice dal vivo e ritrasmetterlo al sito reale all’interno della stessa finestra. Il TOTP non può accorgersi che il codice è stato digitato nel posto sbagliato. Un malware sul dispositivo che esfiltra la chiave segreta lo sconfigge completamente, e un flusso di recupero “ho dimenticato la 2FA” fatto male può aggirarlo del tutto. Una confusione comune che vale la pena chiarire: gli attacchi di SIM-swap sconfiggono i codici monouso via SMS, non il TOTP — il TOTP non ha alcun canale legato al numero di telefono, quindi non c’è nulla che un attaccante possa reindirizzare.
Le Passkey e FIDO2/WebAuthn sono vincolate all’origin, perciò resistono al phishing per costruzione: la credenziale si rifiuta semplicemente di autenticarsi verso il dominio sbagliato. Tratta il TOTP come un passo avanti robusto e disponibile ovunque rispetto alle password, non come la destinazione finale. Si abbina naturalmente al resto del tuo stack di autenticazione: vedi le best practice per la sicurezza dei JWT per il livello del token di sessione che si appoggia a un login verificato, e l’hashing delle password (bcrypt vs Argon2) per il livello della password a riposo che la 2FA va a completare.
Insidie comuni nell’implementare il TOTP
La maggior parte dei bug del TOTP non sta nell’algoritmo, che è fissato dalla RFC, ma nel cablaggio attorno ad esso. Questi sono quelli che pungono chi implementa.
- Deriva dell’orologio del server. Se il server non esegue NTP, la sua idea di “adesso” si allontana da quella del dispositivo dell’utente e i codici smettono di coincidere per tutti. Abilita la sincronizzazione dell’ora di rete su ogni host.
- Chiavi segrete in chiaro o sottoposte a commit. Una chiave segreta in un file di configurazione finita in git è una backdoor permanente. Memorizzala cifrata in un secrets manager, mai nel controllo di versione.
- Nessuna protezione dal replay. Se accetti un codice senza registrare lo step corrispondente, lo stesso codice funziona di nuovo all’interno della sua finestra. Persisti l’ultimo step usato per ciascun utente e rifiuta il riutilizzo.
- Una finestra troppo ampia o troppo stretta. Troppo ampia moltiplica i codici indovinabili e indebolisce la sicurezza; troppo stretta rifiuta utenti legittimi per una deriva minima. La finestra 1 è il bilanciamento abituale.
- Disallineamento dei parametri. Se l’iscrizione codifica SHA-256 e 8 cifre nella URI
otpauth://ma il verificatore presume SHA-1 e 6 cifre, nessun codice sarà mai valido. Leggi l’algoritmo, le cifre e il periodo dalla URI e usali su entrambe le parti. - Nessun codice di backup o di recupero. Quando un telefono si perde, l’unico modo per rientrare è un percorso di recupero. Emetti codici di recupero alla configurazione, e rendili robusti quanto l’account richiede — la stessa logica dietro l’entropia delle password si applica anche ai segreti di recupero.
FAQ
Il TOTP è a prova di phishing?
No. Il TOTP ferma le password trapelate e il brute force remoto, ma un proxy di phishing in tempo reale può mostrare un login falso, catturare il codice dal vivo e ritrasmetterlo al sito reale all’interno della stessa finestra di 30 secondi. Le Passkey e FIDO2 sono il passo avanti resistente al phishing perché vincolano la credenziale all’origin del sito.
Il TOTP è più sicuro della 2FA via SMS?
Sì. I codici SMS viaggiano sulla rete cellulare e possono essere intercettati tramite SIM-swap o attacchi SS7, e dipendono dalla sicurezza del tuo operatore. Il TOTP non ha alcun canale legato al numero di telefono e non trasmette mai il codice, quindi non c’è nulla da intercettare in transito. La chiave segreta viene scambiata una sola volta, alla configurazione.
Cosa succede se perdo il telefono o l’app di autenticazione?
Hai bisogno di un backup predisposto in anticipo. Le opzioni sono i codici di recupero salvati quando configuri la 2FA, un secondo dispositivo iscritto con la stessa chiave segreta, oppure la chiave segreta Base32 originale conservata in un luogo sicuro. Senza una di queste, perdere il dispositivo significa restare bloccato fuori dall’account.
Come fa un server a verificare un codice TOTP?
Ricalcola il codice atteso dalla chiave segreta condivisa e dall’ora corrente, poi confronta il codice inviato con il time step corrente e con uno step da entrambi i lati per tener conto della deriva dell’orologio. Registra inoltre quale step ha avuto corrispondenza, così lo stesso codice non può essere riutilizzato, e limita la frequenza dei tentativi per bloccare gli indovinamenti.
Perché i codici TOTP si rinnovano ogni 30 secondi?
Trenta secondi è il periodo predefinito della RFC 6238: abbastanza lungo da leggere e digitare il codice comodamente, abbastanza breve da far scadere quasi subito un codice catturato da un attaccante. Alcuni sistemi usano un periodo di 60 secondi, che la URI otpauth:// registra in modo che il verificatore vi corrisponda.
Due dispositivi possono condividere una sola chiave segreta TOTP?
Sì. Qualsiasi dispositivo che custodisce la stessa chiave segreta Base32 con un orologio sincronizzato genera codici identici, perché l’algoritmo è deterministico. È esattamente così che funzionano i backup multi-dispositivo delle app di autenticazione, ed è anche il motivo per cui la chiave segreta deve restare privata: chiunque la copi può produrre ogni codice futuro.
Il TOTP è la stessa cosa di Google Authenticator?
No. Il TOTP è l’algoritmo aperto definito nella RFC 6238. Google Authenticator, Authy e 1Password sono app che lo implementano. Poiché lo standard è condiviso, qualsiasi app conforme funziona con qualsiasi servizio che usa il TOTP — non c’è alcun vincolo verso un fornitore in particolare.
Conclusione
Le idee fondamentali sono abbastanza brevi da tenere a mente:
- Il TOTP trasforma una chiave segreta condivisa più l’ora corrente in un codice tramite HMAC e troncamento.
- Entrambe le parti calcolano il codice in modo indipendente; non viene mai inviato sulla rete.
- Verifica con una finestra di ±1 step più protezione dal replay e rate limiting.
- Ferma gli attacchi alle password ma non il phishing in tempo reale — le Passkey chiudono quella falla.
- Mantieni gli orologi del server sincronizzati con NTP e la chiave segreta cifrata e privata.
Vuoi vedere l’algoritmo produrre numeri reali e controllare la tua finestra di verifica? Apri il generatore TOTP / 2FA per calcolare, configurare e verificare i codici interamente nel tuo browser, con la chiave segreta che non lascia mai il tuo dispositivo.