Von Zeit zu Zeit werden Server und Datenbanken gestohlen oder gefährdet. In diesem Zusammenhang ist es wichtig sicherzustellen, dass einige wichtige Benutzerdaten, wie z. B. Kennwörter, nicht wiederhergestellt werden können. Heute lernen wir die Grundlagen für das Hashing und die notwendigen Schritte zum Schutz von Kennwörtern in Ihren Webanwendungen.
Erneut veröffentlichtes TutorialAlle paar Wochen besuchen wir einige der Lieblingsbeiträge unserer Leser aus der gesamten Geschichte der Website. Dieses Tutorial wurde erstmals im Januar 2011 veröffentlicht.
Kryptologie ist ein ausreichend kompliziertes Thema, und ich bin keineswegs ein Experte. In diesem Bereich wird ständig an vielen Universitäten und Sicherheitsbehörden geforscht.
In diesem Artikel werde ich versuchen, die Dinge so einfach wie möglich zu halten, während ich Ihnen eine relativ sichere Methode zum Speichern von Kennwörtern in einer Webanwendung vorstelle.
Durch Hashing wird ein kleines oder großes Datenstück in ein relativ kurzes Datenstück wie eine Zeichenfolge oder eine Ganzzahl umgewandelt.
Dies wird durch die Verwendung einer Einweg-Hash-Funktion erreicht. "Einweg" bedeutet, dass es sehr schwierig (oder praktisch unmöglich) ist, sie umzukehren.
Ein bekanntes Beispiel für eine Hash-Funktion ist md5 (), das in vielen verschiedenen Sprachen und Systemen sehr beliebt ist.
$ data = "Hallo Welt"; $ hash = md5 ($ data); echo $ hash; // b10a8db164e0754105b7a99be72e3fe5
Mit md5 ()
, Das Ergebnis ist immer eine Zeichenfolge mit 32 Zeichen. Es enthält jedoch nur hexadezimale Zeichen. Technisch kann es auch als 128-Bit-Ganzzahl (16 Byte) dargestellt werden. Du darfst md5 ()
viel längere Zeichenfolgen und Daten, und Sie erhalten immer noch einen Hash dieser Länge. Diese Tatsache allein kann Ihnen einen Hinweis geben, warum dies als "Einweg" -Funktion betrachtet wird.
Der übliche Vorgang während einer Benutzerregistrierung:
Und der Anmeldeprozess:
Wenn wir uns für eine anständige Methode für das Hashing des Passworts entschieden haben, werden wir diesen Prozess später in diesem Artikel implementieren.
Beachten Sie, dass das ursprüngliche Passwort nirgendwo gespeichert wurde. Wenn die Datenbank gestohlen wird, können die Benutzeranmeldungen nicht gefährdet werden, oder? Nun, die Antwort lautet "es kommt darauf an." Betrachten wir einige mögliche Probleme.
Eine Hash- "Kollision" tritt auf, wenn zwei verschiedene Dateneingaben denselben resultierenden Hash erzeugen. Die Wahrscheinlichkeit dafür hängt davon ab, welche Funktion Sie verwenden.
Als Beispiel habe ich einige ältere Skripte gesehen, die Passwörter mit crc32 () zum Hashing verwenden. Diese Funktion erzeugt als Ergebnis eine 32-Bit-Ganzzahl. Dies bedeutet, dass es nur 2 ^ 32 (d. H. 4.294.967.296) mögliche Ergebnisse gibt.
Lassen Sie uns ein Passwort eingeben:
echo crc32 ('supersecretpassword'); // Ausgaben: 323322056
Nehmen wir nun die Rolle einer Person an, die eine Datenbank gestohlen hat und den Hashwert hat. Wir können möglicherweise 323322056 nicht in 'supersecretpassword' konvertieren, wir können jedoch ein anderes Passwort herausfinden, das mit einem einfachen Skript in denselben Hashwert konvertiert wird:
set_time_limit (0); $ i = 0; while (wahr) if (crc32 (base64_encode ($ i)) == 323322056) echo base64_encode ($ i); Ausfahrt; $ i ++;
Dies kann eine Weile dauern, sollte jedoch irgendwann eine Zeichenfolge zurückgeben. Wir können diese zurückgegebene Zeichenfolge anstelle von 'supersecretpassword' verwenden. Dadurch können wir uns erfolgreich beim Konto dieser Person anmelden.
Nachdem ich zum Beispiel dieses exakte Skript für einige Momente auf meinem Computer ausgeführt hatte, erhielt ich 'MTIxMjY5MTAwNg ==
'. Lass es uns testen:
echo crc32 ('supersecretpassword'); // Ausgaben: 323322056 Echo crc32 ('MTIxMjY5MTAwNg =='); // Ausgaben: 323322056
Heutzutage kann mit einem leistungsstarken Heim-PC fast eine Milliarde Mal pro Sekunde eine Hash-Funktion ausgeführt werden. Wir brauchen also eine Hash-Funktion, die eine sehr große Auswahl.
Zum Beispiel, md5 ()
könnte geeignet sein, da es 128-Bit-Hashes erzeugt. Dies führt in 340.282.366.920.938.463.463.374.607.431.768.211.456 mögliche Ergebnisse. Es ist unmöglich, durch so viele Iterationen Kollisionen zu finden. Einige Leute haben jedoch noch Wege gefunden, dies zu tun (siehe hier)..
Sha1 () ist eine bessere Alternative und generiert einen noch längeren 160-Bit-Hashwert.
Selbst wenn wir das Kollisionsproblem beheben, sind wir noch nicht sicher.
Eine Regenbogentabelle wird erstellt, indem die Hashwerte häufig verwendeter Wörter und deren Kombinationen berechnet werden.
Diese Tabellen können bis zu Millionen oder sogar Milliarden Zeilen enthalten.
Sie können beispielsweise durch ein Wörterbuch gehen und Hashwerte für jedes Wort generieren. Sie können auch Wörter miteinander kombinieren und Hashes für diese erzeugen. Das ist nicht alles; Sie können sogar beginnen, Ziffern vor / nach / zwischen den Wörtern hinzuzufügen und sie auch in der Tabelle zu speichern.
In Anbetracht dessen, wie billig Speicher heutzutage ist, können gigantische Rainbow Tables produziert und verwendet werden.
Stellen wir uns vor, dass eine große Datenbank zusammen mit 10 Millionen Passwort-Hashes gestohlen wird. Es ist ziemlich einfach, den Regenbogentisch für jeden von ihnen zu durchsuchen. Sicherlich werden nicht alle von ihnen gefunden, aber trotzdem… einige von ihnen werden es tun!
Wir können versuchen, ein "Salz" hinzuzufügen. Hier ist ein Beispiel:
$ password = "easypassword"; // Dies kann in einer Regenbogentabelle gefunden werden //, weil das Kennwort 2 häufig verwendete Wörter enthält echo sha1 ($ password); // 6c94d3b42518febd4ad747801d50a8972022f956 // Verwenden Sie eine Reihe zufälliger Zeichen, und es kann länger sein als $ Salt = "f # @ V) Hu ^% Hgfds"; // Dies wird NICHT in einer vorgefertigten Regenbogentabelle gefunden. echo sha1 ($ salt. $ password); // cd56a16759623378628c0d9336af69b74d9d71a5
Was wir im Grunde tun, ist die Verkettung der "Salt" -String mit den Passwörtern vor dem Hashing. Die resultierende Zeichenfolge wird offensichtlich nicht auf einem vorgefertigten Regenbogentisch sein. Aber wir sind noch nicht sicher!
Denken Sie daran, dass ein Rainbow Table möglicherweise von Grund auf erstellt wird, nachdem die Datenbank gestohlen wurde.
Selbst wenn ein Salz verwendet wurde, wurde dieses möglicherweise zusammen mit der Datenbank gestohlen. Alles, was sie tun müssen, ist, einen neuen Rainbow Table von Grund auf zu generieren, aber dieses Mal verketten sie das Salz mit jedem Wort, das sie in den Tisch legen.
Zum Beispiel in einer generischen Rainbow Table "EasyPassword
"kann existieren. Aber in diesem neuen Rainbow Table haben sie"f # @ V) Hu ^% Hgfdseasypassword
"Auch. Wenn sie alle 10 Millionen gestohlenen gesalzten Hashes an diesem Tisch ausführen, werden sie wieder einige Übereinstimmungen finden können.
Wir können stattdessen ein "einzigartiges Salz" verwenden, das sich für jeden Benutzer ändert.
Ein Kandidat für diese Art von Salt ist der ID-Wert des Benutzers aus der Datenbank:
$ hash = sha1 ($ user_id. $ password);
Dabei wird davon ausgegangen, dass sich die ID-Nummer eines Benutzers niemals ändert, was normalerweise der Fall ist.
Wir können auch eine zufällige Zeichenfolge für jeden Benutzer generieren und diese als eindeutiges Salt verwenden. Aber wir müssen sicherstellen, dass wir das irgendwo im Benutzerdatensatz speichern.
// generiert einen 22 Zeichen langen Zufallsstring function unique_salt () return substr (sha1 (mt_rand ()), 0,22); $ unique_salt = unique_salt (); $ hash = sha1 ($ unique_salt. $ password); // und speichern Sie den $ unique_salt mit dem Benutzerdatensatz //…
Diese Methode schützt uns vor Rainbow Tables, da jetzt jedes einzelne Passwort mit einem anderen Wert gesalzen wurde. Der Angreifer müsste 10 Millionen separate Rainbow Tables generieren, was völlig unpraktisch wäre.
Die meisten Hashfunktionen wurden im Hinblick auf Geschwindigkeit entwickelt, da sie häufig zur Berechnung von Prüfsummenwerten für große Datensätze und Dateien verwendet werden, um die Datenintegrität zu überprüfen.
Wie ich bereits erwähnt habe, kann ein moderner PC mit leistungsstarken GPUs (ja, Videokarten) so programmiert werden, dass er ungefähr eine Milliarde Hash pro Sekunde berechnet. Auf diese Weise können sie einen Brute-Force-Angriff verwenden, um jedes einzelne mögliche Kennwort auszuprobieren.
Vielleicht denken Sie, dass ein Kennwort, das aus mindestens 8 Zeichen besteht, vor einem Brute-Force-Angriff schützen kann. Stellen Sie jedoch fest, ob dies tatsächlich der Fall ist:
Und bei 6 Zeichen langen Passwörtern, die auch häufig vorkommen, würde dies weniger als 1 Minute dauern.
Fühlen Sie sich frei, um 9 oder 10 Zeichen lange Passwörter anzufordern. Sie könnten jedoch einige Benutzer verärgern.
Verwenden Sie eine langsamere Hash-Funktion.
Stellen Sie sich vor, Sie verwenden eine Hash-Funktion, die auf derselben Hardware nur 1 Million Mal pro Sekunde ausgeführt werden kann, anstatt 1 Milliarde Mal pro Sekunde. Der Angreifer würde dann 1000 Mal länger brauchen, um einen Hash brutal zu erzwingen. 60 Stunden würden sich in fast 7 Jahre verwandeln!
Eine Möglichkeit, dies zu tun, wäre, es selbst zu implementieren:
function myhash ($ password, $ unique_salt) $ salt = "f # @ V) Hu ^% Hgfds"; $ hash = sha1 ($ unique_salt. $ password); // machen es 1000 mal länger für ($ i = 0; $ i < 1000; $i++) $hash = sha1($hash); return $hash;
Sie können auch einen Algorithmus verwenden, der einen "Kostenparameter" unterstützt, beispielsweise BLOWFISH. In PHP kann dies mit der Krypta()
Funktion.
function myhash ($ password, $ unique_salt) // Das Salz für Blowfish sollte 22 Zeichen lang sein und die Rückgabe-Verschlüsselung ($ password, '$ 2a $ 10 $'. $ unique_salt);
Der zweite Parameter zum Krypta()
Funktion enthält einige durch Dollarzeichen ($) getrennte Werte.
Der erste Wert ist '$ 2a', was bedeutet, dass wir den BLOWFISH-Algorithmus verwenden werden.
Der zweite Wert, in diesem Fall '$ 10', ist der "Kostenparameter". Dies ist der Logarithmus zur Basis 2, wie viele Iterationen ausgeführt werden (10 => 2 ^ 10 = 1024 Iterationen). Diese Zahl kann zwischen 04 und 31 liegen.
Lassen Sie uns ein Beispiel ausführen:
Funktion myhash ($ password, $ unique_salt) Rückgabewerte ($ password, '$ 2a $ 10 $'. $ unique_salt); function unique_salt () return substr (sha1 (mt_rand ()), 0,22); $ password = "verysecret"; echo myhash ($ password, unique_salt ()); // Ergebnis: $ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC
Der resultierende Hash enthält den Algorithmus ($ 2a), den Kostenparameter ($ 10) und den verwendeten 22-Zeichen-Salt. Der Rest ist der berechnete Hash. Lassen Sie uns einen Test ausführen:
// gehe davon aus, dass dies aus der Datenbank gezogen wurde $ hash = '$ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; // gehe davon aus, dass dies das Passwort ist, das der Benutzer eingegeben hat, um sich wieder in $ password = "verysecret" einzuloggen; if (check_password ($ hash, $ password)) echo "Zugriff gewährt!"; else echo "Zugriff verweigert!"; function check_password ($ hash, $ password) // Die ersten 29 Zeichen enthalten Algorithmus, Kosten und Salt. // Lassen Sie uns $ full_salt nennen. $ full_salt = substr ($ hash, 0, 29); // Die Hash-Funktion für $ password ausführen $ new_hash = crypt ($ password, $ full_salt); // gibt wahr oder falsch zurück ($ hash == $ new_hash);
Wenn wir das ausführen, sehen wir "Zugriff gewährt!"
Unter Berücksichtigung aller oben genannten Punkte schreiben wir eine Utility-Klasse basierend auf dem, was wir bisher gelernt haben:
class PassHash // blowfish private static $ algo = '$ 2a'; // Kostenparameter private static $ cost = '$ 10'; // hauptsächlich für den internen Gebrauch public static function unique_salt () return substr (sha1 (mt_rand ()), 0,22); // Dies wird verwendet, um eine statische Hash-Hashfunktion ($ password) zu generieren ($ password, self :: $ algo. self :: $ cost. '$'. self :: unique_salt ()); // Dies wird verwendet, um ein Passwort mit einer öffentlichen statischen Hash-Funktion zu vergleichen. check_password ($ hash, $ password) $ full_salt = substr ($ hash, 0, 29); $ new_hash = crypt ($ password, $ full_salt); return ($ hash == $ new_hash);
Hier ist die Verwendung während der Benutzerregistrierung:
// Include der Klasse requir ("PassHash.php"); // Lese alle Formulareingaben von $ _POST //… // mache deine regulären Formularüberprüfungen // // // Hash das Passwort $ pass_hash = PassHash :: Hash ($ _ POST ['password']); // Alle Benutzerinformationen in der Datenbank speichern, außer $ _POST ['password'] // // Stattdessen $ pass_hash speichern //…
Und hier ist die Verwendung während eines Benutzeranmeldeprozesses:
// Include der Klasse requir ("PassHash.php"); // Lesen Sie alle Formulareingaben von $ _POST //… // Abrufen des Benutzerdatensatzes basierend auf $ _POST ['username'] oder ähnlich // // // Überprüfen Sie das Kennwort, mit dem der Benutzer versucht hat, sich mit if (PassHash :: check_password $ user ['pass_hash'], $ _POST ['password']) // Zugriff gewähren //… else // Zugriff verweigern //…
Der Blowfish-Algorithmus ist möglicherweise nicht in allen Systemen implementiert, obwohl er mittlerweile recht beliebt ist. Sie können Ihr System mit diesem Code überprüfen:
if (CRYPT_BLOWFISH == 1) Echo "Ja"; else echo "Nein";
Ab PHP 5.3 müssen Sie sich jedoch keine Sorgen machen. PHP wird mit dieser integrierten Implementierung ausgeliefert.
Diese Methode zum Hashing von Passwörtern sollte für die meisten Webanwendungen ausreichend sein. Vergessen Sie nicht: Sie können auch verlangen, dass Ihre Mitglieder stärkere Kennwörter verwenden, indem Sie Mindestlängen, gemischte Zeichen, Ziffern und Sonderzeichen erzwingen.
Eine Frage an Sie, Leser: Wie gehen Sie Ihre Passwörter ein? Können Sie Verbesserungen gegenüber dieser Implementierung empfehlen??