Forstå Hashfunksjoner og Hold Passord Safe

Fra tid til annen blir servere og databaser stjålet eller kompromittert. Med dette i bakhodet er det viktig å sikre at noen viktige brukerdata, for eksempel passord, ikke kan gjenopprettes. I dag skal vi lære det grunnleggende bak hashing og hva det tar å beskytte passordene i webapplikasjonene dine.

Publisert opplæring

Noen få uker besøker vi noen av leserens favorittinnlegg fra hele historien til nettstedet. Denne opplæringen ble først publisert i januar 2011.


1. Ansvarsfraskrivelse

Kryptologi er et tilstrekkelig komplisert emne, og jeg er på ingen måte en ekspert. Det skjer konstant forskning på dette området, i mange universiteter og sikkerhetsbyråer.

I denne artikkelen vil jeg prøve å holde ting så enkelt som mulig, samtidig som jeg presenterer en rimelig sikker metode for lagring av passord i en webapplikasjon.


2. Hva gjør "Hashing"?

Hashing konverterer et stykke data (enten små eller store) til et relativt kort stykke data som en streng eller et heltall.

Dette oppnås ved å bruke en enveis hash-funksjon. "One-way" betyr at det er svært vanskelig (eller praktisk talt umulig) å reversere det.

Et vanlig eksempel på en hash-funksjon er md5 (), som er ganske populær på mange forskjellige språk og systemer.

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

Med MD5 (), Resultatet vil alltid være en 32 tegn lang streng. Men den inneholder bare heksadesimale tegn; Teknisk kan det også representeres som et 128-biters (16 byte) heltall. Du kan MD5 () mye lengre strenger og data, og du vil fortsatt ende opp med en hash av denne lengden. Dette faktum alene kan gi deg et hint om hvorfor dette betraktes som en "enveis" -funksjon.


3. Bruke en Hash-funksjon for lagring av passord

Den vanlige prosessen under en brukerregistrering:

  • Bruker fyller ut registreringsskjema, inkludert passordfeltet.
  • Nettskriptet lagrer all informasjon i en database.
  • Passordet kjøres imidlertid gjennom en hash-funksjon, før den lagres.
  • Den opprinnelige versjonen av passordet er ikke lagret hvor som helst, så det blir teknisk kassert.

Og påloggingsprosessen:

  • Bruker går inn i brukernavn (eller e-post) og passord.
  • Skriptet kjører passordet gjennom samme hashing-funksjon.
  • Skriptet finner brukeroppføringen fra databasen, og leser det lagrede hashed-passordet.
  • Begge disse verdiene blir sammenlignet, og tilgangen gis hvis de samsvarer.

Når vi bestemmer oss for en anstendig metode for hashing passordet, skal vi implementere denne prosessen senere i denne artikkelen.

Merk at det opprinnelige passordet aldri har blitt lagret hvor som helst. Hvis databasen er stjålet, kan brukerinnloggene ikke bli kompromittert, ikke sant? Vel, svaret er "det avhenger." La oss se på noen potensielle problemer.


4. Problem nr. 1: Hash kollisjon

En hash "kollisjon" oppstår når to forskjellige datainnganger genererer den samme resulterende hashen. Sannsynligheten for dette skjer avhengig av hvilken funksjon du bruker.

Hvordan kan dette utnyttes?

Som et eksempel har jeg sett noen eldre skript som brukte crc32 () til hash-passord. Denne funksjonen genererer et 32-biters heltall som resultat. Dette betyr at det bare er 2 ^ 32 (dvs. 4 294 967 296) mulige utfall.

La oss hash et passord:

ekko crc32 ('supersecretpassword'); // utganger: 323322056

La oss nå anta rollen som en person som har stjålet en database, og har hashverdien. Vi kan ikke konvertere 323322056 til 'supersecretpassword', men vi kan finne ut et annet passord som vil konvertere til samme hashverdi, med et enkelt skript:

set_time_limit (0); $ i = 0; mens (sann) if (crc32 (base64_encode ($ i)) == 323322056) echo base64_encode ($ i); exit;  $ i ++; 

Dette kan løpe en stund, men til slutt bør den returnere en streng. Vi kan bruke denne returnerte strengen - i stedet for 'supersecretpassword' - og det vil tillate oss å kunne logge inn på den personenes konto.

For eksempel, etter å ha kjørt dette eksakte skriptet for noen få minutter på datamaskinen min, ble jeg gitt 'MTIxMjY5MTAwNg =='. La oss teste det ut:

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

Hvordan kan dette forhindres?

I dag kan en kraftig hjemme-pc brukes til å drive en hash-funksjon nesten en milliard ganger per sekund. Så vi trenger en hash-funksjon som har a veldig stort utvalg.

For eksempel, MD5 () kan være egnet, da det genererer 128-biters hash. Dette betyr 340,282,366,920,938,463,463,374,607,431,768,211,456 mulige utfall. Det er umulig å gjennomføre så mange iterasjoner for å finne kollisjoner. Men noen mennesker har fortsatt funnet måter å gjøre dette på (se her).

SHA1

Sha1 () er et bedre alternativ, og det genererer en enda lengre 160-biters hashverdi.


5. Problem nr. 2: Rainbow-tabeller

Selv om vi løser kollisionsproblemet, er vi fremdeles ikke trygge ennå.

Et regnbuebord er bygget ved å beregne hashverdiene for vanlige ord og deres kombinasjoner.

Disse tabellene kan ha så mange som millioner eller milliarder av rader.

For eksempel kan du gå gjennom en ordliste og generere hashverdier for hvert ord. Du kan også begynne å kombinere ord sammen, og generere hash for dem også. Det er ikke alt; Du kan til og med begynne å legge til tall før / etter / mellom ord, og lagre dem i tabellen også.

Med tanke på hvor billig lagring er i dag, kan gigantiske regnbuebord produseres og brukes.

Hvordan kan dette utnyttes?

La oss forestille oss at en stor database er stjålet, sammen med 10 millioner passord hashes. Det er ganske enkelt å søke på regnbuebordet for hver av dem. Ikke alle av dem vil bli funnet, absolutt, men likevel ... noen av dem vil!

Hvordan kan dette forhindres?

Vi kan prøve å legge til et "salt". Her er et eksempel:

$ password = "easypassword"; // dette kan bli funnet i et regnbuebord // fordi passordet inneholder 2 vanlige ord echo sha1 ($ password); // 6c94d3b42518febd4ad747801d50a8972022f956 // bruk gjeng med tilfeldige tegn, og det kan være lengre enn dette $ salt = "f # @ V) Hu ^% Hgfds"; // dette vil IKKE bli funnet i noen forhåndsbygd regnbuebord ekko sha1 ($ salt. $ passord); // cd56a16759623378628c0d9336af69b74d9d71a5

Det vi i utgangspunktet gjør, er å sammenkoble "salt" -strengen med passordene før de har kastet dem. Den resulterende strengen vil åpenbart ikke være på noen forhåndsbygde regnbuebord. Men vi er fremdeles ikke trygge ennå!


6. Problem nr. 3: Rainbow-tabellene (igjen)

Husk at det kan opprettes et regnbuebord fra begynnelsen, etter at databasen er stjålet.

Hvordan kan dette utnyttes?

Selv om et salt ble brukt, kan dette ha blitt stjålet sammen med databasen. Alt de trenger å gjøre er å generere en ny Rainbow-tabell fra bunnen av, men denne gangen slår de sammen saltet til hvert ord som de setter i bordet.

For eksempel, i en generisk Rainbow Table, "easypassword"kan eksistere. Men i denne nye Rainbow Table har de"f # @ V) Hu ^% Hgfdseasypassword"i tillegg. Når de kjører alle de 10 millioner stjålet saltede hashene mot dette bordet, vil de igjen kunne finne noen kamper.

Hvordan kan dette forhindres?

Vi kan i stedet bruke et "unikt salt", som endres for hver bruker.

En kandidat for denne typen salt er brukerens id-verdi fra databasen:

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

Dette forutsetter at brukerens id-nummer aldri endres, noe som vanligvis er tilfelle.

Vi kan også generere en tilfeldig streng for hver bruker og bruke det som det unike saltet. Men vi må sørge for at vi lagrer det i brukeroppføringen et sted.

// genererer en 22 tegn lang tilfeldig streng-funksjon unikt_salt () retur substr (sha1 (mt_rand ()), 0,22);  $ unique_salt = unique_salt (); $ hash = sha1 ($ unique_salt. $ passord); // og lagre $ unique_salt med brukeroppføringen // ... 

Denne metoden beskytter oss mot Rainbow Tables, fordi nå er hvert enkelt passord blitt saltet med en annen verdi. Angriperen ville måtte generere 10 millioner separate regnbuebord, noe som ville være helt upraktisk.


7. Problem # 4: Hash Speed

De fleste hashing-funksjoner har blitt designet med fart i tankene, fordi de ofte brukes til å beregne sjekksumverdier for store datasett og filer for å kontrollere dataintegritet.

Hvordan kan dette utnyttes?

Som nevnt tidligere kan en moderne PC med kraftige GPU (ja, videokort) programmeres for å beregne omtrent en milliard hash per sekund. På denne måten kan de bruke et brute force-angrep for å prøve hvert eneste mulig passord.

Du kan tro at det krever minst 8 tegn langt passord, kan holde det trygt fra et brutalt kraftangrep, men la oss avgjøre om det faktisk er tilfellet:

  • Hvis passordet kan inneholde små bokstaver, store bokstaver og nummer, er det 62 tegn (26 + 26 + 10).
  • En 8 tegn lang streng har 62 ^ 8 mulige versjoner. Det er litt over 218 billioner.
  • Med en hastighet på 1 milliard hash per sekund, som kan løses på rundt 60 timer.

Og for 6 tegn lange passord, som også er ganske vanlig, ville det ta under 1 minutt.

Ta gjerne 9 eller 10 tegn lange passord, men du kan begynne å irritere noen av brukerne dine.

Hvordan kan dette forhindres?

Bruk en tregere hash-funksjon.

Tenk deg at du bruker en hash-funksjon som bare kan kjøre 1 million ganger per sekund på samme maskinvare, i stedet for 1 milliard ganger per sekund. Det ville da ta angriperen 1000 ganger lenger for å brute tvinge en hash. 60 timer vil bli til nesten 7 år!

En måte å gjøre det på er å implementere det selv:

funksjon myhash ($ passord, $ unique_salt) $ salt = "f # @ V) Hu ^% Hgfds"; $ hash = sha1 ($ unique_salt. $ passord); // gjør det ta 1000 ganger lenger for ($ i = 0; $ i < 1000; $i++)  $hash = sha1($hash);  return $hash; 

Eller du kan bruke en algoritme som støtter en "kostnadsparameter", som for eksempel BLOWFISH. I PHP kan dette gjøres ved hjelp av krypten () funksjon.

funksjon myhash ($ passord, $ unique_salt) // saltet for blowfish skal være 22 tegn lang returkryptering ($ passord, $ 2a $ 10 $ '. $ unique_salt); 

Den andre parameteren til krypten () funksjonen inneholder noen verdier skilt av dollartegnet ($).

Den første verdien er '$ 2a', noe som indikerer at vi skal bruke BLOWFISH-algoritmen.

Den andre verdien, '$ 10' i dette tilfellet, er "kostnadsparameter". Dette er basis-2-logaritmen av hvor mange iterasjoner den skal kjøre (10 => 2 ^ 10 = 1024 iterasjoner.) Dette nummeret kan variere mellom 04 og 31.

La oss få et eksempel:

funksjon myhash ($ passord, $ unique_salt) return crypt ($ passord, '$ 2a $ 10 $'. $ unique_salt);  funksjon unique_salt () return substr (sha1 (mt_rand ()), 0,22);  $ password = "verysecret"; ekko myhash ($ passord, unique_salt ()); // resultat: $ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC

Den resulterende hasen inneholder algoritmen ($ 2a), kostnadsparameteren ($ 10) og 22 karaktersaltet som ble brukt. Resten av det er den beregnede hasen. La oss kjøre en prøve:

// antar at dette ble trukket fra databasen $ hash = '$ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; // antar at dette er passordet brukeren skrev inn for å logge seg tilbake i $ password = "verysecret"; hvis (check_password ($ hash, $ passord)) echo "Access Granted!";  ellers echo "Access nektet!";  funksjon check_password ($ hash, $ passord) // første 29 tegn inkluderer algoritme, kostnad og salt // la oss kalle det $ full_salt $ full_salt = substr ($ hash, 0, 29); // Kjør hash-funksjonen på $ password $ new_hash = crypt ($ passord, $ full_salt); // returnerer sann eller falsk retur ($ hash == $ new_hash); 

Når vi kjører dette, ser vi "Access Granted!"


8. Sette det sammen

Med alle de ovennevnte i tankene, la oss skrive en verktøysklasse basert på det vi lærte så langt:

klasse PassHash // blowfish privat statisk $ algo = '$ 2a'; // cost parameter privat statisk $ cost = '$ 10'; // hovedsakelig for intern bruk offentlig statisk funksjon unikt_salt () retur substr (sha1 (mt_rand ()), 0,22);  // dette vil bli brukt til å generere en hash offentlig statisk funksjon hash ($ passord) return crypt ($ passord, selv :: $ algo. selv :: $ cost. '$'. selv :: unique_salt ());  // dette vil bli brukt til å sammenligne et passord mot en hash offentlig statisk funksjon check_password ($ hash, $ passord) $ full_salt = substr ($ hash, 0, 29); $ new_hash = crypt ($ passord, $ full_salt); returnere ($ hash == $ new_hash); 

Her er bruken under brukerregistrering:

// inkludere klassen krever ("PassHash.php"); // Les alle skjemainnganger fra $ _POST // ... // Gjør ditt vanlige skjema validering stuff // ... // hash passordet $ pass_hash = PassHash :: hash ($ _ POST ['passord']); // lagre all brukerinformasjon i DB, unntatt $ _POST ['passord'] // lagre $ pass_hash i stedet // ... 

Og her er bruken under en brukerloggingsprosess:

// inkludere klassen krever ("PassHash.php"); // les alle skjemainnganger fra $ _POST // ... // hente brukeroppføringen basert på $ _POST ['brukernavn'] eller lignende // ... // sjekke passordet brukeren prøvde å logge inn med hvis (PassHash :: check_password ( $ bruker ['pass_hash'], $ _POST ['passord']) // gi tilgang // ... annet // nekte tilgang // ...

9. En kommentar om blowfish tilgjengelighet

Blowfish-algoritmen kan ikke implementeres i alle systemer, selv om det er ganske populært nå. Du kan sjekke systemet med denne koden:

hvis (CRYPT_BLOWFISH == 1) echo "Yes";  annet ekko "nei"; 

Men fra PHP 5.3 trenger du ikke å bekymre deg; PHP leveres med denne implementasjonen innebygd.


Konklusjon

Denne metoden for hashing-passord skal være solid nok for de fleste webapplikasjoner. Når det er sagt, ikke glem det: Du kan også kreve at medlemmene bruker sterkere passord, ved å håndheve minimumslengde, blandede tegn, siffer og spesialtegn.

Et spørsmål til deg, leser: hvordan har du passordene dine? Kan du anbefale noen forbedringer over denne implementeringen?