Hash-functies begrijpen en wachtwoorden veilig houden

Van tijd tot tijd worden servers en databases gestolen of aangetast. Met dit in gedachten is het belangrijk ervoor te zorgen dat bepaalde cruciale gebruikersgegevens, zoals wachtwoorden, niet kunnen worden hersteld. Vandaag gaan we de basis achter hashen leren en wat er nodig is om wachtwoorden in uw webtoepassingen te beschermen.

Opnieuw gepubliceerde zelfstudie

Om de paar weken bekijken we enkele van onze favoriete lezers uit de geschiedenis van de site. Deze tutorial werd voor het eerst gepubliceerd in januari 2011.


1. Disclaimer

Cryptologie is een voldoende gecompliceerd onderwerp en ik ben geenszins een expert. Er is constant onderzoek gaande in dit gebied, in veel universiteiten en veiligheidsagentschappen.

In dit artikel zal ik proberen dingen zo eenvoudig mogelijk te houden, terwijl ik u een redelijk veilige methode presenteer voor het opslaan van wachtwoorden in een webtoepassing.


2. Wat doet "hasp"?

Hashing converteert een gegeven (klein of groot) in een relatief kort stukje gegevens, zoals een tekenreeks of een geheel getal.

Dit wordt bereikt door gebruik te maken van een one-way hash-functie. "One-way" betekent dat het erg moeilijk (of praktisch onmogelijk) is om het om te keren.

Een bekend voorbeeld van een hash-functie is md5 (), wat vrij populair is in veel verschillende talen en systemen.

$ data = "Hallo wereld"; $ hash = md5 ($ data); echo $ hash; // b10a8db164e0754105b7a99be72e3fe5

Met md5 (), het resultaat is altijd een 32-teken lange reeks. Maar het bevat alleen hexadecimale tekens; technisch gezien kan het ook worden gerepresenteerd als een geheel getal van 128 bits (16 bytes). Je kan md5 () veel langere strings en data, en je krijgt nog steeds een hash van deze lengte. Dit feit alleen al kan u een hint geven over waarom dit wordt beschouwd als een "one-way" -functie.


3. Een hash-functie gebruiken voor het opslaan van wachtwoorden

Het gebruikelijke proces tijdens een gebruikersregistratie:

  • De gebruiker vult het registratieformulier in, inclusief het wachtwoordveld.
  • Het webscript slaat alle informatie op in een database.
  • Het wachtwoord wordt echter door een hashfunctie geleid voordat het wordt opgeslagen.
  • De originele versie van het wachtwoord is nergens opgeslagen, dus wordt het technisch weggegooid.

En het inlogproces:

  • De gebruiker voert gebruikersnaam (of e-mail) en wachtwoord in.
  • Het script voert het wachtwoord uit via dezelfde hash-functie.
  • Het script vindt het gebruikersrecord uit de database en leest het opgeslagen gehashte wachtwoord.
  • Beide waarden worden vergeleken en de toegang wordt verleend als ze overeenkomen.

Zodra we een fatsoenlijke methode voor het hashen van het wachtwoord hebben gekozen, gaan we dit proces verderop in dit artikel implementeren.

Merk op dat het oorspronkelijke wachtwoord nooit ergens is opgeslagen. Als de database wordt gestolen, kunnen de aanmeldingen van de gebruiker niet worden gehackt, toch? Welnu, het antwoord is "het hangt ervan af". Laten we een paar mogelijke problemen bekijken.


4. Probleem # 1: botsing met hash

Een "botsing" van hash treedt op wanneer twee verschillende gegevensinvoeren dezelfde resulterende hash genereren. De waarschijnlijkheid dat dit gebeurt, hangt af van de functie die u gebruikt.

Hoe kan dit worden geëxploiteerd?

Ik heb bijvoorbeeld een aantal oudere scripts gezien die crc32 () gebruikten voor hash-wachtwoorden. Deze functie genereert een 32-bits geheel getal als resultaat. Dit betekent dat er slechts 2 ^ 32 (dat wil zeggen 4.294.967.296) mogelijke uitkomsten zijn.

Laten we een wachtwoord hashen:

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

Laten we nu de rol aannemen van iemand die een database heeft gestolen en de hash-waarde heeft. We kunnen 323322056 mogelijk niet converteren naar 'supersecretpassword', maar we kunnen een ander wachtwoord berekenen dat in dezelfde hash-waarde wordt omgezet, met een eenvoudig script:

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

Dit kan een tijdje duren, maar uiteindelijk moet het een tekenreeks retourneren. We kunnen deze teruggegeven string gebruiken - in plaats van 'supersecretpassword' - en het zal ons in staat stellen om succesvol in te loggen in het account van die persoon.

Nadat ik bijvoorbeeld een paar seconden lang dit exacte script op mijn computer had uitgevoerd, kreeg ik 'MTIxMjY5MTAwNg =='. Laten we het testen:

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

Hoe kan dit worden voorkomen?

Tegenwoordig kan een krachtige thuis-pc worden gebruikt om een ​​hashfunctie bijna een miljard keer per seconde uit te voeren. Dus we hebben een hash-functie nodig die een heel groot bereik.

Bijvoorbeeld, md5 () kan geschikt zijn, omdat het 128-bits hashes genereert. Dit vertaalt zich in 340.282.366.920.938.463.463.374.607.431.768.211.456 mogelijke uitkomsten. Het is onmogelijk om door zoveel herhalingen te lopen om botsingen te vinden. Sommige mensen hebben echter nog steeds manieren gevonden om dit te doen (zie hier).

SHA1

Sha1 () is een beter alternatief en genereert een nog langere 160-bits hash-waarde.


5. Probleem # 2: Rainbow Tables

Zelfs als we het probleem met de botsing oplossen, zijn we nog steeds niet veilig.

Een regenboogtabel wordt gebouwd door de hash-waarden van veel gebruikte woorden en hun combinaties te berekenen.

Deze tabellen kunnen zoveel als miljoenen of zelfs miljarden rijen bevatten.

U kunt bijvoorbeeld een woordenboek doorlopen en hash-waarden voor elk woord genereren. Je kunt ook beginnen met het combineren van woorden en het genereren van hashes voor die ook. Dat is niet alles; je kunt zelfs beginnen met het toevoegen van cijfers voor / na / tussen woorden, en ze ook in de tabel opslaan.

Rekening houdend met het feit dat goedkope opslag tegenwoordig is, kunnen gigantische Rainbow Tables worden geproduceerd en gebruikt.

Hoe kan dit worden geëxploiteerd?

Laten we ons voorstellen dat een grote database wordt gestolen, samen met 10 miljoen wachtwoord-hashes. Het is vrij eenvoudig om de regenboogtabel voor elk ervan te doorzoeken. Niet allemaal zullen ze worden gevonden, zeker, maar toch ... sommige zullen dat wel zijn!

Hoe kan dit worden voorkomen?

We kunnen proberen een "zout" toe te voegen. Hier is een voorbeeld:

$ wachtwoord = "easypassword"; // dit kan gevonden worden in een regenboogtabel // omdat het wachtwoord 2 gewone woorden echo sha1 ($ wachtwoord) bevat; // 6c94d3b42518febd4ad747801d50a8972022f956 // gebruik een stel willekeurige tekens en het kan langer zijn dan dit $ salt = "f # @ V) Hu ^% Hgfds"; // dit zal NIET gevonden worden in een pre-built rainbow table echo sha1 ($ salt. $ wachtwoord); // cd56a16759623378628c0d9336af69b74d9d71a5

Wat we in feite doen, is de "salt" -reeks aaneenschakelen met de wachtwoorden voordat ze worden versleept. De resulterende reeks zal uiteraard niet op een van de vooraf gebouwde regenboogtafels staan. Maar we zijn nog steeds niet veilig!


6. Probleem # 3: Rainbow Tables (opnieuw)

Vergeet niet dat een Rainbow-tabel helemaal opnieuw kan worden gemaakt nadat de database is gestolen.

Hoe kan dit worden geëxploiteerd?

Zelfs als een zout werd gebruikt, is dit mogelijk samen met de database gestolen. Het enige wat ze moeten doen is een nieuwe Rainbow-tafel maken, maar deze keer voegen ze het zout toe aan elk woord dat ze in de tabel zetten..

Bijvoorbeeld in een generieke Rainbow Table, "easypassword"bestaat misschien, maar in deze nieuwe Rainbow Table hebben ze"f # @ V) Hu ^% HgfdseasypasswordOok wanneer ze alle 10 miljoen gestolen gezouten hashes tegen deze tafel hebben uitgevoerd, zullen ze opnieuw enkele overeenkomsten kunnen vinden.

Hoe kan dit worden voorkomen?

We kunnen in plaats daarvan een 'uniek zout' gebruiken, dat voor elke gebruiker verandert.

Een kandidaat voor dit soort zout is de id-waarde van de gebruiker uit de database:

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

Dit gaat ervan uit dat het id-nummer van een gebruiker nooit verandert, wat meestal het geval is.

We kunnen ook een willekeurige reeks genereren voor elke gebruiker en die gebruiken als het unieke zout. Maar we moeten ervoor zorgen dat we dat ergens in het gebruikersrecord opslaan.

// genereert een 22 tekens lange willekeurige stringfunctie unique_salt () return-substraat (sha1 (mt_rand ()), 0,22);  $ unique_salt = unique_salt (); $ hash = sha1 ($ unique_salt. $ wachtwoord); // en sla het $ unique_salt op met het gebruikersrecord // ... 

Deze methode beschermt ons tegen Rainbow Tables, omdat nu elk afzonderlijk wachtwoord gezouten is met een andere waarde. De aanvaller zou 10 miljoen afzonderlijke Rainbow Tables moeten genereren, wat volledig onpraktisch zou zijn.


7. Probleem # 4: hash-snelheid

De meeste hash-functies zijn ontworpen met het oog op snelheid, omdat ze vaak worden gebruikt om checksum-waarden voor grote gegevenssets en bestanden te berekenen, om te controleren op gegevensintegriteit.

Hoe kan dit worden geëxploiteerd?

Zoals ik eerder al zei, kan een moderne pc met krachtige GPU's (ja, videokaarten) worden geprogrammeerd om ongeveer een miljard hashes per seconde te berekenen. Op deze manier kunnen ze een brute force-aanval gebruiken om elk mogelijk wachtwoord te proberen.

U denkt misschien dat een wachtwoord van minimaal 8 tekens lang mogelijk veilig is voor een brute aanval, maar laten we eens kijken of dat inderdaad het geval is:

  • Als het wachtwoord kleine letters, hoofdletters en cijfers kan bevatten, is dat 62 (26 + 26 + 10) mogelijke tekens.
  • Een lange reeks van 8 tekens heeft 62 ^ 8 mogelijke versies. Dat is iets meer dan 218 biljoen.
  • Met een snelheid van 1 miljard hashes per seconde, kan dat binnen ongeveer 60 uur worden opgelost.

En voor wachtwoorden met een lengte van 6 tekens, wat ook heel normaal is, zou het minder dan 1 minuut duren.

Voel je vrij om wachtwoorden van 9 of 10 tekens lang te maken, maar misschien irriteer je sommige van je gebruikers.

Hoe kan dit worden voorkomen?

Gebruik een langzamere hash-functie.

Stel je voor dat je een hash-functie gebruikt die maar 1 miljoen keer per seconde op dezelfde hardware kan worden uitgevoerd, in plaats van 1 miljard keer per seconde. Het zou de aanvaller dan 1000 keer langer duren om een ​​hash brute kracht te geven. 60 uur zou veranderen in bijna 7 jaar!

Een manier om dat te doen zou zijn om het zelf te implementeren:

function myhash ($ wachtwoord, $ unique_salt) $ salt = "f # @ V) Hu ^% Hgfds"; $ hash = sha1 ($ unique_salt. $ wachtwoord); // zorg ervoor dat het 1000 keer langer duurt voor ($ i = 0; $ i < 1000; $i++)  $hash = sha1($hash);  return $hash; 

Of u kunt een algoritme gebruiken dat een "kostenparameter" ondersteunt, zoals BLOWFISH. In PHP kan dit worden gedaan met behulp van de crypt() functie.

function myhash ($ wachtwoord, $ unique_salt) // het zout voor kogelvissen moet een crypt van 22 karakters lang zijn ($ wachtwoord, '$ 2a $ 10 $'. $ unique_salt); 

De tweede parameter voor de crypt() functie bevat enkele waarden gescheiden door het dollarteken ($).

De eerste waarde is '$ 2a', wat aangeeft dat we het BLOWFISH-algoritme zullen gebruiken.

De tweede waarde, in dit geval '$ 10', is de 'kostenparameter'. Dit is de logaritme van basis-2 van het aantal iteraties dat wordt uitgevoerd (10 => 2 ^ 10 = 1024 iteraties.) Dit aantal kan variëren van 04 tot 31.

Laten we een voorbeeld uitvoeren:

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

De resulterende hash bevat het algoritme ($ 2a), de kostenparameter ($ 10) en het 22-tekens zout dat werd gebruikt. De rest is de berekende hash. Laten we een test uitvoeren:

// neem aan dat dit uit de database is gehaald $ hash = '$ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; // neem aan dat dit het wachtwoord is dat de gebruiker heeft ingevoerd om zich aan te melden in $ password = "verysecret"; if (check_password ($ hash, $ password)) echo "Access Granted!";  else echo "Toegang geweigerd!";  functie check_password ($ hash, $ wachtwoord) // eerste 29 karakters bevatten algoritme, kosten en zout // laten we het $ full_salt $ full_salt = substr ($ hash, 0, 29) noemen; // voer de hash-functie uit op $ wachtwoord $ new_hash = crypt ($ wachtwoord, $ full_salt); // geeft true of false return terug ($ hash == $ new_hash); 

Wanneer we dit uitvoeren, zien we "Access Granted!"


8. Het samenvoegen

Laten we, met al het bovenstaande in gedachten, een utility class schrijven op basis van wat we tot nu toe hebben geleerd:

class PassHash // blowfish private static $ algo = '$ 2a'; // cost parameter private static $ cost = '$ 10'; // voornamelijk voor intern gebruik openbare statische functie unique_salt () retoursubstraat (sha1 (mt_rand ()), 0,22);  // dit wordt gebruikt om een ​​hash te genereren openbare statische functie hash ($ wachtwoord) return crypt ($ wachtwoord, zelf :: $ algo. self :: $ cost. '$'. self :: unique_salt ());  // dit wordt gebruikt om een ​​wachtwoord te vergelijken met een hash public static function check_password ($ hash, $ password) $ full_salt = substr ($ hash, 0, 29); $ new_hash = crypt ($ wachtwoord, $ full_salt); return ($ hash == $ new_hash); 

Hier is het gebruik tijdens gebruikersregistratie:

// include de class require ("PassHash.php"); // lees alle formulierinvoer van $ _POST // ... // voer uw normale formuliervalidatie uit // // // hash het wachtwoord $ pass_hash = PassHash :: hash ($ _ POST ['password']); // sla alle gebruikersinformatie op in de database, exclusief $ _POST ['wachtwoord'] // sla $ pass_hash op in plaats // ... 

En hier is het gebruik tijdens een gebruikersaanmeldproces:

// include de class require ("PassHash.php"); // lees alle formulierinvoer van $ _POST // ... // haal de gebruikersrecord op op basis van $ _POST ['gebruikersnaam'] of vergelijkbaar // ... // controleer het wachtwoord waarmee de gebruiker probeerde aan te melden met if (PassHash :: check_password ( $ user ['pass_hash'], $ _POST ['wachtwoord']) // toegang verlenen // ... else // toegang weigeren // ...

9. Een opmerking over Blowfish-beschikbaarheid

Het Blowfish-algoritme is mogelijk niet in alle systemen geïmplementeerd, hoewel het nu vrij populair is. U kunt uw systeem controleren met deze code:

if (CRYPT_BLOWFISH == 1) echo "Ja";  else echo "Nee"; 

Vanaf PHP 5.3 hoef je je echter geen zorgen te maken; PHP wordt geleverd met ingebouwde implementatie.


Conclusie

Deze methode om hashing-wachtwoorden te gebruiken, moet voor de meeste webtoepassingen solide zijn. Dat gezegd hebbende, vergeet niet: je kunt ook eisen dat je leden sterkere wachtwoorden gebruiken, door minimale lengtes, gemengde karakters, cijfers en speciale karakters op te leggen.

Een vraag aan u, lezer: hoe hash uw wachtwoorden? Kunt u verbeteringen aanbevelen bij deze implementatie??