Comprendere le funzioni di hash e mantenere le password sicure

Di volta in volta, server e database vengono rubati o compromessi. Con questo in mente, è importante assicurarsi che alcuni dati utente cruciali, come le password, non possano essere recuperati. Oggi apprenderemo le nozioni di base relative all'hashing e quello che serve per proteggere le password nelle vostre applicazioni web.

Tutorial ripubblicato

Ogni poche settimane, rivisitiamo alcuni dei post preferiti del nostro lettore da tutta la cronologia del sito. Questo tutorial è stato pubblicato per la prima volta nel gennaio del 2011.


1. Disclaimer

La crittologia è un argomento sufficientemente complicato e io non sono affatto un esperto. C'è una costante ricerca in questo settore, in molte università e agenzie di sicurezza.

In questo articolo, cercherò di mantenere le cose il più semplici possibile, pur presentando un metodo ragionevolmente sicuro per memorizzare le password in un'applicazione web.


2. Che cosa fa "Hashing"?

Hashing converte un pezzo di dati (piccolo o grande) in una porzione relativamente breve di dati come una stringa o un intero.

Questo si ottiene usando una funzione di hash unidirezionale. "A senso unico" significa che è molto difficile (o praticamente impossibile) invertirlo.

Un esempio comune di una funzione di hash è md5 (), che è abbastanza popolare in molti linguaggi e sistemi diversi.

$ data = "Hello World"; $ hash = md5 ($ data); echo $ hash; // b10a8db164e0754105b7a99be72e3fe5

Con md5 (), il risultato sarà sempre una stringa lunga 32 caratteri. Ma contiene solo caratteri esadecimali; tecnicamente può anche essere rappresentato come un intero di 128 bit (16 byte). Potresti md5 () stringhe e dati molto più lunghi, e finirai comunque con un hash di questa lunghezza. Questo fatto da solo potrebbe darti un suggerimento sul motivo per cui questa è considerata una funzione "a senso unico".


3. Utilizzo di una funzione hash per la memorizzazione di password

Il solito processo durante la registrazione di un utente:

  • L'utente compila il modulo di registrazione, incluso il campo della password.
  • Lo script Web memorizza tutte le informazioni in un database.
  • Tuttavia, la password viene eseguita tramite una funzione hash, prima di essere archiviata.
  • La versione originale della password non è stata memorizzata da nessuna parte, quindi è tecnicamente scartata.

E il processo di accesso:

  • L'utente inserisce il nome utente (o e-mail) e la password.
  • Lo script esegue la password attraverso la stessa funzione di hashing.
  • Lo script trova il record utente dal database e legge la password hash memorizzata.
  • Entrambi questi valori vengono confrontati e l'accesso è concesso se corrispondono.

Una volta deciso un metodo decente per l'hashing della password, implementeremo questo processo più avanti in questo articolo.

Si noti che la password originale non è mai stata memorizzata da nessuna parte. Se il database viene rubato, gli accessi utente non possono essere compromessi, giusto? Bene, la risposta è "dipende". Diamo un'occhiata ad alcuni potenziali problemi.


4. Problema 1: collisione di hash

Una "collisione" di hash si verifica quando due diversi input di dati generano lo stesso hash risultante. La probabilità che ciò accada dipende dalla funzione che usi.

Come può essere sfruttato?

Ad esempio, ho visto alcuni script precedenti che utilizzavano le password di hash crc32 (). Questa funzione genera un numero intero a 32 bit come risultato. Questo significa che ci sono solo 2 ^ 32 (cioè 4.294.967.296) risultati possibili.

Facciamo hash una password:

echo crc32 ('supersecretpassword'); // output: 323322056

Ora, assumiamo il ruolo di una persona che ha rubato un database e ha il valore hash. Potremmo non essere in grado di convertire 323322056 in "supersecretpassword", tuttavia, possiamo trovare un'altra password che converta allo stesso valore di hash, con un semplice script:

set_time_limit (0); $ i = 0; while (true) if (crc32 (base64_encode ($ i)) == 323322056) echo base64_encode ($ i); Uscita;  $ i ++; 

Questo potrebbe funzionare per un po ', anche se, alla fine, dovrebbe restituire una stringa. Possiamo usare questa stringa restituita - anziché "supersecretpassword" - e ci permetterà di accedere con successo all'account di quella persona.

Ad esempio, dopo aver eseguito questo script esatto per alcuni momenti sul mio computer, mi è stato dato "MTIxMjY5MTAwNg =='. Proviamolo:

echo crc32 ('supersecretpassword'); // output: 323322056 echo crc32 ('MTIxMjY5MTAwNg =='); // output: 323322056

Come può essere prevenuto?

Oggigiorno, un potente PC di casa può essere utilizzato per eseguire una funzione di hash quasi un miliardo di volte al secondo. Quindi abbiamo bisogno di una funzione di hash che ha a molto vasta gamma.

Per esempio, md5 () potrebbe essere adatto, in quanto genera hash a 128 bit. Questo si traduce in 340.282.366,920,938,463,463,374,607,431,768,211,456 possibili risultati. È impossibile eseguire così tante iterazioni per trovare le collisioni. Tuttavia alcune persone hanno ancora trovato il modo di farlo (vedi qui).

SHA1

Sha1 () è un'alternativa migliore e genera un valore hash a 160 bit ancora più lungo.


5. Problema n. 2: tabelle Rainbow

Anche se risolviamo il problema di collisione, non siamo ancora al sicuro.

Una tabella arcobaleno viene creata calcolando i valori hash delle parole comunemente usate e le loro combinazioni.

Queste tabelle possono contenere milioni o addirittura miliardi di righe.

Ad esempio, è possibile passare attraverso un dizionario e generare valori hash per ogni parola. Puoi anche iniziare a combinare le parole insieme e generare hash per quelle. Questo non è tutto; puoi persino iniziare ad aggiungere cifre prima / dopo / tra le parole e memorizzarle nella tabella.

Considerando quanto sia poco costoso lo stoccaggio, oggigiorno è possibile produrre e utilizzare gigantesche tavole Rainbow.

Come può essere sfruttato?

Immaginiamo che un grande database venga rubato, insieme a 10 milioni di hash delle password. È abbastanza facile per cercare la tabella arcobaleno per ciascuno di essi. Non tutti saranno trovati, certo, ma comunque ... alcuni di loro lo faranno!

Come può essere prevenuto?

Possiamo provare ad aggiungere un "sale". Ecco un esempio:

$ password = "easypassword"; // questo può essere trovato in una tabella arcobaleno // perché la password contiene 2 parole comuni echo sha1 ($ password); // 6c94d3b42518febd4ad747801d50a8972022f956 // usa un gruppo di caratteri casuali, e può essere più lungo di questo $ salt = "f # @ V) Hu ^% Hgfds"; // questo NON si troverà in nessuna tabella rainbow pre-costruita echo sha1 ($ salt. $ password); // cd56a16759623378628c0d9336af69b74d9d71a5

Quello che fondamentalmente facciamo è concatenare la stringa "salt" con le password prima di eseguirne l'hashing. La stringa risultante ovviamente non sarà su alcuna tabella arcobaleno pre-costruita. Ma non siamo ancora al sicuro ancora!


6. Problema n. 3: tabelle Rainbow (di nuovo)

Ricorda che una Rainbow Table può essere creata da zero, dopo che il database è stato rubato.

Come può essere sfruttato?

Anche se fosse stato usato un sale, questo potrebbe essere stato rubato insieme al database. Tutto quello che devono fare è generare un nuovo Rainbow Table da zero, ma questa volta concatenano il sale ad ogni parola che stanno mettendo in tavola.

Ad esempio, in un generico Rainbow Table, "easypassword"può esistere, ma in questo nuovo Rainbow Table, hanno"f # @ V) Hu ^% Hgfdseasypassword"anche quando corrono tutti i 10 milioni di hash salati rubati contro questo tavolo, saranno di nuovo in grado di trovare alcune partite.

Come può essere prevenuto?

Possiamo invece utilizzare un "sale unico", che cambia per ciascun utente.

Un candidato per questo tipo di sale è il valore id dell'utente dal database:

$ hash = sha1 ($ user_id. $ password);

Ciò presuppone che il numero ID di un utente non cambi mai, il che è in genere il caso.

Potremmo anche generare una stringa casuale per ciascun utente e utilizzarla come unico sale. Ma dovremmo assicurarci che lo memorizziamo nel record dell'utente da qualche parte.

// genera una funzione di stringa casuale lunga 22 caratteri unique_salt () return substr (sha1 (mt_rand ()), 0,22);  $ unique_salt = unique_salt (); $ hash = sha1 ($ unique_salt. $ password); // e salva $ unique_salt con il record utente // ... 

Questo metodo ci protegge da Rainbow Tables, perché ora ogni singola password è stata salata con un valore diverso. L'attaccante dovrebbe generare 10 milioni di Tabelle Arcobaleno separate, il che sarebbe del tutto impossibile.


7. Problema n. 4: velocità hash

La maggior parte delle funzioni di hashing sono state progettate pensando alla velocità, poiché sono spesso utilizzate per calcolare i valori di checksum per i set di dati e file di grandi dimensioni, per verificare l'integrità dei dati.

Come può essere sfruttato?

Come ho detto prima, un PC moderno con potenti GPU (sì, schede video) può essere programmato per calcolare all'incirca un miliardo di hash al secondo. In questo modo, possono usare un attacco di forza bruta per provare ogni singola password possibile.

Potresti pensare che richiedere una password minima di 8 caratteri possa tenerlo al sicuro da un attacco di forza bruta, ma stabiliamo se questo è, in effetti, il caso:

  • Se la password può contenere lettere minuscole, lettere maiuscole e numeri, questo è 62 (26 + 26 + 10) caratteri possibili.
  • Una stringa lunga 8 caratteri ha 62 ^ 8 possibili versioni. Questo è poco più di 218 trilioni.
  • Ad un tasso di 1 miliardo di hash al secondo, che può essere risolto in circa 60 ore.

E per le password lunghe 6 caratteri, che è anche abbastanza comune, ci vorrebbero meno di 1 minuto.

Sentiti libero di richiedere password di 9 o 10 caratteri, tuttavia potresti iniziare a dare fastidio ad alcuni dei tuoi utenti.

Come può essere prevenuto?

Utilizzare una funzione hash più lenta.

Immagina di utilizzare una funzione hash che può essere eseguita solo 1 milione di volte al secondo sullo stesso hardware, anziché 1 miliardo di volte al secondo. Avrebbe quindi impiegato l'aggressore 1000 volte di più per forzare un hash. 60 ore si sarebbero trasformate in quasi 7 anni!

Un modo per farlo sarebbe implementarlo tu stesso:

function myhash ($ password, $ unique_salt) $ salt = "f # @ V) Hu ^% Hgfds"; $ hash = sha1 ($ unique_salt. $ password); // fallo prendere 1000 volte più a lungo per ($ i = 0; $ i < 1000; $i++)  $hash = sha1($hash);  return $hash; 

Oppure puoi usare un algoritmo che supporta un "parametro di costo", come BLOWFISH. In PHP, questo può essere fatto usando il cripta() funzione.

function myhash ($ password, $ unique_salt) // il sale per blowfish dovrebbe essere crypt di ritorno di 22 caratteri ($ password, $ 2a $ 10 $ '. $ unique_salt); 

Il secondo parametro per il cripta() la funzione contiene alcuni valori separati dal simbolo del dollaro ($).

Il primo valore è '$ 2a', che indica che useremo l'algoritmo BLOWFISH.

Il secondo valore, "$ 10" in questo caso, è il "parametro di costo". Questo è il logaritmo in base 2 di quante iterazioni verrà eseguito (10 => 2 ^ 10 = 1024 iterazioni). Questo numero può variare tra 04 e 31.

Facciamo un esempio:

function myhash ($ password, $ unique_salt) return crypt ($ password, '$ 2a $ 10 $'. $ unique_salt);  function unique_salt () return substr (sha1 (mt_rand ()), 0,22);  $ password = "verysecret"; echo myhash ($ password, unique_salt ()); // risultato: $ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC

L'hash risultante contiene l'algoritmo ($ 2a), il parametro di costo ($ 10) e il sale di 22 caratteri che è stato utilizzato. Il resto è l'hash calcolato. Facciamo un test:

// supponiamo che questo sia stato estratto dal database $ hash = '$ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; // presume che questa sia la password che l'utente ha inserito per riaccedere in $ password = "verysecret"; if (check_password ($ hash, $ password)) echo "Accesso concesso!";  else echo "Accesso negato!";  function check_password ($ hash, $ password) // primi 29 caratteri includono algoritmo, costo e sale // chiamiamolo $ full_salt $ full_salt = substr ($ hash, 0, 29); // esegue la funzione di hash su $ password $ new_hash = crypt ($ password, $ full_salt); // restituisce un ritorno vero o falso ($ hash == $ new_hash); 

Quando eseguiamo questo, vediamo "Accesso concesso!"


8. Mettiamola insieme

Con tutto ciò di cui sopra in mente, scriviamo una classe di utilità basata su ciò che abbiamo imparato finora:

class PassHash // blowfish private static $ algo = '$ 2a'; // costo parametro private static $ cost = '$ 10'; // principalmente per uso interno public static function unique_salt () return substr (sha1 (mt_rand ()), 0,22);  // questo sarà usato per generare un hash pubblico di hash public function ($ password) return crypt ($ password, self :: $ algo. self :: $ cost. '$'. self :: unique_salt ());  // questo sarà usato per confrontare una password con una funzione statica pubblica hash check_password ($ hash, $ password) $ full_salt = substr ($ hash, 0, 29); $ new_hash = crypt ($ password, $ full_salt); return ($ hash == $ new_hash); 

Ecco l'utilizzo durante la registrazione dell'utente:

// include la classe require ("PassHash.php"); // leggi tutto il modulo di input da $ _POST // ... // esegue la convalida del modulo normale // ... // hash la password $ pass_hash = PassHash :: hash ($ _ POST ['password']); // memorizza tutte le informazioni utente nel DB, escluso $ _POST ['password'] // memorizza $ pass_hash invece // ... 

Ed ecco l'utilizzo durante un processo di accesso utente:

// include la classe require ("PassHash.php"); // legge tutto il form input da $ _POST // ... // recupera il record utente basato su $ _POST ['username'] o simile // ... // controlla la password che l'utente ha provato ad accedere con if (PassHash :: check_password ( $ user ['pass_hash'], $ _POST ['password']) // concedere l'accesso // ... else // negare l'accesso // ...

9. Una nota sulla disponibilità di Blowfish

L'algoritmo Blowfish potrebbe non essere implementato in tutti i sistemi, anche se è abbastanza popolare ormai. Puoi controllare il tuo sistema con questo codice:

if (CRYPT_BLOWFISH == 1) echo "Sì";  else echo "No"; 

Tuttavia, a partire da PHP 5.3, non è necessario preoccuparsi; PHP viene fornito con questa implementazione integrata.


Conclusione

Questo metodo di password di hashing dovrebbe essere abbastanza solido per la maggior parte delle applicazioni web. Detto questo, non dimenticare: puoi anche richiedere che i tuoi membri utilizzino password più potenti, applicando lunghezze minime, caratteri misti, cifre e caratteri speciali.

Una domanda per te, lettore: come hai le tue password? Potete consigliare eventuali miglioramenti rispetto a questa implementazione?