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æringNoen få uker besøker vi noen av leserens favorittinnlegg fra hele historien til nettstedet. Denne opplæringen ble først publisert i januar 2011.
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.
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.
Den vanlige prosessen under en brukerregistrering:
Og påloggingsprosessen:
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.
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.
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
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 () er et bedre alternativ, og det genererer en enda lengre 160-biters hashverdi.
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.
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!
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å!
Husk at det kan opprettes et regnbuebord fra begynnelsen, etter at databasen er stjålet.
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.
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.
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.
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:
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.
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!"
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 // ...
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.
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?