Archiviare le password in sicurezza con KDF/password-hashing (Trilogia Della Password – 3 di 3)
![]()
Dopo aver capito come creare password inviolabili anche avendo a disposizione tutta l'energia termica dell'universo, pensiamo al modo migliore per archiviarle.
Pensare di lasciare la password raw in un database, rappresenta un grosso rischio in virtù di un possibile attacco offline.
- Rainbow Table
- Come ti blocco la Rainbow Table: Il “Salt” (sale)
- Il livello successivo: Il Pepper
- Final step: L’hashing
- Un piccolo esempio: hashing di una password con Argon2
- Gestione del pepper
- HSM
- Key / Algo Rotation
Una prima linea di difesa consiste nel memorizzare il digest della password, una stringa alfanumerica univoca generata da un apposito algoritmo, così da lasciare nelle mani dell'attaccante degli oggetti che, per la loro non invertibilità, non permettono di risalire alle password.
La scelta dell'algoritmo di hashing diventa critica al fine di scongiurare altri tipi di attacchi. Ad es. SHA-256, pur essendo ottimo e molto efficiente per il digest e la firma anche di file di grandi dimensioni, mostra il fianco, proprio in virtù della sua velocità, nel caso di attacchi con:
- brute-force: si calcola l’hash di password casuali fino a trovare una corrispondenza,
- dizionario: un'alternativa intelligente alla forza bruta. Si punta alle password più comuni, si calcola l'hash e si controlla se c'è corrispondenza.
- Rainbow Table: l'attacco al dizionario più insidioso di tutti
Oltre al fatto che, la funzione di hashing , essendo deterministica, permette di capire chi sono gli utenti che hanno la stessa password, dal momento che avranno lo stesso digest.
Rainbow Table
Una Rainbow Table è un enorme dizionario pre-calcolato che contiene:
- Milioni di password comuni.
- Il relativo hash corrispondente.
Invece di calcolare l’entropia di ogni tentativo, l’attaccante ruba il database degli hash e fa un semplice “Cerca e Trova”. Se l’hash della tua password è nella tabella, la tua password è violata in millisecondi, indipendentemente da quanto fosse alta la sua entropia teorica.
Oss.: Una password totalmente casuale, generata da un CSPRNG affidabile, con un'alta entropia (>120), rimarrebbe comunque inviolabile anche dalla rainbow table perché la probabilità che quella password si trovi nel dizionario, sarebbe equivalente ad indovinarla.
Come ti blocco la Rainbow Table: Il “Salt” (sale)
Per rendere inutili le Rainbow Table, i sistemi sicuri utilizzano il Salt. Il salt è una stringa di dati casuali (generati da una sorgente d’entropia affidabile ovviamente) che viene aggiunta alla password prima di calcolarne il digest.
In questo modo le rainbow table vengono vanificate perché gli hash precalcolati sulle password raccolte, mancando il salt, non valgono più. Anche se due utenti avessero la stessa password, avrebbero degli hash completamente diversi.
Anche per questo motivo non è un problema che il salt sia pubblico, perché il suo obiettivo non è nascondere quello che è un pezzetto di password a tutti gli effetti, ma di impedire economie di scala degli attacchi perché, pur potendo disporre offline di un database di decine di milioni di utenti, gli hash della mia Rainbow Table (che può arrivare a pesare anche decine di GB) andrebbero tutti ricalcolati per ogni utente, con un costo computazionale e di archiviazione inimmaginabile.
Per riassumere, gli ingredienti di base sono:
- una buona sorgente d’entropia: una fonte di casualità certificata per generare un salt unico;
- entropia della password: sempre buona norma, ove possibile, come sappiamo ormai fare (https://noblogo.org/aytin/come-generare-una-password-o-un-keyfile-sicuri-trilogia-della-password-1-di). Evita attacchi brute-force o al dizionario in cui l’attaccante prova a indovinare;
- salt: protegge la password dagli attacchi basati su database pre-calcolati (Rainbow Table).
Il livello successivo: Il Pepper
È vero che col salt andiamo a complicare lo sfruttamento di un attacco offline ma possiamo fare di meglio.
Il punto d'attenzione è che il salt protegge le password di tutti gli utenti. Ma un attaccante potrebbe non essere affatto interessato a violare ogni singolo utente (niente economia di scala) ma solo alcuni. E allora l'attacco attraverso Ranbow Table potrebbe essere di nuovo praticabile.
Ma gli informatici sono dei gran giocherelloni, si sa. Visto che abbiamo già il “sale”, perché non finire aggiustando con un po’ di “pepe”? Detto, fatto!
Il pepper, come il salt, è un'altra password generata con gli stessi criteri del salt ma le analogie finiscono qua perché:
- a differenza del salt che si trova nel database, il pepper è separato da ques'ultimo. L'ideale sarebbe un HSM;
- Il salt è visibile a tutti, attaccante compreso. Il pepper è segreto. Un eventuale data breach che permette all'attaccante di disporre offline di tutto il database degli utenti, “vedrà” certamente gli eventuali salt ma sarà ignaro del fatto che gli mancherà sempre un pezzo di chiave;
- il salt è diverso per ogni utente, il pepper, di solito, è unico;
- il salt serve a rendere uniche la password degli utenti, il pepper protegge l'intero database da attacchi offline.
Il pepper è la chiave di un HMAC, o di un meccanismo di cifratura simmetrica, applicato al digest della password (che ricordo essere salt+password in realtà), che sarà ciò che verrà archiviato.
Va detto che l'uso del “pepper” complica ulteriormente lo scenario di archiviazione. Nella stragrande maggioranza dei casi è sufficiente scegliere un buon algoritmo di password hashing (vedi paragrafo successivo) per scoraggiare gli attaccanti. “Pepare” le password prevederebbe, come detto sopra, l'uso di un HSM per es, e tutta una serie di riflessioni di contorno che evidenzierò più avanti.
Final step: L'hashing
L'ultimo punto da dettagliare è l'hash della password.
L'hash crittografico, in uso in questi casi, deve soddisfare le seguenti proprietà:
- resistenza alla pre-immagine: dato un hash h, deve essere impossibile trovare una password p t.c. H(p) = h (non invertibilità della funzione hash)
- resistenza alla pre-immagine secondaria: dato una password p1, deve essere impossibile trovare un'altra password p2 t.c. H(p1) = H(p2) (resistenza debole alle collisioni)
- resistenza alle collisioni: è impossibile trovare due password diverse, p1 e p2, t.c. H(p1) = H(p2) (resistenza forte alle collisioni)
- effetto valanga: il cambio di un solo bit della password deve cambiare radicalmente l'intero hash
In un sistema moderno, l'hash non può essere delegato a funzioni di tipo SHA perché nascono per altri compiti,
SHA-2 e SHA-3 nascono per il digest veloce, per verificare l'integrità di file anche molto grossi o firmare documenti. La loro eccellente velocità diventa il loro più grosso difetto quando si parla di password. Negli scenari precedenti di attachi offline, l'hacker che dispone di una grossa potenza di calcolo, può ricostituire velocemente le rainbow table per n utenti. Magari non di tutti ma di quelli attenzionati.
Le funzioni di derivazione della chiave (KDF) come pbkdf2 e quelle ancora più estreme come B/Scrypt, Argon2, oltre che soddisfare tutti i punti precedentemente elencati tipici di funzioni di password hashing, sono progettate per essere computazionalmente pesantissime da calcolare perché il loro scopo non è il digest ma la protezione di un segreto contro il brute-force. E mentre le vecchie KDF come pbdfk2 sono CPU bound, ma non GPU bound, le KDF più moderne come Bcrypt, Scrypt ma soprattutto Argon2, agiscono pesantemente su tempo, memoria e parallelismo e l'attacco offline di cui sopra diventa impraticabile.
PBKDF2
È il decano delle KDF. Applica iterativamente una funzione pseudorandomica, come HMAC con uno SHA, con salt alla password. Il conteggio delle iterazioni è un parametro configurabile.
PBDKF2 è uno standard di lunga data ampiamente adottato. Se non ci sono necessità stringenti di sicurezza o requisiti legacy, è una buona scelta.
Il fatto di essere solo CPU bound però non la rende la scelta ideale in scenari dove gli attaccanti possono attingere a risorse di calcolo considerevoli
Bcrypt
Basato su Blowfish, anche Bcrypt usa un hash crittografico sulla password con parametri il salt e un fattore di costo.
Il fattore di costo aumenta esponenzialmente il numero di iterazioni per adattarsi all'aumento di potenza di calcolo dell'hardware.
Bcrypt è stato progetto per essere lento e resistente a semplici attacchi di forzat bruta. Tuttavia, il basso utilizzo di ram richiesto dal calcolo lo rendono poco resistente ad attacchi sferrati usando hardware specializzato.
Bcrypt ha dalla sua una storia solidissima in ragione della quale da 20 anni a questa parte non sono state trovate vulnerabilità critiche nel suo design.
Per questo motivo Bcrypt cifra le password di sistema di OpenBSD dal 1999, come pure ha cifrato quelle di tante distro Linux per anni, prima che passassero ad Argon2 o yescrypt (default di Fedora).
Domina nei framework web ([Python] Django, [Ruby] Ruby on Rails, [PHP] Laravel), [Java] Spring, Node.js), nelle applicazioni (Ansible / Terraform, Docker), nel web (la cifratura in .htpasswd di Apache e Nginx) visto che la sua semplcitià di implementazione gli ha permesso di trovarsi praticamente in ogni linguaggio.
È presente come alternativa anche nei password manager benché molti di essi abbiano spostato il default verso Argon2 o PBKDF2 per conformità agli standard FIPS.
È molto semplice implementare e anche da usare perché bisogna agire solo sul fattore di costo (consigliato almeno 10-12, altrimenti diventa troppo vulnerabile ad attacchi sferrati attraverso la GPU)
Scrypt
Rilasciato nel 2009, Scrypt è stato il primo algoritmo a introdurre il concetto di Memory Hardness ed è stato progettato per rendere economicamente poco conveniente il ricorso ad hardware specializzato come gli ASIC o i FPGA e incidere pesantemente su CPU, ram e parallelismo.
Il suo alveo principale sono state le cripto-valute, molte monete lo usano per il mining.
Scrypt lo troviamo in quasi tutti i linguaggi di programmazione, in Tarsnap, servizio di baclup online creato dallo stesso autore di Scrypt, è stato usato da LastPass ed è presente come opzione in VeraCrypt per derivare la chiave dalla password. Fino ad Android 9 era l'algoritmo usato per la FDE del dispositvio (passato poi al FBE) . Presente anche su FreeBSD come opzione per la cifratura delle password di sistema e come opzione su LUKS per la cifratura degli slot delle chiavi.
Su Scrypt i parametri da configurare sono:
- Costro CPU/Memoria (N): un parametro che aumenta i costi computazionali di cpu e memoria
- DImensione del blocco ®: influenza la larghezza di banda della memoria
- Parallelizzazione (p): indica quanto deve incidere sul calcolo parallelo
In questo modo riesce ad essere sia CPU bound che GPU bound che, a differenza di Bcrypt, lo rende resistente anche ad attacchi facenti uso di hardware specializzato..
Di contro, in ambiente in cui siamo vincolati dalle risorse disponibili, la sua potenza diventa un fattore limitante. Quasi paragonabile ad Argon2 in quanto a robustezza, il suo unico tallone d'Achille è la permeabilità ad attacchi di tipo side-channel.
yescrypt
Piccola menzione per yescrypt, appartenente alla famiglia “Scrypt”, pensato per essere ancora più resistente di Scrypt agli attacchi GPU e FPGA ma con una gestione più intelligente delle risorse.
Grazie alle sue peculiarità, di fatto, è diventato il successore spirituale di Bcrypt nei sistemi operativi gnu/linux dove, a cominciare da Fedora, passando per Debian, Ubuntu, Arch, Kali, è il default per la cifratura delle password di sistema in /etc/shadow.
È talmente incardinato ormai nei sistemi operativi, che è la libreria libxcrypt di yescrypt a gestire la tipica funzione crypt() di C che è la base della crittografia su tutti i sistemi gnu/linux moderni.
La sua robustezza unita alla gestione intelligente delle risorse lo rende un coltellino svizzero di riferimento utile per es. per versione custom di LUKS su sistemi embedded, che magari fanno uso di cpu meno recenti, oppure come opzione per strumenti di backup specialistici
Di fatto, sui sistemi operativi, yescrypt s'è guadagnato un consenso amplissimo dovuto alla sua scalabilità, alla sua capacità di usare anche la ROM per rendere il cracking ancora più difficile e senza pesare sulla RAM e alla sua compatibilità potendosi inserire perfettamente nella storica funzione crypt() di C come detto prima.
Se Argon2 è il vincitore accademico avendo vinto il Password Hashing Competition del 2015, yescrypt per la sua robustezza, efficienza e flessibilità si ritaglia un profilo di indispensabilità nei sistemi operativi,
Argon2
E veniamo al dominatore indiscusso di questa che non è una llista esaustiva di KDF.
Argon2 è LO standard moderno per il password hashing raccomandato da OWASP e IETF.
È il riferimento per praticamente ogni password manager: Bitwarden, KeppasXC, 1Password, a cui assegnanp la protezione della Master Password
È la scelta principale per la cifratura degli hard disk anche con impostazioni molto aggressive, in ragione delle quali un ritardo di mezzo secondo (un tempo enorme se venisse scalato esponenzialmente) nell'apertura di un HD è assolutamente accettabile. È il default di LUKS2 (LUKS1 usava PBKDF2) e di VeraCrypt, con cui ha sostituito SHA-512.
Come Bcrypt, è implementato estensivamente su praticamente ogni frameword web e backend, da PHP, Django (Python), Laravel fino a Node.js.
Nei sistemi operativi, laddove yescrytpt domina nella gestione delle password utente, Argon2 è usato per compiti più critici. Dal kernel Linux per gestire internamente le chiavi crittografiche o da macOS / iOS, dove algoritmi proprietari ispirati fortemente ad Argon2, proteggono i dati nel Secure Enclave.
Argon2 setta 3 parametri principali per regolare la sua forza:
- t: iterazioni, quante volte vengono rimescolati i dati (default Bitwarden = 3)
- m: memoria, quanta ram deve occupare il calcolo. Questa è la misura anti-GPU (default Bitwarden = 16 (64MB))
- p: parallelismo, quanti core della cpu usare. Questa è la misura anti-CPU (default Bitwarden = 4)
La variante id è anche resistente agli attacchi side-channel perché impediscono a un attaccante di capire la password osservando i tempi di accesso alla memoria.
Un piccolo esempio: hashing di una password con Argon2
Il grosso vantaggio degli algoritmi di kdf è che sono naturalmente resilienti rispetto all'evoluzione tecnologica che produce macchine con sempre maggiore potenza di calcolo. Da pbkdf2 in poi, il salt implicito che invalida le rainbow table precalcolate e la possibilità di calibrare il key stretching in moda da agire intensivamente su ram e cpu, permettono all'algoritmo di adeguarsi per conservare la sua robustezza.
Mini-script per l'hashing di una password fornita dall'utente con argon2 settato al default di Bitwarden:
echo -n "Password: "; read -s PASSWORD
# Genero un Salt casuale di 128 bit
SALT=$(openssl rand -base64 128)
PASSWORD_HASH=$(echo "${PASSWORD}" | argon2 "${SALT}" -m 16 -t 3 -p 4 -id -e)
PASSWORD_HASH e SALT sono i dati che verranno archiviati e, poiché argon2 “frulla” la password con un salt, è praticamente impossibile risalire alla password originale.
La verifica è tuttavia banale perché, avendo il salt e la password da verificare, si ricrea l'hash con argon2 e si confronta con l'hash memorizzato.
Per maggior sicurezza salt e digest possono essere memorizzati in punti differenti. L'importante è che possano essere recuperate a partire dall'utente.
Gestione del pepper
Col pepper le cose cambiano un pochino perché:
- deve essere archiviato con tutte le paranoie possibili in un punto diverso dal database degli utenti
- il key rotation del pepper non è banale
Mini-script che mostra come applicare salt e pepper all'hashing di una password:
# L'utente inserisce la password
echo -n "Password: "; read -s PASSWORD
# Genero un Salt casuale di 128 bit unico per ogni utente
SALT=$(openssl rand -base64 128)
# Anche PEPPER sarà qualcosa del tipo "openssl rand -base64 128"
# e si troverà in un punto esterno al database degli utenti.
PEPPER=$(get_pepper_from_ext)
# Digest della password+salt
PASSWORD_HASH=$(echo "${PASSWORD}" | argon2 "${SALT}" -m 16 -t 3 -p 4 -id -e)
# HMAC del digest con PEPPER come chiave
PASSWORD_PEPPER=$(echo "${PASSWORD_HASH}" | openssl dgst -sha256 -hmac "${PEPPER}" -binary | base64)
HSM
Quella vista prima è una versione molto edulcorata di ciò che avviene nella realtà. Il pepper, non può essere gestito con leggerezza visto che è un segreto che protegge non un singolo oggetto ma intere classi, come db di utenti.
L'apparato che gestisce chiavi di questo tipo e di questa importanza, deve essere robusto, praticamente inattaccabile, quasi completamente isolato dal resto dei sistemi a meno delle applicazioni, e solo di quelle, che hanno il permesso di richiedere una chiave,
Apparati hardware specializzati che assolvono a tutte queste funzioni e anche di più, sono gli HSM (Hardware Security Module) che garantiscono il ciclo di vita delle chiavi, dalla generazione alla distruzione, includendo versionamento, rotazione e backup. Sono concepiti per resistere anche a manipolazioni forzate che possono innescare un meccanismo di autodistruzione e, particolare rilevante, le operazioni crittografiche basate sulle chiavi protette vengono svolte dall'hsm che consegna al client il risultato delle operazioni, non le chiavi. Nel nostro caso, l'HSM dovrebbe restituirci l'hmac del digest della password che gli inviamo.
Key / Algo Rotation
Cosa succede se cambio pepper o algoritmo (anche la sua configurazione)? Non avendo disponibilità in alcun modo della password dovrò adottare una strategia ad-hoc. Fra tutti gli scenari possibili, il miglior compromesso fra sicurezza e comodità secondo me, è quello basato sul wrapping.
È necessario innanzitutto che vengano conservate le versioni delle chiavi per i servizi che le richiedono. E a questo dovrebbe pensarci l'HSM, se ce n'è uno o qualcosa di custom che abbia funzionalità analoghe. Inoltre dovrebbero esserci dei flag che indichino quali sono gli utenti a cui sono state applicate le nuove configurazioni.
Caso A: Algo rotation
Supponiamo che l'algoritmo di hashing venga cambiato o vengano cambiate le sue configurazioni.
Premessa: Nel mio DB degli utenti, in corrispondenza di ogni utente, avrò:
- il digest della password “pepato”: HMAC ( pepper, HASH ( salt, password ) )
- il salt
Il wrapping: La strategia sarà quello di “avvolgere” la password di ogni utente col nuovo algoritmo, settare un qualche flag che mi indichi l'operazione compiuta e archiviare il tutto.
- Imponiamo il nuovo algoritmo a tutti gli utenti “imbustando” il digest attuale (in questo caso 'HMAC in realtà, visto che abbiamo a che fare anche col pepper) con il nuovo digest HASH_NEW: HASH_NEW ( salt_new, HMAC ( pepper, HASH ( salt, password ) ) ).
- Per ogni utente averemo dunque:
- il nuovo digest al posto di quello vecchio,
- il nuovo salt
- il vecchio salt
- Settiamo il flag del cambio algoritmo a true (o quello che è)
- Quando l'utente effettuerà il login con successo e il flag sarà a “true”, abbiamo la password che ci permetterà di eliminare il vecchio “involucro” e ripristinare l'HMAC del nuovo digest: HMAC ( pepper, HASH_NEW ( salt_new, password ) ) e il flag ritornerà a “false“
Considerazioni:
- La sicurezza non viene compromessa perché il digest di un digest, con KDF configurate a dovere, non comporta alcun rischio.
- La fase di verifica è quella che si complica di più perché in base al valore del flag, dovrà essere effettuata in maniera differente.
- Se il flag è “true” (nella nostra convenzione), dopo il login devo avere gli elementi per calcolare il digest in questo modo: HASH_NEW ( salt_new, HMAC ( pepper, HASH ( salt, password ) ) ).
- Se il flag è a false, calcolerò al solito: HMAC ( pepper, HASH_NEW ( salt_new, password ) )
Caso B: Pepper rotation
Supponiamo che a ruotare sia il pepper. Procediamo sempre con il wrapping massivo su tutti gli utenti incapsulando il digest :
HMAC ( pepper, HASH ( salt, password ) )
con quello nuovo:
HMAC ( pepper_new, HMAC ( pepper, HASH ( salt, password ) ) )
mettendo il flag a “true”.
Come prima, una volta che gli utenti cominceranno a fare il login, se il flag è “true” innanzitutto verificherò che:
HMAC ( pepper_new, HMAC ( pepper, HASH ( salt, password ) ) )
sia uguale a ciò che è stato archiviato. Se così fosse, ora che sono di nuovo in possesso della password, ripristinerò l'HMAC con:
HMAC ( pepper_new, HASH ( salt, password ) )
memorizzandolo al posto di quello vecchio e rimettendo il flag a false.
Considerazioni: La modifica massiva delle password degli utenti, stavolta passa dall'HSM e potrebbe essere un problema perché un HSM è progettato per scoraggiare flooding di richieste.
È vero che il pepper è sempre lo stesso per tutti gli utenti ma, come ricordavo prima, di solito un HSM non fornisce i suoi segreti ma solo i risultati crittografici delle loro applicazioni.
#kdf #pbkdf2 #bcrypt #scrypt #yescrypt #argon2 #luks #cryptography #aes #sha #digest #RainbowTable #BruteForce #salt #pepper #entropy #hsm #hmac #hash