Come si conservano le password dei tuoi utenti?
Si sente troppo spesso parlare di data breach con dump di database che contengono password in chiaro. Si tratta di data breach che riguardano spesso aziende importanti e enti pubblici. L'ultima performance di Anonymous ai danni di diversi enti e università come riportato in questo post (credit: Nicola Vanin) parla di dump contenenti diversi dati sensibili tra cui password memorizzate in chiaro. Questo è indice del fatto che la cultura della cyber security in Italia non è ancora decollata. La sicurezza di una infrastruttura è nella maggioranza dei casi affidata al buon senso e alla preparazione degli IT (soprattutto developer) chiamati a progettare, costruire e mantenere le infrastrutture. Se questo lo affianchiamo al sempre più crescente fenomeno del body rental, il quale ha come conseguenza quello di affidare la propria infrastruttura a personale demotivato e poco qualificato, il mix diventa esplosivo.
Premetto che io sono del parere che in una realtà di medie dimensioni, debba esserci almeno un blue-team e una cybersecurity policy ben definita. Mi rendo conto però che questa non è la realtà e che passerà ancora un po' di tempo affinchè diventi una prassi consolidata.
Ne frattempo se sei un developer ci sono diverse cose che devi conoscere riguardo al tema della cybersecurity e una di queste è sapere come conservare una password e come autenticare gli utenti sulla tua piattaforma web.
Arriveremo alla soluzione finale per gradi (se hai fretta salta i prossimi due paragrafi dell'articolo) partendo dalla bad practice, ovvero come non si conserva una password.
Password in chiaro
Il modo più semplice (e più sbagliato) per conservare una password è quello di memorizzarla nel nostro database così come l'utente ce la fornisce:
{ 'username': 'mario', 'password': 'Pas2w0rd' }
Un database del genere, nelle mani sbagliate non solo comprometterebbe la sicurezza dei dati dell'utente limitatamente al nostro sistema, ma considerando che spesso gli utenti hanno la cattiva abitudine di usare la stessa password anche per altri sistemi, verrebbe compromessa la sicurezza anche al di fuori del nostro raggio d'azione. In questo senso la password è a tutti gli effetti un'informazione sensibile soggetta alla Regolamentazione Europea per la Protezione dei Dati (GDPR).
Ma come si conserva una password? La buona notizia è che per assolvere allo scopo per il quale la password esiste, noi (la piattaforma) non abbiamo bisogno di conoscerla. Significa che non serve conoscere e quindi conservare la password, quello che ci serve in realtà è conoscere e conservare la prova che ci consente di capire se l'utente, in fase di autenticazione, ci sta fornendo la password corretta. Questo concetto ricorre spesso nell'ambito della sicurezza e della crittografia, si chiama Zero Knowledge (ZK), e si riferisce alla possibilità di provare una verità senza conoscerla. La ZK alleggerisce di parecchio le responsabilità di conservazione della prova, tuttavia le insidie non sono ancora finite.
Hash della password
Le funzioni di Hash sono una categoria di algoritmi crittografici che hanno diverse proprietà:
- Generano una sequenza di bit partendo da un qualsiasi input.
- Il risultato è di una lunghezza fissa in bit che dipende dallo specifico algoritmo di hash (256, 512, ecc.).
- Il risultato è irreversibile, non è possibile risalire al dato originale partendo dall'hash.
- L'algoritmo ripetuto sullo stesso input genera sempre lo stesso risultato.
- Una piccola variazione dell'input genera un hash completamente diverso, questo rende il risultato dell'algoritmo impredicibile.
- Due input completamente diversi (es. un numero intero e un file contenente un brano musicale) potrebbero generare due hash identici. Questa è detta collisione. Ad oggi non si conoscono casi di collisioni che riguardano i moderni algoritmi di hash (SHA256, Keccack, ecc.).
L'utilizzo dell'hash per i nostri scopi (la gestione delle password) ci consente di sfruttare il principio della Zero Knowledge in questo modo:
- Memorizziamo l'hash dell'utente nel nostro database.
- In fase di autenticazione, quando l'utente prova a collegarsi inviando le sue credenziali, la piattaforma calcola l'hash della password inviata e la confronta con l'hash memorizzato.
- Se i due hash coincidono si può essere ragionevolmente sicuri che la password fornita sia effettivamente quella usata in fase di registrazione. Questo perchè come abbiamo detto, password leggermente diverse generano hash completamente diversi mentre le collisioni sono un evento le cui probabilità sono praticamente nulle.
- Se il database finisce nelle mani sbagliate, grazie all'irreversibilità dell'algoritmo di hash, il malintenzionato non potrà conoscere la password.
Se fossimo in un mondo ideale e tutti gli utenti utilizzassero password complesse e diverse per ogni piattaforma con la quale sono collegati, questo sistema sarebbe sufficiente a garantire un ottimo livello di sicurezza. Purtroppo, gli utenti usano spesso password troppo semplici e uguali per diverse piattaforme. Negli anni gli hacker hanno creato database di diverse decine di GB di hash riferiti a password note. Questo significa che è molto probabile che un hash presente nel nostro database sia presente anche in questi dizionari vanificando di fatto l'efficacia del sistema.
Salt & Hash delle password
Per superare il problema della Rainbow Table Attack, l'idea è quella di generare, per ogni utenza, una sequenza di bit casuale, detta Salt, e generare l'hash della password concatenata a questa sequenza.
h = hash(password || salt)
oppure un più fashion:
h = hash(hash(password) || salt)
Memorizzando il salt nelle informazioni relative alla nostra utenza:
{ 'username': 'mario', 'hash': 0x0...., 'salt': 0x0... }
La fase di verifica non differisce dalla precedente, bisogna semplicemente ricordarsi di concatenare password e salt prima di eseguire l'hash e confrontarlo con quello memorizzato.
In caso di furto del database, il malintenzionato non potrebbe avvalersi delle Rainbow Table e sarebbe costretto a rigenerare gli hash per ogni password nota e per ogni salt presente nel database, operazione che risulta più dispendiosa dal punto di vista computazionale.
Questo è il minimo sindacale sufficiente per gestire una password.
Best practice di autenticazione
L'efficacia di questo metodo di conservazione della prova di conoscenza della password dipende fortemente dalle best practice adottate in fase di autenticazione. Di seguito un breve riepilogo con riferimenti per chi volesse approfondire:
- Utilizzare una comunicazione TLS in fase di autenticazione per mitigare attacchi Man in the Middle.
- Presentare un CAPTCHA all'utente dopo diversi tentativi falliti di login per mitigare Brute Force Attack.
- Utilizzare session ID, Timestamp o tecniche similari per mitigare Replay Attack (riguarda poco l'oggetto dell'articolo in sé ma mi sembrava corretto inserirla in un elenco di best practice).
- Dare la possibilità di impostare la Two Factor Authentication per fare in modo che la sola conoscenza della password non sia sufficiente in fase di autenticazione.
- Sensibilizzare l'utente sull'uso di password complesse suggerendo magari strumenti di gestione delle password.
Queste sono best practices che coinvolgono diversi stakeholders: developer, blue team, UX designer, utenti e che convergono tutte verso l'obiettivo di creare un sistema più sicuro dal punto di vista della sicurezza dei dati.
L'hashing delle password oggi
A questo punto è doveroso indicare che non tutti gli algoritmi di hash garantiscono lo stesso livello di sicurezza. Alcuni degli algoritmi considerati oggi più sicuri sono quelli di tipo SHA2 (utilizzato da Bitcoin), SHA3 (utilizzato da Ethereum), Blake2, Whirlpool, RIPEMD-160 (utilizzato da Bitcoin).
Tuttavia, rigenerare un dizionario di hash specifico per un determinato Salt noto, oggi è diventato sempre più semplice man mano che le potenzialità offerte dagli hardware aumentano. Attraverso hardware specializzato (RIG di GPU o ASIC), oggi più che mai è possibile ricostruire i dizionari in modo performante.
Questa possibilità mina fortemente l'efficacia del metodo appena visto (h = hash(p||s)), soprattutto in scenari in cui il livello di sicurezza deve essere elevato.
A tal proposito sono nati algoritmi di generazione chiavi resistenti a questo tipo di hardware. Tra i più utilizzati vi sono Scrypt e Argon2. Si tratta di algoritmi crittografici di tipo KDF resistenti agli hardware specializzati.
Scrypt è in grado di generare un hash il cui calcolo necessita una quantità elevata di capacità computazionale e RAM. Sebbene questo non costituisca un problema in fase di login (la singola computazione di un hash), diventa un grosso ostacolo per i malintenzionati che volessero ricostruire un dizionario di milioni di hash. Scrypt accetta in input parametri che indicano la quantità di risorse che l'hardware deve sostenere per ricavare l'hash.
h = Scrypt(passphrase, salt, n, r, p, len)
in cui:
- passphrase è la password
- salt è appunto il salt
- n è un parametro legato al costo in termini di CPU/RAM. Deve essere una potenza di 2. Oggi 1024 è un parametro sufficiente.
- r è un parametro legato alla fase iniziale di mixing dell'algoritmo. Generalmente è 8 o 16. Altri parametri sono generalmente usati per regolare l'algoritmo su hardware dedicati.
- p è un parametro che serve a moltiplicare il lavoro in termini di CPU senza aumentare il costo della RAM. Viene usato in quanto n agisce linearmente sia sulla CPU utilization che sulla quantità di RAM.
- len è la lunghezza desiderata dell'hash che si vuole ottenere.
Tutti questi parametri vengono conservati con la stessa logica vista in precedenza in modo da permettere la ricostruzione dell'hash in fase di login:
{ 'username': 'mario', 'hash': 0x0...., 'salt': 0x0..., 'n': 1024, 'r': 8, 'p': 16 }
In conclusione è importante dire che gli algoritmi di derivazione chiavi resistenti all'hardware, in scenari di tipo web application, vanno calibrati in modo da rendere complicata la vita ai malintenzionati senza impattare disastrosamente sulla scalabilità dell'architettura. Per questo è necessario un lavoro di squadra con i solution architect durante la progettazione del servizio di autenticazione (in modo da allocare risorse dedicate e renderlo scalabile orizzontalmente) nonchè in fase di tuning dell'algoritmo KDF.