Passwörter sorgen bei vielen für eine ordentliche Portion Stress. Egal ob Nutzer oder Entwickler, keiner mag sie so richtig und dementsprechend nachlässig ist man auch bei der Sicherheit von Passwörtern. Während ein Nutzer meist zu schwache Passwörter verwendet, die leicht erraten werden können, sind Entwickler nachlässig bei der sicheren Speicherung der Passwörter ihrer Nutzer.

Damit das aufhört, setzen wir uns genau damit auseinander: Wie bewahre ich sicher Passwörter auf, welchen Hashing-Algorithmus sollte ich nutzen, wie kann ich das Ganze konkret mit PHP umsetzen und was muss ich dabei beachten?

Diese und weitere Fragen werden im Folgenden beantwortet. Ein kleiner Tipp voraus: Der Schlüssel zum Erfolg ist die PHP Password Hashing API.

Passwortsicherheit - Ein Trauerspiel

Passwörter sind heute aus unserer digitalen Welt nicht mehr wegzudenken. Ob man es will oder nicht, jeder muss sich mittlerweile damit abfinden, den einen oder anderen Account inkl. Passwort zu besitzen, vorausgesetzt man möchte in irgendeiner Form am digitalen Leben teilhaben.

Aus der Sicht von vielen Nutzern richten Passwörter meist nur ein riesiges Chaos an und sind praktisch die Personifizierung von Stress. Wer kennt es schließlich nicht, wenn man mal wieder das Passwort eines wichtigen Accounts vergessen hat, sich das Passwort womöglich extra noch irgendwo aufgeschrieben hat, den Zettel aber einfach nicht unter dem riesigen Papierberg auf dem Schreibtisch finden kann oder sogar noch so schlau war und auf die Idee gekommen ist, den Zettel an einem besonders sicheren Ort zu verstecken?

Richtig, für viele ist das Passwort mit einer guten Portion Stress und Kontrollverlust verbunden, sodass man das Passwort, was eigentlich für die Sicherheit des Accounts sorgen soll, letztendlich nur noch verflucht und irgendwann schlicht ein extrem einfaches Passwort à la "123456" nutzt. Dass dies mit einem großen Sicherheitsrisiko verbunden ist, weil es nun selbst für jeden Menschen sehr leicht zu erraten ist, sollte eigentlich jedem klar sein. Eigentlich, und trotzdem existiert praktisch ein internationales Wettrennen um das schlechteste Passwort.

Doch welche Rolle spielen da eigentlich die Betreiber bzw. die Entwickler der Software? Nun, ja. Zum Ärger der Nutzer erzwingen sie beispielsweise ein Passwort mit mindestens 8 Zeichen, bestehend aus Zahlen, Klein- und Großbuchstaben sowie Sonderzeichen. Leider, oder zum Glück, - kommt eben auf den Standpunkt an - ist die Zahl der Websites und Applikationen mit solchen Vorgaben eher noch gering.

Womöglich größer ist da noch die Anzahl der Entwickler, die komplett rücksichtslos mit dem Thema Passwortsicherheit umgehen und z.B. auf MD5 als Hashing-Algorithmus setzen oder die Passwörter sogar einfach im Klartext speichern. Jeder, der sich zumindest ein bisschen auskennt, sollte wissen, dass dies alles andere als sicher ist. Ein unerlaubter Zugriff auf die eigene Datenbank und alle Nutzerkonten sind gefährdet. Sowas könnte spätestens mit der DSGVO auch zu ernsthaften rechtlichen Konsequenzen führen.

Wie wir das alles nach Möglichkeit verhindern können, schauen wir uns jetzt an.

So geht es richtig: Sicheres Passwort-Hashing

Wie bereits angedeutet, ist der Schlüssel zum Erfolg das Hashen von Passwörtern.

Was ist Passwort-Hashing?

Passwort-Hashing ist ein Prozess, bei dem aus einer beliebig langen Zeichenkette, also dem Passwort, ein Wert mit einer vorgegebenen Länge berechnet wird. Dies erfolgt durch eine sogenannte Hashfunktion oder einem entsprechenden Algorithmus. Das Ergebnis nennt man Hashwert.

Wesentlich für eine Hashfunktion sind folgende Eigenschaften:

  • Eine Hashfunktion ist eine Einwegfunktion. Aus dem berechneten Hashwert kann also nicht mehr die ursprüngliche Eingabe errechnet werden. Anders gesagt: Es lässt sich nicht zurückverwandeln bzw. umkehren.
  • Aufgrund der beliebigen Länge des Eingabewerts und der begrenzten Länge des Ausgabewerts können verschiedene Eingaben zu demselben Ergebnis führen (Kollision). Gute Hashfunktionen verursachen weniger bzw. möglichst gar keine Kollisionen.
  • Ähnliche Eingabewerte erzeugen ganz andere Hashwerte, sodass z.B. anhand der Hashwerte keine Rückschlüsse auf die Ähnlichkeit zweier Passwörter möglich sind.

Zur Veranschaulichung, hier noch ein Beispiel mit der kryptografischen Hashfunktion SHA-256. Für die Eingabe "Hello World!" erhalten wir folgenden Hashwert:

7F83B1657FF1FC53B92DC18148A1D65DFC2D4B1FA3D677284ADDD200126D9069

SHA-256 wurde übrigens von der NSA entwickelt, falls es jemanden interessiert. Ja, genau. Die, die unsere Bundeskanzlerin Angela Merkel ausspioniert haben. ;D

Eine kryptographische bzw. kryptologische Hashfunktion bezeichnet übrigens eine spezielle Form einer Hashfunktion, welche kollisionsresistent ist. Somit ist es praktisch unmöglich, zwei unterschiedliche Eingabewerte zu finden, die einen identischen Hashwert ergeben.

Warum sollte ich Passwörter hashen?

Passwörter vor dem Abspeichern in der Datenbank zu hashen, ist deshalb so wichtig, weil man es einem möglichen Angreifer, der Zugriff zur Datenbank erlangt, damit erschwert, die ursprünglichen Passwörter herauszufinden. Aus einem Hashwert lässt sich schließlich das Passwort nicht einfach ablesen und das Zurückrechnen ist durch die Konstruktion der Hashfunktion als eine Einwegfunktion nicht möglich.

Jetzt fragen sich manche natürlich, wie sie dann selber die Passwörter mit der Eingabe des Benutzers beim Login vergleichen können, richtig? Das geht ganz einfach. Hierfür muss schlicht erneut der Hashwert berechnet und im Anschluss mit dem Hashwert in der Datenbank verglichen werden. Stimmen die Hashwerte überein, hat der User das richtige Passwort eingegeben.

Wichtig zu wissen ist beim Hashen von Passwörtern allerdings auch, dass diese auch ihre Grenzen haben. Passwort-Hashing erschwert nämlich lediglich das Auslesen der Passwörter aus dem Datenspeicher, also z.B. aus der Datenbank oder einer Datei. Schafft es ein Angreifer hingegen, Code in eure Anwendung einzuschleusen, womit er die Passwörter noch vor dem Hashen auslesen kann, habt ihr ein ziemlich großes Problem. In einem solchen Fall hilft das Hashen von Passwörtern natürlich nicht, da es nur eine begrenzt sichere Möglichkeit darstellt, Passwörter zu speichern.

Warum sollte ich MD5 und Co. nicht für die Speicherung von Passwörtern nutzen?

Hashing-Algorithmen wie MD5, SHA-1 und SHA-256 sollten niemals für das Hashen von Passwörtern genutzt werden. Abgesehen davon, dass die NSA bei SHA-256 ihre Finger mit ihm Spiel hatte und dies allein für manche sicher schon Grund genug ist, sind solche Hashing-Algorithmen auf Geschwindigkeit und Effizienz optimiert. Dies hat zur Folge, dass es mit modernen Techniken und leistungsstarker Hardware kein sonderlich großes Kunststück mehr ist, diese Hashing-Algorithmen z.B. mit sogenannten Brute-Force-Attacken anzugreifen. Des Weiteren existieren teilweise schon riesige Listen mit Passwörtern und Zeichenkombinationen inkl. derem zugehörigen Hashwert.

Hat ein Angreifer also Zugang zu eurer Datenbank mit den Passwörtern, die mit der Hashfunktion MD5 gehasht wurden, ist es ein leichtes Spiel, die Hashwerte aus der Liste mit den Hashwerten in der Datenbank zu vergleichen. Findet man eine Übereinstimmung, so kennt man dank der Liste das Passwort. Solche Listen werden auch Rainbow Tables, also Regenbogentabellen genannt.

Wie sollte ich meine Passwörter stattdessen hashen?

Ein wesentliches Kriterium bei der Auswahl eines geeigneten Passwort-Hashing-Algorithmus ist der Berechnungsaufwand. Desto länger es braucht, einen Hash zu berechnen, desto länger braucht z.B. auch eine Brute-Force-Attacke. Zu beachten gilt hier jedoch, dass dieser Berechnungsaufwand sich nicht allzu negativ auf die Performance der Anwendung auswirkt. Es muss also ein gutes Mittelmaß gefunden werden.

Ein wenig Salz und Pfeffer...

Ebenfalls sehr wichtig beim richtigen Hashen von Passwörtern ist der Einsatz von einem sogenannten Salt. Das Salt (Salz) ist praktisch eine zufällige Zeichenkette, die beim Hashen zu dem Passwort hinzugefügt wird. Dank dieser zufälligen Zeichenkette lässt sich die Erstellung bzw. Nutzung von Rainbow Tables verhindern, da durch das Salt selbst gleiche Passwörter auch unterschiedliche Hashwerte erhalten.

Dies führt, wie gesagt, dazu, dass sich keine Liste mit Passwörtern und zugehörigen Hashwerten mehr heranziehen lässt, um die gehashten Passwörter in eurer Datenbank mit den Hashwerten aus der Liste zu vergleichen und so im Falle einer Übereinstimmung das Passwort herauszufinden, weil jeder Hashwert durch das zugefügte Salz praktisch einzigartig wird. Somit ist es egal, wenn 100 Nutzer das gleiche Passwort benutzen. Anhand der Hashwerte ließe sich das dann nämlich gar nicht mehr herausfinden.

Neben dem Salt gibt es auch noch das sogenannte Pepper. Das Pepper (Pfeffer) wird vor dem Berechnen des Hashwertes an das Passwort angehangen und setzt sich ebenfalls aus einer möglichst schwer zu erratenen Zeichenkette auseinander. Der wesentliche Unterschied zum Salt ist, dass der Pfeffer geheim ist. Während das Salt mit in der Datenbank gespeichert wird, meist zusammen mit dem eigentlichen Hashwert des Passworts, wird das Pepper an einem sicheren Ort gespeichert und gilt in der Regel für alle Passwörter.

Der Vorteil an der Hinzufügung eines Peppers ist, dass dadurch schwache Passwörter deutlich verstärkt werden, da eben das Passwort sozusagen um den Pfeffer erweitert wird. Lautete das Passwort zuvor "passwort", sieht es danach vielleicht so aus: "passwortbtef7rt786r3gb33tr3rg7633uzvbfdv".

Letztlich führt diese Vorgehensweise dazu, dass bei einem Vorfall, bei dem ein Angreifer Zugriff auf die Datenbank und damit die gehashten Passwörter erhält, auch ein Wörterbuchangriff (Dictionary Attack), bei dem praktisch einfache Passwörter wie "123456" und "passwort" durchprobiert werden, nicht mehr funktioniert.

Der Grund dafür liegt schlicht und ergreifend in der Tatsache, dass die Hashwerte in der Datenbank nicht mehr zu dem schwachen Passwort gehören. Stattdessen wurden sie basierend auf unserem schwachen Passwort plus dem starken Pfeffer erstellt.

Wichtig zu wissen ist, dass auch ein Pepper natürlich nichts bringt, wenn der Angreifer Kontrolle über den Server erlangt und so z.B. auch das Pepper kennt.

Seit PHP 5.5 bietet PHP die native Password Hashing API an. Mit dieser ist es extrem einfach, Passwörter sicher zu hashen sowie beim Login die Richtigkeit zu verifizieren. So übernimmt diese beispielsweise auch die Generierung des Salts. Dazu später mehr.

Bcrypt: Der Industriestandard unter den Passwort-Hashing-Funktionen

Der empfohlene Algorithmus ist übrigens Blowfish, genauer gesagt die kryptographische Hashfunktion bcrypt, welche auf diesem Verschlüsselungs-Algorithmus basiert und speziell auf das Hashen von Passwörtern spezialisiert ist.

Bcrypt ist der Standardalgorithmus von der PHP Password Hashing API und ist zumindest aktuell noch der Industriestandard, wenn es um das sichere Speichern von Passwörtern geht.

Bcrypt ist deswegen ideal, weil es viel Rechenzeit benötigt und somit Brute-Force-Angriffe und das Erstellen von Rainbow Tables deutlich erschwert. Darüber hinaus lässt sich der Algorithmus dank seines sogenannten Kostenfaktors gut skalieren. Mit diesem Kostenfaktor kann nämlich entschieden werden, wie wie viel Zeit die Berechnung eines Hashwerts benötigt. Werden die Computer mit der Zeit also immer leistungsfähiger, lässt sich der Kostenfaktor erhöhen und bcrypt sollte weiterhin vergleichsweise sicher sein.

Konkret bestimmt der Kostenfaktor, wie oft die bcrypt-Funktion hintereinander aufgerufen wird. Diese Anzahl der "Runden" errechnet sich folgendermaßen: rounds = 2x. x ist der Kostenfaktor, also z.B. 10. In diesem Fall hätten wir 1024 Durchgänge. Würden wir den Kostenfaktor um eins erhöhen, erhalten wir die doppelte Anzahl an Durchgängen, nämlich 2048.

Weitere gute Passwort-Hashing-Algorithmen

Der Grund, warum ich eben bcrypt als "aktuell noch der Industriestandard", vorgestellt habe, ist, dass es noch ein paar andere Algorithmen gibt, die sich ähnlich gut oder eventuell sogar besser für das Hashen von Passwörtern eignen.

Zu empfehlen sind aktuell Folgende:

Besonders letzterem könnte man sozusagen durchaus eine glorreiche Zukunft voraussagen. Argon2 ist der Gewinner der Password Hashing Competition (2013-2015) und wurde in diesem Zuge als neuer Algorithmus zum Hashen mit Passwörtern empfohlen. Mit PHP 7.2 erhielt dieser Einzug in die Password Hashing API von PHP und könnte zukünftig eventuell bcrypt als Standard-Algorithmus ablösen.

Näher darauf eingehen werde ich in diesem Artikel nicht. Wer an mehr Informationen zu diesem Thema interessiert ist, kann sich mal meinen Artikel zu den Neuerungen von PHP 7.2 durchlesen. Dort gehe ich etwas ausführlicher auf den neuen Algorithmus ein.

PHP Password Hashing API

Kommen wir nun endlich zum praktischen Teil. Zum sicheren Hashen von Passwörtern mit PHP sollte heutzutage stets die native Password Hashing API verwendet werden, welche mit PHP 5.5 eingeführt wurde.

Falls jemand noch mit einer geringeren PHP-Version arbeitet: Bitte upgraden, z.B. auf PHP 7.2! Es gibt zwar auch Implementationen für niedrigere Versionen, aber es gibt schlicht und ergreifend keinen Grund mehr, warum man weiterhin auf PHP 5.4.x oder sogar niedriger setzen sollte.

Vorausgesetzt ihr gehört zu den Leuten, die immer schön ihr System pflegen und upgraden, herzlichen Glückwunsch. Habt ihr PHP 7.2 könntet ihr sogar schon in den Genuss von Argon2 kommen. Aber wir schweifen ab...

Also, die PHP Password Hashing API bringt insgesamt vier Funktionen mit:

Wir schauen uns die vier Funktionen der Reihe nach an und starten mit password_get_info().

password_get_info()

Die Funktion password_get_info() kann dafür verwendet werden, um Informationen über einen Hash zu erhalten. Als einziges Argument wird also der Hashwert als String eingeführt. Wird dieser Hash von der Password Hashing API unterstützt, wird ein Array mit Informationen zum Hash zurückgegeben. Der Hash muss also von der Funktion password_hash() erzeugt worden sein.

Das Array, das zurückgegeben wird, ist assoziativ und enthält folgende drei Elemente:

  • 'algo', eine der Passwort-Algorithmus-Konstanten
  • 'algoName', der lesbare Name des Algorithmus
  • 'options', die Optionen, die der Funktion password_hash() übergeben wurden

Ein Beispiel:

<?php

$hash = '$2y$12$36IIK7w/kcmOvZQtVtmB8.SHFaH/UVF6jIYpRra7SFPgrjvsKZc2m';

echo '<pre>', var_dump(password_get_info($hash)), '</pre>';

/*
    Ausgabe:
    
    array(3) {
        ["algo"] => int(1)
        ["algoName"] => string(6) "bcrypt"
        ["options"] => array(1) {
            ["cost"] => int(12)
        }
    }
*/

Ich bin ganz ehrlich: Ich habe die Funktion noch nie gebraucht und wüsste eigentlich auch nicht so wirklich, wofür man sie nutzen sollte. Machen wir weiter...

password_hash()

Die password_hash()-Funktion ist hingegen deutlich interessanter. Sie ist dafür zuständig, den Passwort-Hash zu erstellen. Hierfür müssen wir als Argument das Passwort als Zeichenkette sowie den Algorithmus angeben, den wir verwenden möchten. Optional können wir als drittes Argument auch noch ein Array mit Optionen mitgeben.

Zur Verfügung stehen aktuell folgende Algorithmus-Konstanten:

  • PASSWORD_DEFAULT - Benutzt den bcrypt-Algorithmus, wird aber mit der Zeit angepasst.
  • PASSWORD_BCRYPT - Möglichkeit, explizit den bcrypt-Algorithmus auszuwählen.
  • PASSWORD_ARGON2I - Möglichkeit, explizit den Argon2-Algorithmus auszuwählen (ab PHP 7.2).

Eine der Konstanten wird als zweites Argument angegeben, um den entsprechenden Algorithmus auszuwählen.

Für das Array mit Optionen lassen sich bei bcrypt, aktuell PASSWORD_DEFAULT sowie PASSWORD_BCRYPT, folgende Anpassungen vornehmen:

  • 'salt' (string) - Nutzung eines eigenen Salts, seit PHP 7 "deprecated"
  • 'cost' (int) - Der Kostenfaktor, standardmäßig 10

Beim Salt ist es wichtig zu wissen, dass man diese Option tatsächlich nie nutzen sollte. PHP generiert standardmäßig einen sicheren, zufälligen Salt. Diese Option ist nicht umsonst seit PHP 7 als "deprecated" eingestuft und sollte nicht mehr verwendet werden. Jedes Mal, wenn ihr Gebrauch von dieser Option macht, sollte PHP also eine Warnung aussprechen, vorausgesetzt ihr seid nicht in Besitz einer geringeren Version. Aber auch für PHP 5 gilt, nicht nutzen!

Wer auf Argon2 setzen möchte, kann sich gerne über die verfügbaren Optionen in der PHP-Dokumentation informieren oder, wie schon gesagt, einfach meinen Artikel über PHP 7.2 lesen. Ich habe mich dazu entschieden, Argon2 aus diesem Artikel größtenteils rauszulassen, damit es nicht viel zu viel wird.

Schauen wir uns nochmal die Parameter der password_hash()-Funktion an, müssen wir übrigens darauf achten, dass wir beim Passwort nicht die Länge von 72 Zeichen überschreiten. In diesem Fall würde beim Einsatz des bcrypt-Algorithmus das Passwort des Nutzers nämlich gekürzt, was es dringend zu verhindern gilt. Dies sollte man also bei der Validierung der Benutzereingaben während der Registrierung beachten.

Zu den Rückgabewerten lässt sich noch sagen, dass password_hash() den Hashwert als String zurückgibt. Im Fehlerfall wird false zurückgeliefert. Wichtig zu wissen ist dabei, dass die Funktion password_hash() unter der Verwendung von bcrypt immer einen String bestehend aus genau 60 Zeichen erzeugt.

Sollte man die Konstante PASSWORD_DEFAULT nutzen, ist zu empfehlen, die Größe des Datenbankfeldes nicht auf 60 Zeichen zu begrenzen, da der eingesetzte Algorithmus sich bei dieser Konstante in Zukunft ändern kann. Auf der sicheren Seite ist man mit 255 Zeichen.

Nutzt man hingegen definitiv den bcrypt-Algorithmus durch die Konstante PASSWORD_BCRYPT, ist es eine Überlegung wert, die Größe hingegen auf 60 Zeichen zu begrenzen, um Speicherplatz zu sparen.

Zum Abschluss ist hier noch ein Beispiel für den Einsatz der password_hash()-Funktion:

<?php

const COST = 12;
const PEPPER = '.m9h-RL=^M/72;tdU\Bz';

echo password_hash('ilovecats123' . PEPPER, PASSWORD_BCRYPT, [
    'cost' => COST
]);

// Ausgabe: $2y$12$36IIK7w/kcmOvZQtVtmB8.SHFaH/UVF6jIYpRra7SFPgrjvsKZc2m

password_needs_rehash()

Die Funktion password_needs_rehash() ist ebenfalls Teil der Password Hashing API und kann dafür genutzt werden, um zu überprüfen, ob ein übergebener Hash mit den übergebenen Optionen (Hash-Algorithmus und Optionen wie Kostenfaktor) übereinstimmt.

Ist dies nicht der Fall, nimmt die Funktion an, das ein erneutes Hashen notwendig ist. Kommt es zu dieser Entscheidung, gibt die Funktion true zurück. Ist der Hash noch auf dem neuesten Stand, wird false zurückgegeben.

Die Parameter stimmen im Grunde mit der Funktion password_hash() überein, abgesehen davon, dass statt dem Passwort im Klartext der Hash übergeben wird. Der Hash muss natürlich von der Funktion password_hash() erzeugt worden sein.

Ein mögliches Szenario, wofür diese Funktion gemacht ist, ist das regelmäßige Aktualisieren der Passwort-Hashes auf den neuesten Sicherheitsstand. So könnte man beispielsweise bei jedem Login (oder alle paar Wochen/Monate beim Login) mit password_needs_rehash() abfragen, ob sich z.B. der Kostenfaktor geändert hat oder im Falle von PASSWORD_DEFAULT nun ein neuer Algorithmus eingesetzt wird. Ist dies der Fall, erstellt man mit password_hash() einen neuen Hash. Wichtige Voraussetzung dafür: Das Passwort muss im Klartext vorhanden sein, was natürlich nur nach einer Eingabe des Passworts beim Login der Fall ist und auch nur der Fall sein sollte!

Hier ist ein Beispiel für einen solchen Anwendungsfall:

<?php

const COST = 12;
const PEPPER = '.m9h-RL=^M/72;tdU\Bz';

$password = 'ilovecats123'; // Passwort, das vom Nutzer beim Login eingegeben wurde
$hash = '$2y$12$36IIK7w/kcmOvZQtVtmB8.SHFaH/UVF6jIYpRra7SFPgrjvsKZc2m'; // Hash aus DB

$options = [
    'cost' => COST
];

// Passwort mit Hash aus DB vergleichen
if (password_verify($password . PEPPER, $hash)) {
    // Ist Passwort-Hash nicht mehr auf dem neuesten Stand?
    if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options)) {
        // erstelle neuen Hash
        $new_hash = password_hash($password . PEPPER, PASSWORD_DEFAULT, $options);
        
        // Hash in DB aktualiseren
    }

    // User einloggen
}

password_verify()

Die vierte und letzte Funktion der PHP Password Hashing API ist password_verify(). Mit password_verify() lässt sich überprüfen, ob ein Passwort und ein Hash zusammenpassen.

Die Funktion ist gegen Timing-Angriffe abgesichert und sollte deswegen ausnahmslos zum Verifizieren des Passworts genutzt werden.

Als ersten Parameter erwartet password_verify() das Passwort als String, welches vom Benutzer eingegeben wurde. Als zweites Argument wird dann der Hash aus der Datenbank, ebenfalls in Form einer Zeichenkette, übergeben.

Der Rückgabewert der Funktion ist entweder true oder false, basierend darauf, ob das Passwort richtig ist oder nicht.

Nun noch ein Beispiel:

<?php

const COST = 12;
const PEPPER = '.m9h-RL=^M/72;tdU\Bz';

$password = 'ilovecats123'; // Passwort, das vom Nutzer beim Login eingegeben wurde
$hash = '$2y$12$36IIK7w/kcmOvZQtVtmB8.SHFaH/UVF6jIYpRra7SFPgrjvsKZc2m'; // Hash aus DB

$options = [
    'cost' => COST
];

// Passwort mit Hash aus DB vergleichen
if (password_verify($password . PEPPER, $hash)) {
  // User einloggen
} else {
  // Passwort falsch
}

Den richtigen Kostenfaktor finden

Je nachdem, wie leistungsfähig euer Server ist, ist es ggf. nötig, den Kostenfaktor der password_hash()-Funktion in Verbindung mit bcrypt anzupassen, um ein gesundes Mittelmaß zwischen Performance und Sicherheit zu finden. Denn: Desto höher der Kostenfaktor, desto stärker ist der Hash. Gleichwohl gilt aber auch: Desto höher der Kostenfaktor, desto länger muss der Nutzer bei der Registrierung darauf warten, dass sein Passwort erfolgreich gehasht wurde.

Mit dem folgenden Script lässt sich ein geeigneter Kostenfaktor sehr einfach ermitteln. Der Code stammt aus dem PHP-Handbuch:

<?php
/**
* This code will benchmark your server to determine how high of a cost you can
* afford. You want to set the highest cost that you can without slowing down
* you server too much. 8-10 is a good baseline, and more is good if your servers
* are fast enough. The code below aims for ≤ 50 milliseconds stretching time,
* which is a good baseline for systems handling interactive logins.
*/
$timeTarget = 0.05; // 50 milliseconds 

$cost = 8;
do {
    $cost++;
    $start = microtime(true);
    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
} while (($end - $start) < $timeTarget);

echo "Appropriate Cost Found: " . $cost;

Ihr müsst oben lediglich die Variable $timeTarget entsprechend eurer Wünsche anpassen und das Script anschließend auf eurem Server ausführen. Viel Spaß beim Ausprobieren!

Ich persönlich halte die 50 Millisekunden übrigens für ein etwas zu geringes Ziel. Es sollte in den meisten Fällen kein Problem sein, wenn ein Nutzer auch mal 100-500 Millisekunden auf die Registrierung warten muss. Die 500 Millisekunden würde ich allerdings definitiv nicht überschreiten und sind vielleicht etwas viel, schließlich muss man auch an Spitzenlastzeiten auf dem eigenen Server denken. Dann dauert das Ganze natürlich ggf. etwas länger. Gut sind wahrscheinlich ca. 200 Millisekunden, aber das ist lediglich meine Ansicht der Dinge. Da kann man sich sicherlich drüber streiten.

Alles zusammen: Vollständiges Beispiel

Bevor wir nun zum Abschluss dieses Artikels kommen, bringen wir nochmal alles zusammen, damit das Ganze unfallfrei in der Praxis angewandt werden kann.

Dass der Code nicht einfach so kopiert werden kann, sollte übrigens klar sein. Das Ganze würde man natürlich normalerweise in unterschiedliche Dateien, Klassen, Funktionen usw. aufteilen, je nachdem wie eben die Anwendung strukturiert ist.

Wer noch etwas tiefer in dieses Thema einsteigen möchte und nicht versteht, wie man die PHP Password Hashing API tatsächlich in der Praxis einsetzt, kann z.B. mal tiefer in den Quellcode meines Login-Scripts einsteigen. Da werdet ihr fündig.

Hier ist nun das finale Beispiel:

<?php

/**
* config.php
*
* Eine Art Konfigurationsdatei, wo z.B. auch API Keys und Datenbankzugangsdaten hinterlegt werden.
* Es kann sich hierbei genauso um eine .xml oder .json-Datei handeln.
* Des Weiteren ist es möglich, sowas in den Umgebungsvariablen abzulegen.
*/

const PEPPER = '.m9h-RL=^M/72;tdU\Bz';
const COST = 12;

/* config.php */

/**
* register.php
*
* Seite für den Registrierungsprozess.
* Höchstwahrscheinlich in der Praxis anders strukturiert.
* 
* Wichtig: Bei der Validierung der Nutzereingaben nicht vergessen, Passwort auf 72 Zeichen zu begrenzen,
* vorausgesetzt bcrypt wird genutzt.
*/

// Alle Nutzereingaben wurden erfolgreich geprüft, Nutzer kann registriert werden

$password = 'ilovecats123'; // Passwort aus Registrierungsformular

// Passwort-Hash für Nutzer erstellen, Pepper hinzufügen!
$hash = password_hash($password . PEPPER, PASSWORD_BCRYPT, [
'cost' => COST    
]);

if (!$hash) {
  // irgendwas ist beim Erstellen des Hashes schiefgegangen, Fehlermeldung an Nutzer
}

// Alles hat funktioniert, Nutzer in DB speichern inkl. Passwort-Hash

/* register.php */

/**
* login.php
*
* Seite für den Loginprozess.
* Höchstwahrscheinlich in der Praxis anders strukturiert.
*/

// Alle Nutzereingaben (außer Passwort) wurden erfolgreich geprüft, Passwort kann überprüft werden, um Nutzer dann einzuloggen

$password = 'ilovecats123'; // Passwort, das vom Nutzer beim Login eingegeben wurde
$hash = '$2y$12$36IIK7w/kcmOvZQtVtmB8.SHFaH/UVF6jIYpRra7SFPgrjvsKZc2m'; // Hash aus DB

$options = [
    'cost' => COST
];

// Passwort mit Hash aus DB vergleichen, Pepper nicht vergessen!
if (password_verify($password . PEPPER, $hash)) {
    // Ist Passwort-Hash nicht mehr auf dem neuesten Stand?
    if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options)) {
        // erstelle neuen Hash, Pepper wieder hinzufügen!
        $new_hash = password_hash($password . PEPPER, PASSWORD_DEFAULT, $options);
        
        // Hash in DB aktualiseren
    }

    // User einloggen
}

/* login.php */

Zusammenfassung und Fazit

Kommen wir nun zu einer kurzen Zusammenfassung:

Der richtige Hashing-Algorithmus

Zum Speichern von Passwörtern sollte ein Hashing-Algorithmus gewählt werden, der speziell für das Hashen von Passwörtern optimiert wurde. Bcrypt ist der Industriestandard und hat in PHP abgesehen von Argon2 keine Konkurrenz. Also entweder Bcrypt oder Argon2. Finger weg von MD5, SHA-256 und Co. zum Abspeichern von Passwörtern!

Salt und Pepper nicht vergessen

Jedes Passwort wird um Salt und Pepper ergänzt, was das Ganze um zusätzliche Sicherheit erweitert und z.B. den Einsatz von Regenbogentabellen sowie Wörterbuchangriffen erschwert. Dank der PHP Password Hashing API muss man sich um das Salt nicht kümmern.

Kostenfaktor an Server anpassen

Bei jedem guten Passwort-Hashing-Algorithmus gibt es Kostenfaktoren. Dies zu nutzen und den eigenen Bedürfnissen anzupassen, sodass ein gutes Mittelmaß zwischen Sicherheit und Performance vorliegt, ist hier angesagt!

PHP Password Hashing API nutzen

Die native Password Hashing API von PHP macht einiges einfacher und sicherer. Ergreift die Chance und nutzt keine eigenen oder gar fremden inoffiziellen Implementationen!

Das war's. Ich hoffe, dieser Artikel war für viele hilfreich.

Auch wenn doch einiges vorliegt, was es beim Hashen von Passwörtern zu beachten gilt, ist es mit PHP doch letztendlich vergleichsweise einfach, den Schutz der Passwörter seiner Nutzer sicherzustellen. Es gibt zwar nie eine hundertprozentige Sicherheit, aber die in diesem Artikel vorgestellten Maßnahmen sollten auf jeden Fall ihren Zweck halbwegs zuverlässig erfüllen, sodass man nicht jede Nacht im Bett liegen und sich um die Sicherheit der Passwörter seiner Nutzer den Kopf zerbrechen muss.

In diesem Sinne: Schlaft gut! ;D

Weiterführende Links / Quellen