Laboratorio di Crittografia

Esercitazioni su hash, salt, HMAC e Base64

Di cosa tratta il laboratorio

Molti meccanismi di sicurezza si basano su una funzione di hash: un calcolo che da un dato di qualsiasi lunghezza ricava un valore breve e di lunghezza fissa, dal quale non si può risalire al dato di partenza. Il laboratorio mostra due contesti in cui queste funzioni si usano: la conservazione delle password nelle basi di dati e la sicurezza delle comunicazioni. Chiude con Base64, una codifica che si incontra spesso accanto a questi strumenti e che a volte viene scambiata per una forma di protezione.

Le esercitazioni si svolgono nel browser. I calcoli avvengono sul computer in uso: nessun dato inserito nella pagina viene inviato in rete.

Le quattro schede

Le schede affrontano tre argomenti. Le prime due sono strettamente collegate; la terza e la quarta si possono svolgere anche separatamente, ma richiamano concetti delle precedenti.

Conservare le password

La scheda 2 approfondisce il fattore di costo introdotto nella scheda 1.

1. Hash e salt

Come fa un server a verificare una password senza memorizzarla?

Perché una funzione di hash veloce come SHA-256 non basta, a cosa serve il salt e come funziona bcrypt.

2. Fattore di costo

Quanto deve essere lento bcrypt?

Come si regola il tempo di calcolo e perché si può aumentare nel corso degli anni.

Sicurezza delle comunicazioni

Usa la funzione di hash della scheda 1, con l'aggiunta di una chiave.

3. HMAC

Come si verifica che un messaggio non sia stato modificato e provenga da chi conosce una chiave segreta?

Perché un hash semplice non protegge da una modifica intenzionale e come il destinatario verifica il codice ricevuto.

Rappresentare i dati

Compare nella stringa di bcrypt (scheda 1) e nei token firmati con HMAC (scheda 3).

4. Base64

Che differenza c'è tra codificare e cifrare?

Come Base64 scrive i byte con caratteri di testo, quanto allunga i dati e perché chiunque può decodificarli.

Obiettivo. Capire come un server conserva le password senza memorizzarle, e perché per questo scopo non basta una funzione di hash veloce come SHA-256.

1 Funzione di hash

Una funzione di hash trasforma un input di qualsiasi lunghezza in un valore di lunghezza fissa, detto hash o digest. SHA-256 produce sempre 256 bit, mostrati qui come 64 cifre esadecimali (ogni cifra rappresenta 4 bit). L'hash viene chiamato anche impronta del dato: lo identifica senza contenerlo.

Per l'uso con le password contano due proprietà. La funzione è deterministica: lo stesso input produce sempre lo stesso hash, e questo permette al server di confrontare la password inserita all'accesso con quella registrata. È inoltre unidirezionale: dall'input l'hash si calcola subito, mentre dall'hash non è praticabile risalire all'input. Il server può quindi conservare solo gli hash, e chi sottrae l'archivio non vi trova le password.

SHA-256
—

Prova. Cambia una sola lettera della password e osserva l'hash. Cambia in quasi tutte le cifre, senza somiglianze con il valore precedente: è l'effetto valanga. Due password simili non producono hash simili, quindi l'hash non indica quanto un tentativo sia vicino alla password corretta.

2 Una funzione veloce e una lenta

L'unidirezionalità non impedisce un altro tipo di attacco. Chi ha sottratto l'archivio può scegliere password candidate (parole comuni, nomi, date, password già trapelate da altri servizi), calcolarne l'hash e confrontarlo con quelli sottratti. Il numero di tentativi non ha limiti, perché i calcoli avvengono sui computer dell'attaccante e non sul server: si parla di attacco offline.

In questo attacco conta la velocità della funzione. SHA-256 è progettato per essere veloce, perché si usa anche per controllare file di grandi dimensioni: con una sola scheda grafica si calcolano miliardi di hash SHA-256 al secondo. bcrypt è progettato per essere lento: con il fattore di costo 12, usato in questo passo, la stessa scheda ne calcola nell'ordine delle migliaia al secondo. Per l'utente che accede la differenza non si nota, perché il server calcola un solo hash; per chi prova milioni di candidati il tempo complessivo si moltiplica.

Il pulsante calcola con entrambe le funzioni l'hash della password scritta al passo 1.

Password del passo 1:

SHA-256
—
bcrypt
—

Prova. Premi il pulsante e confronta i due tempi. I valori misurati nel browser sono indicativi: dipendono dal computer e da un'implementazione JavaScript di bcrypt più lenta di quelle usate sui server. Conta il rapporto tra i due tempi.

3 Il salt

Poiché SHA-256 è deterministico, in un archivio senza salt due utenti con la stessa password hanno lo stesso hash. Con un solo calcolo l'attaccante individua tutti gli utenti che usano una password comune. Può anche preparare in anticipo gli hash delle password più diffuse (tabelle precalcolate, tra cui le rainbow table) e ricavare una password con una semplice ricerca.

Il salt è un valore casuale, diverso per ogni utente, che viene combinato con la password prima del calcolo dell'hash. bcrypt lo genera automaticamente a ogni calcolo. Il salt non è segreto: è scritto nella stessa stringa del risultato. Serve a rendere diversi gli hash di password uguali, e obbliga l'attaccante a ripetere i calcoli per ciascun utente. Le tabelle precalcolate, costruite senza conoscere il salt, non sono più utilizzabili.

Anche qui si calcola l'hash della password del passo 1. Ogni pressione del pulsante aggiunge una riga alla tabella, con la password usata e i due risultati.

Password del passo 1:

Prova. Premi più volte il pulsante senza cambiare la password. La colonna SHA-256 resta invariata, quella bcrypt cambia a ogni riga perché cambia il salt. Per ridurre l'attesa, in questo passo bcrypt usa il fattore di costo 10.

4 Verifica di una password con bcrypt

Se l'hash cambia a ogni calcolo, il server deve comunque poter verificare la password all'accesso. Per farlo legge dalla stringa memorizzata il fattore di costo e il salt, ricalcola l'hash della password inserita con gli stessi parametri e confronta il risultato con la parte finale della stringa. I due valori coincidono solo se la password è quella registrata.

Qui la stringa memorizzata dal server è l'ultima calcolata al passo 2 o 3. Il campo della password inserita all'accesso parte con la stessa password: modificala per simulare un accesso con una password sbagliata.

Calcola prima una stringa bcrypt al passo 2 o 3.

Prova. Premi «Verifica» con la password proposta, poi cambiane un solo carattere e verifica di nuovo.

5 Com'è fatta la stringa bcrypt

La stringa prodotta da bcrypt contiene tutto ciò che serve alla verifica. Il simbolo $ separa la versione dell'algoritmo e il fattore di costo. Seguono, senza separatore, il salt e l'hash, che si distinguono per posizione: i primi 22 caratteri sono il salt, i successivi 31 l'hash. Salt e hash sono scritti in una variante di Base64, la codifica trattata nella scheda 4.

Calcola prima una stringa bcrypt al passo 2 o 3.
versione fattore di costo salt (22 caratteri) hash (31 caratteri)
La libreria JavaScript usata da questa pagina produce la versione $2a$. Le librerie più recenti usano $2b$: l'algoritmo è lo stesso, la revisione corregge un difetto di alcune implementazioni.

In sintesi

Per conservare le password servono due misure insieme. Il salt rende diversi gli hash di password uguali e rende inutili i calcoli fatti in anticipo. La lentezza dell'algoritmo limita il numero di tentativi che l'attaccante può fare contro ciascun utente. Aggiungere un salt a SHA-256 risolve solo il primo problema, perché la funzione resta veloce; bcrypt li risolve entrambi.

Obiettivo. Capire come si regola la lentezza di bcrypt con il fattore di costo, e perché questa regolazione permette di mantenere la protezione negli anni.

1 Il fattore di costo

Al proprio interno bcrypt ripete un'operazione per un numero di volte pari a 2 elevato al fattore di costo. Aumentare il fattore di 1 raddoppia le ripetizioni e quindi il tempo: il costo 11 richiede il doppio del tempo del costo 10, il costo 12 il quadruplo. Il tempo cresce quindi in modo esponenziale rispetto al fattore.

2 Misurare la crescita

Ogni misura aggiunge una riga alla tabella, ordinata per costo. La barra mostra il tempo in proporzione alla misura più lunga; l'ultima colonna riporta il rapporto con il costo immediatamente inferiore, quando è stato misurato.

CostoTempo (ms)RapportoConfronto

Prova. Misura i costi da 8 a 14, uno dopo l'altro, e osserva la colonna del rapporto: dovrebbe essere vicina a 2. Con i costi più bassi le misure sono meno regolari, perché i tempi sono brevi e pesano di più le altre attività del browser.

3 La scelta del valore

Il fattore si sceglie in base al server: il valore più alto che consente di verificare una password in un tempo accettabile per l'utente, di solito nell'ordine di qualche centinaio di millisecondi. Le raccomandazioni OWASP indicano per bcrypt un fattore di costo di almeno 10.

Il fattore è scritto in ogni stringa bcrypt, quindi gli hash già memorizzati restano verificabili anche dopo un aumento. Il server può ricalcolare l'hash con il nuovo costo al successivo accesso dell'utente, l'unico momento in cui dispone della password in chiaro.

In sintesi

Il fattore di costo rende bcrypt adattabile nel tempo. Quando la potenza di calcolo disponibile aumenta, basta incrementare il fattore: ogni unità in più raddoppia il lavoro dell'attaccante, senza cambiare algoritmo né formato degli hash. Per l'utente che accede il costo resta quello di un singolo calcolo.

Nelle schede 1 e 2 la funzione di hash è servita a conservare le password nella base di dati di un servizio: il dato da proteggere restava sul server, e il pericolo era che qualcuno sottraesse l'archivio. Le funzioni di hash si usano anche nella sicurezza delle comunicazioni, dove il problema è diverso. Un messaggio attraversa una rete e passa per dispositivi che né il mittente né il destinatario controllano; lungo il percorso può essere letto, ma anche modificato.

Chi riceve un messaggio deve poter controllare che il contenuto sia arrivato come è stato scritto e che l'abbia scritto chi dichiara di averlo fatto. La cifratura, che rende il contenuto illeggibile a chi non ha la chiave, da sola non basta: non impedisce che il messaggio venga alterato. Per questo controllo si usano strumenti specifici, e HMAC è uno dei più diffusi. Si trova, per esempio, nelle richieste firmate inviate alle API dei servizi web, nelle notifiche automatiche che un servizio invia a un altro (webhook), nei token di accesso JWT e all'interno del protocollo TLS, che protegge le connessioni HTTPS.

Obiettivo. Usare una funzione di hash con una chiave segreta per controllare che un messaggio non sia stato modificato (integrità) e che provenga da chi conosce la chiave (autenticità).

1 Perché un hash semplice non basta

Un hash inviato insieme a un messaggio permette al destinatario di accorgersi di un errore di trasmissione: ricalcola l'hash del messaggio ricevuto e lo confronta con quello allegato. Non protegge invece da una modifica intenzionale. Chi intercetta il messaggio può cambiarlo e calcolare il nuovo hash, perché SHA-256 non richiede alcun segreto; il destinatario riceve così un messaggio alterato accompagnato da un hash corretto.

HMAC (Hash-based Message Authentication Code) introduce una chiave segreta condivisa da mittente e destinatario. Il codice dipende dal messaggio e dalla chiave: senza la chiave non si può calcolare il codice corretto per un messaggio modificato. HMAC non è la semplice concatenazione di chiave e messaggio: applica la funzione di hash due volte secondo uno schema standard (RFC 2104), che evita debolezze note della concatenazione.

SHA-256 del messaggio (calcolabile da chiunque)
—
HMAC-SHA256 (richiede la chiave)
—

Prova. Cambia l'importo da 100 a 900. Cambiano sia l'hash sia l'HMAC, ma solo il primo può essere ricalcolato da chi non conosce la chiave. Cambia poi la chiave: lo stesso messaggio produce un HMAC diverso.

2 La verifica del destinatario

Il mittente invia il messaggio insieme al suo HMAC. Il destinatario ricalcola l'HMAC del messaggio ricevuto con la propria copia della chiave e lo confronta con quello ricevuto. Se i due valori coincidono, il messaggio non è stato modificato ed è stato prodotto da chi possiede la chiave.

HMAC ricalcolato dal destinatario
—
—

Prova. Simula un attacco modificando il messaggio ricevuto, per esempio l'importo: la verifica non riesce. Ripristina il messaggio e cambia invece la chiave del destinatario: anche in questo caso la verifica non riesce.

In sintesi

HMAC garantisce l'integrità e l'autenticità di un messaggio, ma richiede che mittente e destinatario condividano la stessa chiave prima di comunicare. Come scambiarsi la chiave in modo sicuro è il problema della distribuzione delle chiavi, che la crittografia asimmetrica affronta. Inoltre, poiché la chiave è nota a entrambi, un HMAC non permette di dimostrare a un terzo quale dei due abbia scritto il messaggio.

Obiettivo. Capire che Base64 è una codifica reversibile, che cambia la rappresentazione dei dati senza proteggerli, e distinguerla dalla cifratura.

1 Rappresentare byte con caratteri di testo

Molti canali sono stati progettati per trasportare testo: la posta elettronica, gli indirizzi web, formati come JSON. I dati binari, per esempio un'immagine, una chiave o l'output di una funzione di hash, contengono byte che in questi canali possono essere alterati o interpretati come caratteri di controllo. Base64 riscrive qualsiasi sequenza di byte usando 64 caratteri stampabili: le lettere maiuscole e minuscole, le cifre, + e /.

—

Prova. Scrivi nel riquadro e osserva la codifica. In UTF-8 le lettere accentate occupano due byte e il simbolo € tre: il conteggio dei byte lo mostra.

2 La lunghezza aumenta di circa un terzo

Per distinguere 64 simboli bastano 6 bit. Base64 divide quindi i dati in gruppi di 6 bit invece che di 8: ogni gruppo di 3 byte (24 bit) diventa 4 caratteri, e il risultato è circa un terzo più lungo dell'originale. Quando il numero di byte non è un multiplo di 3, la codifica si completa con uno o due caratteri =. Per testi di pochi byte l'aumento percentuale è maggiore, perché questi caratteri di riempimento pesano di più.

Prova. Aggiungi o togli un carattere alla volta e osserva la percentuale e il numero di = finali.

3 La decodifica non richiede segreti

La decodifica ricostruisce esattamente i byte originali senza bisogno di alcuna chiave. Questa è la differenza con la cifratura: un testo cifrato si riporta in chiaro solo conoscendo la chiave, un testo in Base64 può essere decodificato da chiunque. Un esempio: nell'autenticazione HTTP Basic il browser invia nome utente e password codificati in Base64. Senza HTTPS, chi intercetta la richiesta li legge dopo una decodifica immediata.

—
—

4 La variante per gli URL

Negli indirizzi web i caratteri + e / hanno un significato proprio, e = separa i parametri dai loro valori. La variante Base64url sostituisce + con - e / con _ ed elimina gli = finali, così la stringa può essere inserita in un URL senza modifiche. È la variante usata, per esempio, nei token JWT, che possono essere firmati con un HMAC come quello della scheda 3.

—

In sintesi

Codificare non è cifrare. Base64 cambia la rappresentazione dei dati per farli passare attraverso canali di solo testo, ma non li nasconde. Una password o un token in Base64 vanno trattati come se fossero in chiaro.