Din când în când, serverele și bazele de date sunt furate sau compromise. Având în vedere acest lucru, este important să se asigure că anumite date cruciale ale utilizatorilor, cum ar fi parolele, nu pot fi recuperate. Astăzi, vom învăța elementele de bază din spatele hashing-ului și ceea ce este necesar pentru a proteja parolele în aplicațiile dvs. web.
Tutorial publicatLa fiecare câteva săptămâni, revizuim câteva postări preferate ale cititorului nostru de-a lungul istoriei site-ului. Acest tutorial a fost publicat pentru prima dată în ianuarie 2011.
Criptologia este un subiect suficient de complicat și nu sunt niciodată un expert. În acest domeniu se desfășoară cercetări constante, în multe universități și agenții de securitate.
În acest articol, voi încerca să păstrez lucrurile cât mai simplu posibil, prezentând în același timp o metodă rezonabil sigură de stocare a parolelor într-o aplicație web.
Hashing convertește o bucată de date (fie mică sau mare) într-o piesă relativ scurtă de date, cum ar fi un șir sau un întreg.
Acest lucru este realizat prin utilizarea unei funcții de hash într-o singură direcție. "O singură cale" înseamnă că este foarte dificil (sau practic imposibil) să o inversați.
Un exemplu comun al unei funcții hash este md5 (), care este destul de popular în multe limbi și sisteme diferite.
$ data = "Bună ziua"; $ hash = md5 (date $); echo $ hash; // b10a8db164e0754105b7a99be72e3fe5
Cu md5 ()
, rezultatul va fi întotdeauna un șir de 32 de caractere. Dar, conține numai caractere hexazecimale; din punct de vedere tehnic, poate fi reprezentat și ca număr întreg de 128 biți (16 octeți). Poți md5 ()
mult mai lungi și mai multe date și veți ajunge în continuare la un hash de această lungime. Acest fapt singur vă poate da un indiciu despre motivul pentru care aceasta este considerată o funcție "unică".
Procesul obișnuit în timpul înregistrării unui utilizator:
Și procesul de conectare:
Odată ce vom decide o metodă decentă pentru hashing parola, vom implementa acest proces mai târziu în acest articol.
Rețineți că parola originală nu a fost stocată niciodată. Dacă baza de date este furată, datele de conectare ale utilizatorilor nu pot fi compromise, nu? Ei bine, răspunsul este că "depinde". Să analizăm câteva probleme potențiale.
O "ciocnire" a hash-ului are loc atunci când două intrări de date diferite generează același hash rezultat. Probabilitatea acestui lucru depinde de funcția pe care o utilizați.
De exemplu, am văzut câteva scripturi mai vechi care au folosit crc32 () pentru parolele hash. Această funcție generează un întreg pe 32 de biți ca rezultat. Aceasta înseamnă că există doar 2 ^ 32 (adică 4.294.967.296) rezultate posibile.
Să avem o parolă:
echo crc32 ("supersecretpassword"); // outputs: 323322056
Acum, să presupunem rolul unei persoane care a furat o bază de date și are valoarea hash. S-ar putea să nu reușim să convertim 323322056 în "supersecretpassword", dar putem găsi o altă parolă care va converti la aceeași valoare hash, cu un script simplu:
set_time_limit (0); $ i = 0; în timp ce (true) if (crc32 (base64_encode ($ i)) == 323322056) echo base64_encode ($ i); Ieșire; $ i ++;
Acest lucru poate dura un timp, deși, în cele din urmă, ar trebui să returneze un șir. Putem folosi acest șir returnat - în loc de "supersecretpassword" - și ne va permite să ne conectăm cu succes la contul persoanei respective.
De exemplu, după ce am executat acest script exact pentru câteva momente pe calculatorul meu, mi sa dat "MTIxMjY5MTAwNg ==
“. Să încercăm:
echo crc32 ("supersecretpassword"); // ieșiri: 323322056 echo crc32 ('MTIxMjY5MTAwNg =='); // outputs: 323322056
În zilele noastre, un PC de acasă puternic poate fi folosit pentru a rula o funcție hash de aproape un miliard de ori pe secundă. Deci, avem nevoie de o functie hash care are a foarte gamă largă.
De exemplu, md5 ()
ar putea fi potrivită, deoarece generează hashes pe 128 de biți. Aceasta se traduce în 340,282,366,920,938,463,463,374,607,431,768,211,456 rezultate posibile. Este imposibil să fugi prin atâtea iterații pentru a găsi ciocniri. Cu toate acestea, unii oameni au găsit încă modalități de a face acest lucru (vedeți aici).
Sha1 () este o alternativă mai bună și generează o valoare de hash chiar mai lungă de 160 de biți.
Chiar dacă rezolvăm problema de coliziune, încă nu suntem în siguranță.
O masă de curcubeu este construită prin calcularea valorilor hash ale cuvintelor utilizate în mod obișnuit și a combinațiilor acestora.
Aceste tabele pot avea până la milioane sau chiar miliarde de rânduri.
De exemplu, puteți trece printr-un dicționar și puteți genera valori hash pentru fiecare cuvânt. De asemenea, puteți începe să combinați cuvintele și să generați hashes pentru acestea. Asta nu este tot; puteți chiar să începeți să adăugați cifre înainte / după / între cuvinte și să le memorați și în tabel.
Având în vedere cât de ieftin este stocarea în zilele noastre, gigantice Rainbow Tables pot fi produse și utilizate.
Să presupunem că o bază de date mare este furată, împreună cu 10 milioane de parole. Este destul de ușor să căutați tabelul curcubeu pentru fiecare dintre ele. Nu toți vor fi găsiți, cu siguranță, dar, totuși ... unii vor!
Putem încerca să adăugăm o "sare". Iată un exemplu:
$ parola = "easypassword"; // aceasta poate fi găsită într-o masă de curcubeu // deoarece parola conține două cuvinte comune echo sha1 ($ password); // 6c94d3b42518febd4ad747801d50a8972022f956 // folosiți o grămadă de caractere aleatoare și poate fi mai lungă decât acest $ salt = "f # @ V) Hu ^% Hgfds"; // acest lucru NU va fi găsit în niciun tabel curcubeu pre-construit echo sha1 ($ salt $ password); // cd56a16759623378628c0d9336af69b74d9d71a5
Ceea ce facem în mod esențial este să concatenăm sirul "sare" cu parolele înainte de a le sparge. Șirul rezultat, evident, nu va fi pe nici o masă de curcubeu pre-construită. Dar încă nu suntem siguri!
Amintiți-vă că o masă Rainbow poate fi creată de la zero, după ce baza de date a fost furată.
Chiar dacă s-ar folosi o sare, acest lucru poate fi furat împreună cu baza de date. Tot ce trebuie să facă este să genereze o nouă masă Rainbow de la zero, dar de această dată ei concatează sarea cu fiecare cuvânt pe care îl pun în masă.
De exemplu, într-un tabel generic Rainbow, "easypassword
"pot exista, dar în această nouă masă Rainbow, ei au"f # @ V) Hu ^% Hgfdseasypassword
De asemenea, atunci când execută toate cele 10 milioane de hashes salvate furate împotriva acestui tabel, vor putea găsi din nou câteva meciuri.
În schimb, putem folosi o "sare unică", care se schimbă pentru fiecare utilizator.
Un candidat pentru acest tip de sare este valoarea id-ului utilizatorului din baza de date:
$ hash = sha1 ($ user_id. $ parola);
Aceasta presupune că numărul de identificare al unui utilizator nu se modifică niciodată, ceea ce se întâmplă de obicei.
Putem, de asemenea, să generăm un șir aleator pentru fiecare utilizator și să îl folosim ca sare unică. Dar ar trebui să ne asigurăm că stocăm asta în dosarul utilizatorului undeva.
// generează o funcție de șir aleatoare de 22 de caractere unique_salt () return substr (sha1 (mt_rand ()), 0,22); $ unique_salt = unique_salt (); $ hash = sha1 ($ unique_salt. $ parola); // și salvați $ unique_salt cu înregistrarea utilizatorului // ...
Această metodă ne protejează împotriva Rainbow Tables, deoarece acum fiecare parolă a fost salată cu o valoare diferită. Atacatorul ar trebui să genereze 10 milioane de tabele Rainbow separate, ceea ce ar fi complet nepractic.
Cele mai multe funcții de tip hash au fost proiectate cu viteza în minte, deoarece ele sunt adesea utilizate pentru a calcula valorile sumelor de control pentru seturile mari de date și fișiere, pentru a verifica integritatea datelor.
După cum am menționat mai devreme, un PC modern cu GPU-uri puternice (da, plăci video) poate fi programat pentru a calcula aproximativ un miliard de hashes pe secundă. În acest fel, pot folosi un atac de forță brute pentru a încerca fiecare parolă posibilă.
S-ar putea să credeți că necesitatea unei parole de minim 8 caractere ar putea să o mențină în siguranță de la un atac brutal, dar să determinăm dacă într-adevăr acest lucru este cazul:
Și pentru parole de 6 caractere lungi, care este de asemenea destul de comună, ar dura mai puțin de 1 minut.
Simțiți-vă liber să cereți parole de 9 sau 10 caractere, însă este posibil să începeți să vă enervați pe unii dintre utilizatori.
Utilizați o funcție hash mai lentă.
Imaginați-vă că utilizați o funcție hash care poate rula numai 1 milion de ori pe secundă pe același hardware, în loc de 1 miliard de ori pe secundă. Apoi, atacatorul ar fi luat de 1000 de ori mai mult pentru a forța o forță. 60 de ore s-ar transforma în aproape 7 ani!
O modalitate de a face acest lucru ar fi să o implementați singuri:
funcția myhash ($ password, $ unique_salt) $ salt = "f # @ V) Hu ^% Hgfds"; $ hash = sha1 ($ unique_salt. $ parola); // a face să dureze de 1000 de ori mai mult pentru ($ i = 0; $ i < 1000; $i++) $hash = sha1($hash); return $hash;
Sau puteți utiliza un algoritm care acceptă un "parametru de cost", cum ar fi BLOWFISH. În PHP, acest lucru se poate face folosind criptă()
funcţie.
funcția myhash ($ password, $ unique_salt) // sarea pentru blowfish ar trebui să aibă o caractere de returnare de 22 de caractere ($ password, '$ 2a $ 10 $' $ $ unique_salt);
Al doilea parametru la criptă()
funcția conține câteva valori separate de semnul dolarului ($).
Prima valoare este '$ 2a', ceea ce indică faptul că vom folosi algoritmul BLOWFISH.
A doua valoare, "$ 10" în acest caz, este "parametrul de cost". Acesta este logaritmul de bază-2 pentru câte iterații se va desfășura (10 => 2 ^ 10 = 1024 iterații.) Acest număr poate varia între 04 și 31.
Să dăm un exemplu:
funcția myhash ($ password, $ unique_salt) return crypt ($ parola, '$ 2a $ 10 $'. $ unique_salt); funcția unic_salt () return substr (sha1 (mt_rand ()), 0,22); $ password = "verysecret"; echo myhash ($ parola, unique_salt ()); // rezultat: $ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC
Hashul rezultat conține algoritmul ($ 2a), parametrul de cost ($ 10) și sarea de 22 caractere care a fost utilizată. Restul este suma hash calculată. Să facem un test:
// presupune că a fost extras din baza de date $ hash = '$ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; // presupuneți că aceasta este parola utilizată de utilizator pentru a vă conecta înapoi la $ password = "verysecret"; dacă (check_password ($ hash, $ password)) echo "Access Granted!"; altceva echo "Access Denied!"; funcția check_password ($ hash, $ password) // primele 29 de caractere includ algoritmul, costul și sarea // să ne numim $ full_salt $ full_salt = substr ($ hash, 0, 29); // executați funcția hash pe $ password $ new_hash = cript ($ password, $ full_salt); // returnează returnarea adevărată sau falsă ($ hash == $ new_hash);
Când executăm acest lucru, vedem "Accesul acordat!"
Având în vedere toate cele de mai sus, să scriem o clasă de utilități bazată pe ceea ce am învățat până acum:
clasa PassHash // lovitură privată statică $ algo = '$ 2a'; // costul parametrului privat static $ cost = '$ 10'; // în principal pentru uz intern funcția statică publică unique_salt () return substr (sha1 (mt_rand ()), 0,22); // aceasta va fi folosită pentru a genera hash funcția publică statică hash ($ password) return cript ($ password, self :: $ algo.one :: $ cost. '$' self :: unique_salt ()); // aceasta va fi folosită pentru a compara o parolă cu o funcție statică publică hash check_password ($ hash, $ password) $ full_salt = substr ($ hash, 0, 29); $ new_hash = cripta ($ parola, $ full_salt); întoarcere ($ hash == $ new_hash);
Iată utilizarea în timpul înregistrării utilizatorului:
// include cerința de clasă ("PassHash.php"); // citeste toate intrarile din formularul $ _POST // ... // face chestii de validare a formularului obisnuit // ... // hash parola $ pass_hash = PassHash :: hash ($ _ POST ['password']); // stocați toate informațiile despre utilizator în DB, excluzând $ _POST ['password'] // magazin $ pass_hash instead // ...
Și aici este utilizarea în timpul unui proces de autentificare a utilizatorului:
// include cerința de clasă ("PassHash.php"); // citiți toate intrările din formular din $ _POST // ... // preluați înregistrarea utilizatorului bazată pe $ _POST ['username'] sau similar // ... // verificați parola pe care utilizatorul a încercat să o conecteze cu dacă (PassHash :: check_password $ user ['pass_hash'], $ _POST ['parola']) // acord acces // ... altceva // refuza accesul // ...
Este posibil ca algoritmul Blowfish să nu fie implementat în toate sistemele, chiar dacă este destul de popular până acum. Puteți verifica sistemul dvs. cu acest cod:
dacă (CRYPT_BLOWFISH == 1) echo "Da"; altceva echo "Nu";
Totuși, din PHP 5.3, nu trebuie să vă faceți griji; PHP livrează cu această implementare implementată.
Această metodă de parole hashing ar trebui să fie suficient de solidă pentru majoritatea aplicațiilor web. Acestea fiind spuse, nu uitați: de asemenea, puteți solicita ca membrii dvs. să utilizeze parole mai puternice, prin impunerea unor lungimi minime, caractere mixte, cifre și caractere speciale.
O întrebare pentru tine, cititor: cum îți faci parolele? Puteți recomanda îmbunătățiri față de această implementare?