Passwörter sicher hashen

Nahezu alle Dienste im Web, auf dem Telefon oder sonstwo sind heutzutage mit einem vom Nutzer selbst erdachten Passwort gesichert. Und nahezu täglich gibt es Schreckensmeldungen, dass Angreifer irgendwo in ein System eingebrochen sind und die dort gespeicherten Passwörter gestohlen haben.
Dabei fragt man sich unweigerlich mehrere Dinge: Wieso wird ein Passwort so gespeichert, dass es zurückgerechnet werden kann? Wie kann man ein Passwort sicher speichern? Und warum ist die Verschlüsselung niemals perfekt, selbst wenn man sich bemüht?
In diesem Artikel möchte ich nicht darauf eingehen, dass Nutzer kein sicheres Passwort eingeben. Allzu oft lautet ein Passwort etwa „123456“ oder schlicht „passwort“. Da muss man schon sagen: Selbst Schuld. Viele Anbieter von Webdiensten hashen (hoffentlich!) die Passwörter ihrer Nutzer in der Datenbank. Ein Hash ist per Definition zunächst nicht zurückrechenbar. Ganz im Gegensatz zu einer Verschlüsselung ist es hier gar nicht erwünscht, eine Umkehrrechnung anstellen zu können.
Der bekannteste Hash-Algorithmus dürfte md5 sein. Er erzeugt aus einem beliebig langen Text immer einen 32 Zeichen (128 bit) langen Hashwert. Das ergibt theoretisch 340.282.366.920.938.463.463.374.607.431.768.211.456 unterschiedliche Hash-Werte, eigentlich ganz schön viel. Aber: das Wort „passwort“ ergibt eben immer den selben Hash, nämlich „E22A63FB76874C99488435F26B117E37“. Findet ein Angreifer also diesen Hash in der Datenbank, hat er auch das zugehörige Passwort. Es gibt bereits jede Menge Seiten im Internet, wo bekannte Hashes aufgeschlüsselt sind. Bei den meisten dieser so genannten Rainbow-Tables kann man natürlich auch nachschauen, wie der Hash für das eigene Passwort ist, und damit die Datenbank gleich für den Angreifer mit der passenden Antwort füllen.
Gehen wir nun davon aus, dass der Nutzer ein Passwort bestehend aus sechs Kleinbuchstaben gewählt hat. Dann gibt es noch 308.915.776 mögliche md5-Hashes. Mein Computer, ein betagtes Büromodell, braucht im Benchmark genau 592.60063290596 Sekunden, um all diese Kombinationen zu errechnen. In weniger als 10 Minuten also ist das Passwort geknackt.
Also, wie speichern wir das Passwort sicher ab? Absolute Sicherheit gibt es nicht, aber wir können die Zeit, die es zum errechnen eines möglichen Passworts braucht, drastisch verlängern, ohne dass der reguläre Nutzer davon etwas mitbekommt. Dazu brauchen wir mehrere Dinge: Einen besseren Hash-Algorithmus, der längere Hashes errechnet. Einen Salt, welches das vom Nutzer eingegebene Passwort verändert, und viele Iterationen des Hashes.

Schritt eins, der Algorithmus

Anstelle von md5 verwenden wir sha512. Statt 128bit-langen Hashes erzeugt dieser 512bit lange Hashes. Belassen wir es einfach bei „dies sind sehr viele unterschiedliche Hash-Werte“, denn die Angabe dieser Zahl würde definitiv nicht in eine Zeile passen.

Schritt zwei, der Salt

Wir „salzen“ das Passwort, bevor wir den Hash errechnen. Ein Salt ist einfach ein von uns definierter Wert, den wir an das Passwort anhängen, etwa „wort“. Aus „pass“ wird somit vor dem Hashen „passwort“, mit dem bereits gesehen md5-Hash „E22A63FB76874C99488435F26B117E37„. Gibt nun der Angreifer das für diesen Hash bekannte Wort „passwort“ ein, machen wir vor dem Berchnen des Hashwerts „passwortwort“ draus. Der Hash passt nicht, der Angreifer hat das Passwort nicht gefunden. Wir gehen noch einen Schritt weiter, und nutzen nicht nur ein Salt, sondern viele. Um genau zu sein bekommt jeder Nutzer ein individuelles Salt. Das generieren wir entweder beim Registrieren und schreiben es in die Datenbank, oder wir nutzen einen unveränderlichen Wert, der sowieso zum Nutzer gehört.

Schritt drei, Iterationen

Wir berechnen den Hash nicht nur einmal, sondern oft. Sehr oft. Das Ergebnis ist zunächst das selbe wie bei Schritt zwei, der Passwort-Hash in der Datenbank passt nicht zum Hash in einer Rainbow-Table. Wozu das ganze? Hat der Angreifer nicht nur Zugang auf die Hashes, sondern auch auf das Salt kann er in der selben Geschwindigkeit Hashes errechnen als wenn er nur das normale Passwort testet. Ob ich nun „md5(‚passwort‘)“ oder „md5(‚passwortwort‘)“ berechne ist dem Computer egal. Muss ich das aber 10.000 mal machen, um ein Passwort durchzutesten, bremse ich schon massiv aus. Statt der 592 Sekunden würde der md5 über alle 6stelligen Passwörter auf meinem Rechner bereits 68,5 Tage dauern.

Bringen wir alle Schritte zusammen

for ($i = 0; $i < 10240; $i++) {
    $hash = hash('sha512', $hash.$username.SALT);
}

Das Hashen dauert auf meinem Rechner 0.078 Sekunden. Beim Login-Vorgang merkt der normale Anwender das nicht. Der Impakt aber für einen potentiellen Angreifer ist gewaltig. Sofern mit der Algorithmus und das Salt bekannt sind brauche ich bei dieser Variante immer noch weit über 130 Tage, um alle 6-kleinbuchstabigen Kombinationen durchzuprobieren. Und das für jeden einzelnen Nutzer, da jeder Nutzer ein individuelles Salt hat! Bei einer Datenbank mit 100.000 Nutzerdaten brauche ich nun mehr als 35.600 Jahre, um die Passwörter aller Nutzer zu ermitteln! Habe ich Salt und oder den Algorithmus nicht ist es für den Angreifer unmöglich, die Passworte zu ermitteln [Möglich wäre natürlich weiterhin zB ein Brute-Force-Angriff auf das Frontend, das ist eine andere Baustelle].
Es empfiehlt sich übrigens beim Speichern eines Hashes in der DB eine Versionsnummer anzugeben, falls sich die Berechnung des Hashes einmal ändert. So kann man einen problemlosen Übergang „alter“ Hashes zu „neuen“ Hashes sicherstellen. Ich speichere dafür Hashes einfach so:

$1$E22A63FB76874C99488435F26B117E37

Zusammenfassung: Nimm einen aktuellen, stabilen Hash-Algorithmus, nutze einen (oder mehrere) Salts und nutze viele Iterationen. Kommt ein Angreifer an deine Datenbank sind zumindest die Passwort-Daten deiner Nutzer halbwegs sicher. Bei einem „normalen“ md5() brauche ich 592 Sekunden, um alle Passwörter einer Datenbank mit 100.000 Datensätzen zu knacken. Mit dem verbesserten Algorithmus brauche ich 35.600 Jahre!

1 comment for “Passwörter sicher hashen

  1. Clemens
    29. Oktober 2014 at 20:48

    Anstatt sich um das salzen selbst zu kümmern bietet sich an einfach HMAC zu verwenden. Entsprechende Funktionen gibt es in PHP ja. Da HMAC selbst aber noch kein iteriertes Verfahren ist, bleibt das Iterieren selbst zu tun.

Comments are closed.