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 zelfstudieOm 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.
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.
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.
Het gebruikelijke proces tijdens een gebruikersregistratie:
En het inlogproces:
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.
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.
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
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 () is een beter alternatief en genereert een nog langere 160-bits hash-waarde.
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.
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!
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!
Vergeet niet dat een Rainbow-tabel helemaal opnieuw kan worden gemaakt nadat de database is gestolen.
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 ^% Hgfdseasypassword
Ook wanneer ze alle 10 miljoen gestolen gezouten hashes tegen deze tafel hebben uitgevoerd, zullen ze opnieuw enkele overeenkomsten kunnen vinden.
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.
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.
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:
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.
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!"
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 // ...
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.
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??