Passwörter sicher speichern
Immer mehr Webseiten im Internet bieten eine die Möglichkeit, sich mit einem Benutzernamen bzw. der Emailadresse und Passwort anzumelden, um zum Beispiel Kommentare verfassen oder sein Profil in einem sozialen Netzwerk verwalten zu können. Möchte man auf seiner selbst entwickelten Webseite auch eine entsprechende Möglichkeit bieten, dass sich Besucher registrieren und einloggen können, muss man ein paar wichtige Punkte beachten.
Ein sehr wichtiger Punkt ist dabei das sichere Speichern des Passworts, welches der Nutzer gewählt hat. Da die meisten Nutzer sich nicht für jede Webseite ein neues Passwort überlegen und somit auch die Passwörter von den Nutzern von z.B. einem kleinem Forum übers Angeln oder Modellbau für einen Hacker entsprechend interessant sein können, um die entsprechende Kombination aus Benutzername/Email und Passwort auch auf anderen Webseiten wie Ebay, Paypal oder anderen Webseiten ausprobieren und je nach dem Zugriff auf kritische Bereiche erlangen zu können, was für den Nutzer entsprechend teuer und/oder ungemütlich enden könnte, muss man diese sehr sensiblen Daten entsprechend gut schützen.
Inhaltsverzeichnis
Passwörter sicher abspeichern
Für diesen Zweck liefert PHP ab Version 5.5 entsprechende Funktionen (für alle, denen noch kein PHP 5.5+ zur Verfügung steht, wird am Ende noch eine mögliche Alternative für die Nutzung genau dieser Funktionen vorgestellt), mit denen man Passwörter wirklich einfach sicher speichern kann, ohne die genauen technischen Hintergründe kennen zu müssen, die nicht so ganz trivial sind. Aus diesem Grund gehe ich hier nicht zu sehr auf technische Details ein, um vor allem Anfänger nicht direkt mit einem Wall an Informationen abzuschrecken, die sie für das eigentliche Problem „Passwörter sicher speichern“ nicht unbedingt brauchen, da diese Funktionen alles wichtige für einen übernehmen.
Wir werden uns drei einfache Funktionen genauer anschauen, mit denen man die Passwörter seiner Nutzer auch auf lange Zeit sicher speichern kann.
Als erste Funktion ist die Funktion password_hash($passwort, $algorithmus) sehr wichtig, die als ersten Parameter das Passwort erwartet und als zweiten Parameter einen Algorithmus, wobei hier einfach die Konstante PASSWORD_DEFAULT verwendet werden sollte. Diese Funktion erstellt von dem Passwort des Nutzers einen sogenannten Hash. Unter einem Hash kann man sich (stark vereinfacht) so etwas wie eine Quersumme einer Zahl vorstellen. Es wird eine Art „Fingerabdruck“ von diesem Passwort genommen, von dem man nicht ohne weiteres auf das Passwort schließen kann. So ist zum Beispiel die Quersumme von 1337 genau dieselbe, wie von zum Beispiel 4802, nämlich 14, aber 17081994 hat wiederum eine ganze andere, nämlich 39. Jedoch kann man nur anhand der Quersumme nicht ohne weiteres auf die zugrundeliegende Zahl schließen. So ähnlich (wie gesagt: stark vereinfacht) kann man sich einen Hash vorstellen, nur, dass bei einem Hash solche Werte, die denselben Hash als Ergebnis haben, bei weitem nicht so häufig und einfach wie bei einer einfachen Quersumme zu finden sind.
Beispiel:
1 2 3 4 5 6 7 |
<?php // Das gewählte Passwort des Nutzers z.B. bei der Registration $passwort = 'MeinGeheimesPasswortEox'; $hash = password_hash($passwort, PASSWORD_DEFAULT); // $hash beinhaltet nun den Hash, wie z.B. folgenden: // $2y$10$DFcky8xHBKK0XtMX1vCcL.ZDyIR5fe/hPNeFrfkSa.F599Fw2/hPi |
Die Webseite speichert dann nicht das vom Nutzer eingetragene Passwort, sondern nur den „Fingerabdruck“, also den Hash von diesem Passwort. Sollte man diesen Hash in einer Datenbank speichern, so wird empfohlen das entsprechende Feld bereits groß genug einzustellen (z.B. auf 255), da sich die Länge des Hashs mit der Zeit auf Grund eines neuen Standard-Hash-Algorithmus ändern kann (genauere Informationen findest du weiter unten).
Sollte dann ein Hacker an diese Daten kommen können, bleibt diesem nichts anderes übrig, als für jeden Nutzer, dessen Passwort er abgreifen möchte, alle möglichen Passwörter mit demselben Hash-Algorithmus zu hashen und den berechneten Hash mit dem jeweiligen Hash des Nutzers zu vergleichen. Bis man auf diese Weise das entsprechende Passwort findet, dauert es selbst mit einem sehr guten PC bei halbwegs guten Passwörtern bereits mehrere Jahrhunderte oder noch weit aus länger, je nach „stärke“ des Passworts. Und ja, da steht wirklich „Jahrhunderte“.
Nur wie prüft man nun, ob der Nutzer beim Einloggen das richtige Passwort eingegeben hat, ohne gegebenenfalls Jahrhunderte warten zu müssen?
Ganz einfach: Es wird auf dieselbe Weise von dem Passwort, welches der Nutzer beim Einloggen angibt ein Hash berechnet, wie es bei dem festlegen des Passworts (z.B. bei der Registration oder beim ändern des Passworts) der Fall war und vergleichen diesen mit dem gespeicherten Hash des Nutzers, der sich mit dem Passwort einloggen möchte. So müssen wir nur einen Hash berechnen, da wir nicht wissen wollen bzw. müssen, welches Passwort der Nutzer verwendet, sondern nur, ob es dasselbe Passwort ist, wie das, welches der Nutzer z.B. bei der Registration festgelegt hat.
Man könnte Vermuten, dass man dies also nun wie folgt lösen könnte:
1 2 3 4 5 6 7 8 9 10 |
<?php if( $gespeicherter_hash == password_hash($passwort, PASSWORD_DEFAULT) ) { // dies wird so nie der Fall sein echo 'Passwort stimmt!'; } else { echo 'Passwort ist falsch!'; } |
Jedoch liefert password_hash jedes Mal einen anderen Wert zurück, auch wenn man immer dasselbe Passwort verwendet.
Wieso ist das der Fall? Das liegt daran, dass in dem zurückgegebenen Wert nicht nur der eigentliche Hash gespeichert wird, sondern auch welcher Hash-Algorithmus verwendet wurde, wie oft diese Verwendet wurde und ein sogenannter „Salt“, welches einen Hacker zwingt, dass er für jeden Nutzer einzeln alle möglichen Passwörter durchgehen und deren Hash berechnen muss, um an das eigentliche Passwort von dem jeweiligen Nutzer zu kommen, was eben einen immensen Zeit- und Rechenaufwand bedeutet.
Hier kommt die zweite wichtige Funktion ins Spiel, die password_verify($passwort, $hash) heißt und als ersten Parameter das Passwort erwartet, welches auf die Richtigkeit geprüft werden soll (also z.B. welches der Nutzer beim Einloggen angegeben hat) und als zweiten Parameter den von der Webseite gespeicherten Hash.
Diese Funktion ermittelt zuerst die genutzt Konfiguration zur Erstellung des Hash-Wertes. Es ermittelt aus den ersten Zeichen des Hash-Wertes den Hash-Algorithmus, wie oft die Hash-Funktion angewendet wird und den Salt. Danach berechnet die Funktion mit der selben Konfiguration den Hash-Wert basierend auf dem Password und vergleicht dies mit dem gespeicherten Hashwert. Stimmen beide überein, so liefert die Funktion ein true zurück, andernfalls ein false.
Beispiel (z.B. beim Einloggen):
1 2 3 4 5 6 7 8 |
if( password_verify($passwort, $gespeicherter_hash) ) { echo 'Passwort stimmt!'; } else { echo 'Passwort ist falsch!'; } |
Beim Prüfen eines angegebenen Passworts eines Nutzers, wird also zum Vergleich mit dem gespeicherten Hash NICHT erneut password_hash zum berechnen des Hashs von dem angegebenen Passworts verwendet, weil diese selbst beim selben Passwort immer einen anderen Hash erzeugt, sondern das zu prüfende Passwort wird als erster und der gespeicherter Hash als zweiter Parameter der Funktion password_verify übergeben, welche bei einer Übereinstimmung true zurück liefert und andernfalls false.
Gespeicherte Hashes zukunftssicher machen
Nun wurde bereits alles erwähnt, was man dafür braucht, um Passwörter sicher speichern zu können. Jedoch gibt es eine dritte, sehr nützliche Funktion, mit der man sicherstellen kann, dass man immer einen möglichst sicheren Hash-Algorithmus für die gespeicherten Hashes verwendet, ohne den Code später noch einmal ändern zu müssen, wenn sich z.B. herausstellt, dass ein eigentlich als sicher geltender Hash-Algorithmus, den man für seine Hashes verwendet, nicht mehr als sicher gilt.
Weiter oben wurde bei der Funktion password_hash() die Konstante PASSWORD_DEFAULT verwendet, weil diese immer den Wert des Hash-Algorithmus beinhaltet, welcher von PHP in der jeweiligen Version als Standard vorschlägt, welcher entsprechend zur Zeit der Veröffentlichung der jeweiligen Version als sicher gilt.
Da sich dies mit der Zeit ändern kann, auch wenn es eher selten vorkommt, sollte man beim Speichern eines Hashs in einer Datenbank darauf achten, dass das jeweilige Feld/Attribute groß genug ist und das nicht nur zu der Zeit, wo man gerade das PHP-Skript geschrieben hat, sondern auch nach einem möglichen Austausch des Standard-Hash-Algorithmus. Aus diesem Grund würde ich empfehlen das Feld/Attribute für den Hash des Nutzers direkt auf z.B. eine mögliche Länge von 255 Zeichen zu setzen, um Fehler durch „abgeschnittene“ Hashes vorzubeugen.
Nun wird also bei jedem Aufruf von password_hash nun der jeweils zu der Zeit empfohlene Algorithmus verwendet, aber eben nur bei dem berechnen eines neuen Hashs, wie z.B. bei der Registration oder beim ändern des Passwortes.
Hier kommt die dritte und letzte Funktion ins Spiel, auf die ich hier eingehen werde. Diese heißt password_needs_rehash($hash, $algorithmus) und erwartet als ersten Parameter den jeweiligen Hash, der auf einen „veralteten“ Hash-Algorithmus überprüft werden soll, und als zweiten Parameter den aktuellen Algorithmus, der eigentlich verwendet werden sollte, also PASSWORD_DEFAULT.
Will sich ein Nutzer einloggen und stimmt sein Passwort, weil mit den Werten aus dem gespeicherten Hash derselbe Hash-Wert generiert werden kann, so testen wir diesen Hash anschließend (und nur dann!) auf seine Aktualität, mithilfe der password_needs_rehash Funktion und sollte diese true zurück liefern, ist ein neu genieren eines Hashs von dem Passwort des Nutzers empfehlenswert, weil der Standard-Hash-Algorithmus sich geändert hat, was eigentlich nur der Fall ist, wenn dieser besser ist als der alte Standard-Hash-Algorithmus. Und da wir ja z.B. beim Einloggen des Nutzers sein Passwort im Klartext zum überprüfen bekommen haben und sich dieses bereits als richtig herausgestellt hat, können wir dieses Passwort jetzt mit dem neuen Hash-Algorithmus hashen und den alten gespeicherten Hash mit dem neuen ersetzen, da dieser ja dann alle nötigen Informationen wie z.B. den verwendeten Hash-Algorithmus beinhaltet, die die password_verify-Funktion zum überprüfen eines Passworts braucht. Und das alles ohne dass der Nutzer aufgefordert werden muss sein Passwort zu ändern oder er überhaupt in irgendeiner Weise etwas von dem aktualisieren seines Hashs merkt.
Beispiel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php if( password_verify($passwort, $gespeicherter_hash) ) { // Passwort stimmt! if( password_needs_rehash($gespeicherter_hash, PASSWORD_DEFAULT) ) { // Passwort neu hashen $hash = password_hash($passwort, PASSWORD_DEFAULT); // den alten gespeicherten Hash durch den neuen ersetzen // z.B. in der Datenbank } echo 'Passwort stimmt!'; } else { echo 'Passwort ist falsch!'; } |
Alternative für PHP Versionen < 5.5
Da die vorgestellten Funktionen erst mit PHP 5.5 nativ von PHP unterstützt werden, muss man sich mit einer früheren Version von PHP erst einmal anders behelfen. Für diesen Fall gibt es ein PHP-Skript, welches man ganz normal wie jedes andere PHP-Skript per z.B. include() einbinden kann und welches einem genau diese Funktionen zur Verfügung stellt, wie sie hier vorgestellt wurden.
Dieses Skript steht per GitHub zur Verfügung: ircmaxell/password_compat
Dieses Skript setzt jedoch mindestens Version 5.3.7 voraus, welche zum jetzigen Zeitpunkt (30.7.2015) deutlich mehr Leuten zur Verfügung stehen dürfte, als PHP 5.5 und höher.
Und keine Sorge: Bei einem Update von PHP auf Version 5.5 oder höher wird es zu keinen Konflikten kommen, da dieses Skript entsprechend vorher prüft, ob die jeweiligen Funktionen bereits vorhanden sind, bevor es sie selbst implementiert. Man kann es also nach einem Update auf Version 5.5 oder höher einfach mit der Zeit wieder entfernen.
Autor: Tim Pangritz