Nell’articolo Gestione delle password tarmite funzioni hash abbiamo descritto una procedura per il salvataggio delle password utente sulla base dati di una applicazione, basata sull’utilizzo di una funzione hash. Sebbene si tratti di una tecnica molto utilizzata, è anche la più vulnerabile, ed è assolutamente da evitare nelle situazioni in cui la sicurezza è il requisito più importante dell’applicativo.
Vulnerabilità della Funzione Hash
Le funzioni hash sono per loro natura particolarmente efficienti. Consideriamo ad esempio la seguente porzione di codice in cui è calcolato l’hash della parola “password” con l’algoritmo SHA-256.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class SHA256Main { public static void main(String[] args) throws Exception { StopWatch stopWatch = new StopWatch(); stopWatch.start(); MessageDigest md = MessageDigest.getInstance( "SHA-256" ); md.update( "password".getBytes("UTF-8") ); stopWatch.stop(); System.out.println("Time: " + stopWatch.getTime()); } } |
La classe StopWatch appartiene alla libreria Apache Common Lang è serve per calcolare il tempo di esecuzione del programma. Con i mio pc che è un Mac con Intel Core i7 a 2,6 GHz il tempo di elaborazione è di 14 millisecondi. Facendo due rapidi calcoli significa che è possibile calcolare l’hash di 71 parole al secondo, ovvero 4260 al minuto. Se consideriamo il numero di lemmi presenti nel dizionario italiano, che sono approssimativamente 300000 (fonte Trecani), il tempo di calcolo si aggira intorno ai 70 minuti con un laptop.
Questo semplice esempio è sufficiente a farci capire che proteggere una password con questa tecnica è chiaramente vulnerabile ad attacchi di tipo brute force specie se basati su dizionario. La differenza tra i due tipi di attacco è che col brute force vengono provate tutte le stringhe di un alfabeto generandole una di seguito all’altra. Il mio laptop ad esempio ha generato tutte le possibili combinazioni di 8 caratteri nell’alfabeto [A..Z]
in soli 4 minuti e mezzo. Con l’uso di un dizionario, invece, si limita notevolmente il numero di stringhe da testare utilizzando un elenco di password tra le più utilizzate. In questo secondo caso inoltre il dizionario potrebbe essere precalcolato, ovvero contenere già l’hash delle stringhe, in questo modo il test si ridurrebbe ad un confronto.
Uso del Salt
Una evoluzione della tecnica consiste nell’utilizzare un salt, ovvero una stringa generata randomicamente che viene concatenata con la password prima di eseguire l’hash. Naturalmente in questo caso l’applicazione dovrà conservare, sulla propria base dati, sia l’hash che il salt utilizzato. Sebbene l’affidabilità della soluzione non sia legata alla segretezza del salt o alla sua complessità, è buona norma generarlo utilizzando una funzione random sicura. La seguente porzione di codice implementa quanto detto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class SHA256SaltMain { private static final Random RANDOM = new SecureRandom(); public static byte[] getNextSalt() { byte[] salt = new byte[16]; RANDOM.nextBytes(salt); return salt; } public static void main(String[] args) throws Exception { StopWatch stopWatch = new StopWatch(); stopWatch.start(); MessageDigest md = MessageDigest.getInstance( "SHA-256" ); String password = getNextSalt() + "password"; md.update( password.getBytes("UTF-8") ); stopWatch.stop(); System.out.println("Time: " + stopWatch.getTime()); } } |
Questa tecnica vanifica gli attacchi con dizionario precalcolato, in quanto l’hash dipende dal salt e dovrei disporre di un dizionario per ogni possibile salt. Inoltre anche nel caso in cui si riuscisse, in qualche modo, a recuperare il salt utilizzato per una password, questo sarebbe comunque differente da quelli utilizzati per le altre.
Sfortunatamente l’efficienza della funzione di hash rende ancora il sistema vulnerabile ad attacchi di tipo brute force, è sufficiente considerare stringhe più lunghe ovvero che includano il salt e la password. Inoltre altre tecniche di hacking sono emerse che risultano particolarmente efficienti come ad esempio le Rainbow Table.
Key Derivation Function
La tecnica più sicura dal punto di vista della sicurezza crittografica è quella di utilizzare funzioni di derivazione di chiavi. Queste tecniche risolvono un problema differente che è quello di ottenere una chiave crittografica a partire da informazioni note come un PIN o una pass-phrase. L’idea di queste funzioni è quella di rendere più complesso l’attacco rendendo computazionalmente più oneroso il calcolo della chiave derivata.
Uno dei più famosi algoritmi KDF è PBKDF2 (Password-Based Key Derivation Function 2) la cui specifica è definita nella RFC 2898 emanata nel 2000 dalla IETF. L’algoritmo prevede l’utilizzo di una password e di un salt, come per le funzioni hash viste sopra, in aggiunta ad un iteration count che specifica il numero di volte in cui una determinata funzione hash deve essere applicata prima di restituire la chiave derivata.
E la presenza dell’iteration count che rende l’algoritmo particolarmente resistente agli attacchi. La specifica prevede infatti un valore per il parametro almeno pari a 1000, ma con l’evoluzione del parallel computing e delle GPU che sono in grado di eseguire operazioni matematiche in modo molto efficiente, tale valore è ormai considerato insicuro. Ad ogni modo è sufficiente rispondere al problema selezionando iteration count di valore sempre più grande, mantenendo l’algoritmo inalterato.
Di seguito è riportato il codice java per la derivazione di una chiave di cifratura a 256 bit utilizzando come parametri quelli raccomandati dalle linee guida del NIST del 2016, ovvero:
- un salt a 64 bit;
- un iteration count pari a 10000.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class PBKDF2Main { private static final Random RANDOM = new SecureRandom(); public static byte[] getNextSalt() { byte[] salt = new byte[64]; RANDOM.nextBytes(salt); return salt; } public static void main(String[] args) throws Exception { StopWatch stopWatch = new StopWatch(); stopWatch.start(); char[] chars = "password".toCharArray(); byte[] salt = getNextSalt(); int iterations = 10000; PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 256 * 8); SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); byte[] key = skf.generateSecret(spec).getEncoded(); stopWatch.stop(); System.out.println("Time: " + stopWatch.getTime()); } } |
Il tempo di esecuzione dell’operazione sul mio laptop è di 323 millisecondi, determinando un tempo di 26 circa ore per eseguire un attacco di brute force basato sul dizionario della lingua italiana.
Codice Sorgente
Il codice sorgente con l’esempio presentato è scaricabile qui pbkdf2.