Stand: 2013-02-09

Passwörter sicher speichern

Immer wieder gibt es Berichte von Angreifern, die Passwörter aus einer Datenbank entweden und missbrauchen. Im schlimmsten Fall stehen die Passwörter der Kunden im Klartext in der Datenbank. Erfolgreiche Angriffe auf Passwörter sind umso schlimmer, weil viele Nutzer aus Bequemlichkeit für diverse Dienste das immer gleiche Passwort verwenden. Als Entwickler eines Systems hat man deshalb die Verantwortung, die Passwörter so zu speichern, dass sie für Angreifer wertlos sind.

Inhalt

Speicherung als Hash

Dass die Speicherung von Passwörtern im Klartext keine gute Idee ist, hat sich mittlerweile weitgehend herumgesprochen. Oft werden deshalb nicht die Passwörter, sondern nur deren Hash-Werte gespeichert. Hash-Verfahren wie MD5 oder SHA1 berechnen aus einem Klartext eine kryptische Prüfsumme, aus der man nicht den Eingabewert rekonstruieren kann. Der folgende Python-Code berechnet aus dem Wort "geheim" den SHA1-Hash 906072001efddf3e11e6d2b5782f4777fe038739.

>>> from hashlib import sha1
>>> sha1("geheim").hexdigest()
'906072001efddf3e11e6d2b5782f4777fe038739'

Statt des Passwortes im Klartext wird nur dieser Hash-Wert in der Datenbank gespeichert. Um das Passwort bei der Anmeldung zu prüfen, wird erneut der Hash-Wert davon berechnet und mit dem gespeicherten verglichen. Wenn nun ein Angreifer Zugriff auf die Datenbank mit den Nutzerdaten erlangt, kann er mit den Passwort-Hash-Werten zunächst wenig anfangen.

Hash-Werte salzen

Wirklich sicher sind einfache Passwort-Hashes aber nicht, denn viele Nutzer wählen simple Passwörter. Für die gibt es aber Tabellen mit vorberechneten Hash-Werten, mit denen Wörterbuchangriffe ausgeführt werden können. Im Wörterbuch wird dabei zu einem Hash-Wert einfach das Klartextpasswort nachgeschlagen. Auch Rainbow Tables bieten eine Möglichkeit, die Suche nach dem Klartextpasswort zu beschleunigen. Um diese Art von Angriff zu verhindern, werden Hash-Werte mit ein paar Zeichen "gesalzen". Dazu werden ein paar zufällige Zeichen an das Passwort angehängt, bevor der Hash-Wert berechnet wird. Die zusätzlichen Zeichen (engl. "salt value") werden im Klartext neben dem Hash-Wert gespeichert. Wenn sich ein Nutzer anmeldet, wird an sein Passwort vor der Berechnung des Hash-Wertes wieder das "Salz" angehängt, der Hash-Wert berechnet und mit dem gesalzenen Hash in der Datenbank verglichen. Das "Salz" selbst, muss nicht vor Angreifern geschützt werden, denn es dient lediglich dazu, die vorberechneten Hash-Werte aus Rainbow Tables wertlos zu machen.

Ein Beispiel verdeutlicht das Verfahren. Eine Tabelle mit Nutzerdaten sieht etwa so aus:

| name | password_hash                            | salt     |
+------+------------------------------------------+----------+
| Karl | 4676d5dbdfc7cd6eea15bfece83d1a58ef633a26 | u75Pe#mq |

Wenn sich der Nutzer "Karl" anmeldet, wird aus der Datenbank sowohl der Passwort-Hash als auch der Salzwert geladen. Dieser wird an das übermittelte Passwort "geheim" angehängt und der Hash-Wert berechnet:

>>> salt = "u75Pe#mq"
>>> sha1("geheim" + salt).hexdigest()
'4676d5dbdfc7cd6eea15bfece83d1a58ef633a26'

Dieser Wert wird mit dem Passwort-Hash aus der Datenbank verglichen: stimmen beide überein, ist das Passwort korrekt.

Gewöhnliche Hash-Algorithmen sind zu schnell

Mit den verwendeten Hash-Algorithmen gibt es aber ein grundsätzliches Problem: sie sind zu schnell! Diese Algorithmen wurden nämlich dafür entworfen, kryptografische Prüfsummen großer Datenmengen möglichst schnell zu berechnen. Um einem Angreifer beim Berechnen der Hash-Werte auszubremsen, sollte der verwendete Algorithmus also langsam sein. Ein gängiges Verfahren ist, von einem Hash-Wert wieder einen Hash-Wert zu berechnen und das zum Beispiel tausend mal.

def hash(password):
    hash_value = sha1(password).hexdigest()
    for i in range(0, 1000):
        hash_value = sha1(hash_value).hexdigest()
    return hash_value

Lösung

Einfacher und eleganter geht es mit BCrypt, das extra zum Schutz von Kennwörtern erfunden wurde. Hier gezeigt am Beispiel der der Java-Variante jBCrypt:

// Passwort-Hash zur Speicherung in der Datenbank erzeugen.
// Das Ergebnis enthält neben dem eigentlichen Hash auch den Salt Value.
String hashed = BCrypt.hashpw(password, BCrypt.gensalt());

// Prüfen, ob ein Klartextpasswort einem zuvor gehashten Passwort entspricht.
if (BCrypt.checkpw(candidate, hashed))
    System.out.println("Passwort ist korrekt");
else
    System.out.println("Passwort ist falsch");

BCrypt-Implementierungen gibt es für alle gängigen Programmiersprachen (siehe Abschnitt "See also" auf der verlinkten Seite). Eine ebenfalls gute Alternative ist PBKDF2.

Noch besser ist scrypt, das neben reichlich Prozessorleistung auch noch reichlich Speicher verlangt. Damit wird es für Angreifer selbst mit spezieller Hardware schwer, einen Passwort-Hash zu knacken. scrypt kann als Spitze der Technik in Sachen Passwortschutz betrachtet werden.

scrypt-Implementierungen:

Zusammenfassung

Speichere Passwörter niemals im Klartext! Hänge an Passwörter ein paar zufällige Zeichen (salt value), bevor der Hash berechnet wird. Wenn du ein klassisches Hash-Verfahren nutzt, wende es rekursiv an. Verwende zum sicheren Speichern von Passwörtern am besten scrypt. BCrypt und PBKDF2 bieten zwar einen etwas schwächeren Schutz, aber auch auf einem sehr hohen Niveau.

Weitere Informationen

Wesentliche Änderungen