Grundlagen: Sicheres Passwort Hashing mit Salts

Passwörter in Plaintext speichernWer Software entwickelt und dies im Web-Umfeld tut, der hat sicherlich schon das ein oder andere Login-System geschrieben oder zumindest Berührungspunkte in diesem Bereich gehabt. Neben der Logik eines sicheren Login- bzw. User-Systems an und für sich, ist das sichere Speichern von Passwörtern einer der wichtigsten Punkte während der Implementierung.

Selbst wenn der eigentliche Code des Logins zu 100 Prozent fehlerfrei und sicher ist (wovon man in der Praxis nie ausgehen sollte), so kann es durch Sicherheitslücken in der Serversoftware immer noch zu Einbrüchen bzw. Hacks kommen. Es gibt immer eine Variable, auf die man keinen Einfluss hat und so werden tagtäglich Webseiten gehackt, kompromittiert und komplette Datenbanken mit Usernamen und Passwörtern ausgelesen.

Um die Nutzer im Falle eines solchen Hacks bestmöglich zu schützen, sollten die Passwörter durch Salts (=Salz; eine Methodik aus der Kryptologie) geschützt werden.

In der Praxis sieht es jedoch leider so aus, dass vielen Entwicklern zwar bewusst ist, dass sie Passwörter salten sollten, die Meinungen und Ansätze über das “Wie”, also die Umsetzung, jedoch stark voneinander abweichen und teilweise so falsch sind, dass sich der gut gemeinte “Schutz” eher ins Negative auswirkt.

Bestseller Nr. 1 Sichere Webanwendungen: Das Praxisbuch
Bestseller Nr. 2 Sichere Webanwendungen mit PHP
Bestseller Nr. 3 Sichere Webanwendungen

Deshalb möchte ich nachfolgend einmal mit dem Thema Passwort Hashing und Salting aufräumen und vom “Was ist eigentlich Passwort Hashing” bis hin zu “Best Practices für Passwort Hashing” die gesamte Themenpalette abarbeiten.

Was ist Passwort Hashing?

Passwort Hashing bedeutet das Anwenden einer Hashfunktion auf ein Passwort. Eine Hashfunktion (“hash'” ist englisch und steht für zerhacken) ist eine sogenannte Streuwertfunktion, die es ermöglichst große Eingabemengen auf (meist kleinere) Zielmengen zu projizieren. Oftmals haben die Zielmengen auch festgelegte Länge.

Für den Secure Hash Algorithm (kurz SHA) beträgt die Länge der Zielmenge z.B. immer 160 Bit, die wiederum oftmals als 40-stellige Hexadezimalzahl notiert wird.

Hat man also das Passwort “Geheim” und wendet darauf SHA1 an, so ergibt sich folgende Ausgabe:

sha1('Secret') == f4e7a8740db0b7a0bfd8e63077261475f61fc2a6

Lautet das Passwort hingegen “SuperLangesUndSehrSehrGeheimesPasswortMitVielenZahlen6489451456498489494894894” ergibt sich folgender Hash (unter Anwendung von SHA):

sha1('SuperLongAndVerySecretPasswordWithManyNumbers6489451456498489494894894') == 08e114b6377129fa37fed8f86af2e06bf62fad71

Trotz der viel längeren Eingabe hat die Zielmenge immer noch dieselbe Länge von 40 Zeichen. Hieraus ergibt sich ein Problem von Hashfunktionen. Da die Eingabemenge größer sein kann als die Zielmenge, kann es vorkommen, dass zwei unterschiedliche Eingaben den gleichen Hashwert erzeugen. Dies nennt man eine Kollisionen.

Wenn statt dem Passwort der Hash in der Datenbank gespeichert werden soll, so sind Kollisionen ein nicht erwünschter Seiteneffekt. Zudem ist nicht jede Hashfunktion zur Anwendung auf Passwörter geeignet. Aus diesem Grund gibt es eine Unterart der Hashfunktionen, die sogenannten kryptologischen Hashfunktionen.

MD5 ist unsicherDieser Typus von Hashfunktionen zeichnet sich durch eine Reihe von Merkmalen aus. So ist eine kryptologische Hashfunktion per Definition kollisionsresistent, wobei eine Kollisionen theoretisch immer noch möglich ist, jedoch mit einem unüberwindbar hohen Aufwand verbunden ist, sodass praktisch keine Kollision gefunden werden kann. Weiter sollen schon kleinste Änderungen an der Eingabemenge eine scheinbar willkürliche Änderung der Zielmenge bewirken. Zudem soll es nicht möglich sein, aus einem Hash einer kryptografischen Hashfunktion die Eingabe zu rekonstruieren.

Wenn für kryptologische Hashfunktionen dennoch Kollisionen gefunden werden, dann ist entweder eine Schwachstelle im Algorithmus entdeckt worden oder der Aufwand zum Finden einer Kollision ist im Verhältnis zum technischen Fortschritt nicht mehr groß genug um eine Kollisionsresistenz zu sichern. In diesem Falle spricht man davon, dass der jeweilige Algorithmus “broken” (=zerbrochen, kaputt) ist. Von der Nutzung solcher Algorithmen sollte abgesehen werden. (Dies gilt z.B. auch für die MD5-Funktion.)

Zum Passwort-Hashing sollten also ausschließlich kryptologische Hashfunktionen verwendet werden.

Der Standardablauf zur Verwaltung von Nutzerdaten und Passwörten unter Verwendung von Hashfunktionen sieht wie folgt aus:

  1. Der Nutzer registriert sich mit Nutzernamen und Passwort.
  2. Das Passwort wird vorm Speichern in der Datenbank gehasht und nur der Hash wird in der Datenbank abgelegt.
  3. Will sich der Nutzer nun einloggen, gibt er Nutzername und Passwort an. Sein Passwort wird wieder gehasht und mit dem Hash verglichen, der in der Datenbank für seinen Nutzernamen abgelegt ist.
  4. Stimmen die Hashes überein, wird der Nutzer eingeloggt, anderenfalls erhält er eine Fehlermeldung.

Die Fehlermeldung sollte hierbei immer nur sagen, dass die Zugangsdaten falsch sind. Wird angegeben, ob Passwort oder Nutzername falsch sind, kann der Angreifer daraus schon schließen, dass eines der beiden auf jeden Fall richtig ist. Deshalb sollten an dieser Stelle immer nur generische Fehlermeldungen ausgegeben werden.

Durch die obigen vier Schritte, sprich das Hashing der Passwörter, kann sichergestellt werden, dass beim Verlust der Datenbank bzw. einem Einbruch in die Datenbank nur die Hashes publik werden. Mittels der Hashes kann sich der Angreifer jedoch nicht einloggen, da diese in Schritt 3 ja erneut gehasht und somit nicht mehr mit dem Hash des eigentlichen Passworts übereinstimmen würden.

Warum sind Hashes nicht per se sicher?

Nachdem letzten Absatz könnte man meinen, (kryptografische) Hashes seien per se sicher. Man könnte fälschlicherweise annehmen, dass, unter Anwendung der vier Schritte aus vorherigem Absatz, die Passwörter ausreichend gesichert seien und man alles getan hätte, was zu ihrem Schutz nötig ist. Leider ist dem nicht so!

Was ist Brute Force?Zwar haben wir richtigerweise festgestellt, dass sich ein Hacker nicht (ohne Weiteres) mittels der Hashes einloggen kann, jedoch gibt es dennoch Möglichkeiten vom Hash wieder auf das Passwort zu schließen. Und da Nutzer (leider) meist dieselben Anmeldedaten für mehrere Portale/Webseiten nutzen, liegt die Wiederherstellung des Passworts im Interesse des Hackers. Deshalb betrachten wir nun, wie Hashes “entschlüsselt” (oder auch gecracked) werden können, um uns dann in einem späteren Absatz dann damit zu beschäftigen, wie wir eben dieses Risiko weiter eindämmen können.

Prinzipiell kann man zwischen zwei Methoden Hashes zu cracken unterscheiden:

  1. Brute Forcing; Beim Brute Forcing (brute force = rohe Gewalt) wird versucht, durch Ausprobieren aller möglichen Kombinationen, das gesuchte Ergebnis zu erhalten. Hat man also z.B. den Hash “e22a63fb76874c99488435f26b117e37” so bildet man systematisch für alle Kombinationen eines Alphabets (z.B. A-Za-z0-9) den entsprechenden Hashwert und vergleicht diesen mit dem vorhandenen Hash. Sind die Hashes gleich, schaut man, welches die letzte Eingabekombination war und kennt somit das Passwort. Da das Durchprobieren aller Möglichkeiten einen hohen Rechen- und Zeitaufwand bedeutet, kann versucht werden zuerst mittels sogenannten “Dictionaries” zu arbeiten. Ein Dictionary (= Wörterbuch) ist eine Liste mit gängigen Passwörtern. Zu dieser Liste werden dann die Hashes erzeugt und mit dem gesuchten Hash verglichen. Je größer die gehackte Datenbank ist, umso wahrscheinlicher ist es, dass ein Nutzer eines der gängigen Passwörter aus dem Dictionary genutzt hat.
  2. Lookup Tables; Um das Performance-Problem von Brute Force Attacken zu umgehen werden Lookup Tables genutzt. Eine Lookup Table ist eine spezielle Datenstruktur, die vorberechnete Passwort-Hash-Pärchen enthält. Das Besondere an Lookup Tables ist, dass sie selbst bei Datenbeständen von mehreren Millionen Pärchen immer noch mehrere hundert Abfragen pro Sekunde verarbeiten können. Lookup Tables sind also schneller als reines Brute Force, dafür sind sie durch die vorberechneten Werte ebenso wie Dictionary-Attacken auf eine vorbestimmte Eingabemenge (an Passwörtern) begrenzt. Eine Sonderform sind die sogenannten “Rainbow Tables”. Sie stellen eine Mischform aus Lookup Table und Brute Force Attacke dar. So ist ein Teil der Hashes vorberechnet, um zu bestimmen, ob sich der Hash eines Passwortes in einer Submenge befindet. Ist dies der Fall, wird die Submenge per Brute Force gehasht um das passende Passwort zu finden. Auf diese Weise können auf geringerem Platz mehr Hashes gespeichert und somit die Trefferrate erhöht werden.

Kommen wir nun dazu, warum wir Passwörter vor dem Hashen zusätzlichen Salten (= mit einem Salt versehen) sollten.

Nutzen von Salts beim Passwort Hashing

Starke Passwort SaltsMittels eines Salts lässt sich das im vorherigen Abschnitt beschriebene Lookup-Table-Verfahren außer Kraft setzen. Dies funktioniert, weil Lookup Tables davon ausgehen, dass das gleiche Passwort immer den gleichen Hash ergibt. Nutzen zwei User also das gleiche Passwort haben Sie normalerweise (wenn nur gehasht wird) den gleichen Hash.

Versieht man die Passwörter der einzelnen Nutzer nun aber mit einem zufälligen Salt, so ergeben die beiden Passwörter unterschiedliche Hashes, sodass eine Lookup Table nicht funktionieren kann, da vor dem Hack die Salts nicht bekannt sind und somit auch keine Lookup Table vorberechnet werden kann.

Brute Force Attacken sind hiervor jedoch immun. Bei ausreichend langen Passwörtern ist der Aufwand des Brute Forcing jedoch so hoch, dass das Entschlüsseln aller erbeuteten Passwörter nicht in annehmbarer Zeit erfolgen kann. Zudem kann durch Key Stretching das Brute Forcen weiter erschwert werden. (Mehr zu Key Stretching im nächsten Abschnitt.)

Best Practice: Passwort Hashing mit Salt

Bei der Verwendung von Salts sollten folgende Dinge beachtet werden, um eine maximale Effektivität zu erreichen.

  • Der verwendete Salt sollte einzigartig pro Nutzer und Passwort sein. Das heißt, dass nicht nur bei der Registrierung eines Nutzers, sondern auch bei dem Wechsel seines Passworts ein neuer Salt generiert werden sollte.
  • Der Salt sollte aus einer zufälligen Zeichenfolge bestehen. Zur Generierung des Salts sollte ein kryptografisch sicherer Zufallszahlengenerator (bzw. englisch cryptographically secure pseudo-random number generator; kurz: CSPRNG) verwendet werden. Dies ist wichtig, da nicht jeder Zufallsgenerator wirklich zufällig ist. So lassen sich bei vielen Standard-Generatoren Muster erkennen, anhand derer, bei großen Mengen an Zufallszahlen (in diesem Falle Salts), auf andere mit dem Generator erstellte Zahlen (Salts) geschlossen werden kann. In nachfolgender Liste (zum Aufklappen den Titel anklicken), finden sich gängige CSPRNG-Implementierungen für verschiedene Programmiersprachen.
  • Liste mit CSPRNG-Algorithmen & Bibliotheken
    .NET (C#, VB, F#): System.Security.Cryptography.RNGCryptoServiceProvider
    C/C++ (Windows API): CryptGenRandom, ISAAC
    GNU/Linux/Unix-basierte Sprachen: /dev/random oder /dev/urandom als Stream-Source nehmen
    Java: java.security.SecureRandom, Java TRNG Client
    JavaScript: RandomSource.getRandomValues(), CryptoJS
    Lua: LuaCrypto, ISAAC
    NodeJS: crypto.randomBytes()
    Perl: Math::Random::Secure
    PHP: mcryptcreateiv, opensslrandompseudo_bytes, php-csprng
    Python: os.urandom, pyaes / Advanced Encryption Suite
    Ruby: SecureRandom, drbg-rb
    Visual Basic: ISAAC
  • Weiter sollte der Salt mindestens so lang sein wie das Ergebnis der verwendeten Hash-Funktion. Wenn der Salt zu kurz ist, ergeben sich zu wenig Variationen, sodass der Angreifer trotz Verwendung eines Salts seine Lookup-Tables für alle Salt-Variationen in annehmbarer Zeit vorberechnen könnte.
  • Das Hashing an sich sollte durch Erhöhung des Rechenaufwands künstlich verlangsamt werden. Mittels des Salts kann sichergestellt werden, dass Lookup Tables nutzlos werden. Gegen reine Brute Force Attacken helfen Salts jedoch nicht. Um auch Brute Forcing möglichst uneffektiv zu machen, sollte ein Hashing Algorithmus genutzt werden, der Key Stretching unterstützt. Key Stretching Algorithmen sind besonders rechenintensiv, sodass die Hashrate um einen frei definierten Faktor verlangsamt werden kann. (In der Regel erfolgt dies über die Angabe der Iterationen beim Aufruf der Hashing-Funktion.) Schafft eine Cracking Software bei einem normalen Verfahren z.B. 300.000 Hashs/Sekunde kann durch entsprechendes Key Stretching die Rate z.B. auf 5 Hashs/Sekunde verringert werden. Auf diese Art und Weise wird das Brute Forcen ineffektiv. Gängige Librarys zum Key Stretching sind zum Beispiel crypt(5), PBKDF2 oder bcrypt. Der Parameter beim Key Stretching sollte so gewählt werden, dass das Hashing möglichst langsam ist, schwache Geräte bzw. Webserver darunter aber nicht leiden. Wird mit zu vielen Iterationen gearbeitet, kann bei hohem Nutzeraufkommen zum Beispiel die Performance des Webservers leiden, auf dem die Hashing-Operationen ausgeführt werden.

Fassen wir also noch einmal kurz und knapp zusammen:

  1. Der Salt sollte einzigartig pro Nutzer und Passwort sein
  2. Der Salt sollte einem kryptografisch sicheren Zufallsgenerator entspringen
  3. Der Salt sollte mindestens solang wie der Hash sein
  4. Es sollte ein Key Stretching Verfahren angewendet werden

Unter Beachtung der obigen Regeln kann nicht mehr viel schief gehen. Dennoch schauen wir im nächsten Abschnitt noch einmal, was man beim Hashen und Salten definitiv nicht tun sollte.

Wie man Passwörter nicht hasht und saltet

Achtung – nachfolgende Dinge sollten beim Hashen und Salten von Passwörtern ausdrücklich nicht gemacht werden!

  • Salts mehrfach verwenden; Wenn der Salt nur einmalig generiert wird und/oder im Quellcode hart codiert ist, dann verliert er effektiv an Nutzen. Durch einen hart codierten Salt würde das gleiche Passwort zweier Nutzer ebenfalls den gleichen Hash ergeben. Somit müsste die Lookup Table nur einmalig für den definierten Salt neu berechnet werden und der Schutz durch den Salt wäre ausgehebelt. Deswegen sollte ein Salt immer einmalig pro User und Passwort generiert werden.
  • Nutzernamen als Salt verwenden; Das Verwenden des Nutzernamens als Salt ist eine ebenso schlechte Idee. Oftmals nutzen User denselben Nutzernamen für verschiedene Dienste. Würden nun alle Dienste den Nutzernamen als Salt verwenden, könnte ein gecracktes Passwort für sämtliche betroffene Dienste verwendet werden. Deshalb sollte ein kryptografisch sicherer, zufälliger Salt generiert werden.
  • Einen zu kurzen Salt verwenden; Ist der Salt zu kurz gewählt, können Angreifer Lookup Tables für alle möglichen Salt-Variationen vorberechnen. Besteht der Salt zum Beispiel nur aus 2 ASCII zeichen, ergeben sich gerade einmal 95*95=9025 mögliche Salts. Selbst wenn man von einer großen Passwortliste von 20MB für eine Dictionary-Attacke ausgeht, ergibt dies (bei Berechnung der Liste mit allen Salts) gerade einmal eine Datenmenge von 176GB, deren Verarbeitung und Speicherung bei heutigen Maßstäben keine Problem mehr darstellt.
  • Ausschließlich clientseitig hashen; Zwar scheint es auf den ersten Blick Sinn zu machen, das Passwort clientseitig, also zum Beispiel im Browser mittels Javascript, zu hashen, um das Passwort nicht im “Klartext” zum Server übertragen zu müssen, jedoch birgt dies bei genauerer Betrachtung mehrere Risiken. Zum einen kann nicht immer gewährleistet werden, dass der Browser des Nutzers JavaScript unterstützt und zum anderen hieße dies, dass nur noch der berechnete Hash an den Server geschickt und dort gegen den Hash in der Datenbank verglichen würde. Dies hieße auch, dass ein Angreifer beim Erbeuten des Hashs den Hash nicht mehr entschlüsseln müsste, sondern diesen direkt zum einloggen verwenden könnte, was eine große Schwachstelle darstellt. Das clientseitige Hashen und Salten kann also höchstens eine Zusatzleistung zum serverseitigen Hashen darstellen. Zudem sollte bedacht werden, dass clientseitiges Hashen kein Ersatz für eine sichere Verbindung (z.B. HTTPS TLS/SSL) zwischen Client und Server ist. Wer nur clientseitig hasht, aber keine sichere Transportstrecke nutzt, läuft Gefahr, dass der Hash mittels Man-in-the-Middle-Attacke abgegriffen und daraufhin missbraucht wird.
  • Unsichere oder selbst geschriebene Hashing-Algorithmen verwenden; Auf einigen Internetseiten/-foren wird vorgeschlagen, zur “Verbesserung der Sicherheit” mehrere Hash-Funktionen zu kombinieren oder ein und dieselbe Funktion verschachtelt aufzurufen. Solche Vorschläge können zum Beispiel wie folgt aussehen: sha1(sha1(passwort)) oder md5(sha1(passwort)). Was hierbei oft vergessen wird, ist die Abschätzung von Nutzen und Kosten. Auf der Nutzen-Seite steht im Idealfall der minimal höhere Rechenaufwand. Dieser Ansatz ist jedoch mit einem iterativen Algorithmus (Stichwort: Key Stretching) wesentlich effektiver umzusetzen. Auf der Kosten-Seite steht das Risiko die Sicherheit zu schwächen oder Inkompatibilitätsprobleme hervorzurufen, sodass von solchen Methoden abgesehen werden sollte. Ebenfalls ist es keine gute Idee, seinen eigenen Hash-Algorithmus entwerfen zu wollen. Oftmals passieren hierbei Fehler, die wiederum die komplette Sicherheit infrage stellen. Etablierte Algorithmen hingegen sind zigfach getestet und wesentlich stabiler, sodass auf einen bereits etablierten Algorithmus zurückgegriffen werden sollte.

Weitere Sicherheitsmaßnahmen

Unter Anwendung der bisher in diesem Artikel gelernten Methoden – Hashing, Salting und Key Stretching – lässt sich bereits ein gut gesichertes System aufsetzen. Dennoch gibt Hacker sind überalles noch weitere Möglichkeiten, den Schutz der Passwörter zu erhöhen. Eine dieser Möglichkeiten ist das Verschlüsseln der Hashes.

Verschlüsselung von Hashes

Durch die Verschlüsselung von Hashes kann sichergestellt werden, dass Brute Force Attacken nutzlos werden. Bei einem Hack erhält der Hacker die Hashes in verschlüsselter Form. Erzeugt er nun Hashes, um diese mit den erbeuteten (vermeintlichen) Hashes zu vergleichen, wird kein Match finden, da er die von ihm erzeugten Hashes noch unter Anwendung des gleichen Schlüssels wie die Applikation verschlüsseln müsste.

Zur Verschlüsselung von Hashes eignen sich Lösungen wie AES oder HMAC. Damit dieses System funktioniert, sollte der Schlüssel (engl. secret) jedoch auf einem dedizierten System liegen bzw. die komplette Verschlüsselung auf eben diesem passieren. Liegt der Schlüssel/die Verschlüsselungslogik auf dem gleichen Server wie die Datenbank, ist es wahrscheinlich, dass der Angreifer auch hierauf Zugriff hat, sodass die Verschlüsselung ausgehebelt werden kann. Sollte es nicht anders gehen oder keine geeignete Hardware vorhanden sein, ist es jedoch immer noch besser die Verschlüsselungslogik auf dem gleichen Webserver wie die Datenbank zu implementieren als gar nicht zu verschlüsseln. Nichtsdestotrotz sollte abgewägt werden, ob die Sicherheitsansprüche es nicht gebieten, in solch einem Fall zusätzliche, dedizierte Hardware anzuschaffen.

Weiter sollte der Schlüssel für die Hash-Verschlüsselung während der Installation einer Instanz dynamisch erzeugt werden. Ein, in der Software fest hinterlegter, Schlüssel steigert das Risiko eines erfolgreichen Angriffs. Hat der Angreifer einmal Zugriff zum Code, könnte er bei einem fest hinterlegten Schlüssel alle Instanzen dieser Software attackieren. Hingegen er bei einem dynamisch generierten Schlüssel Zugriff auf jedes System haben müsste, dass er angreifen will.

Zeit-konstanter Hash-Vergleich

Bei der Implementierung des Hash-Vergleichs, also bei der Validierung des Passworts, sollte darauf geachtet werden, dass der Vergleich immer gleich lang dauert. Das meint, dass sowohl die Validierung eines gültigen Passwort Hashes als auch die Validierung eines ungültigen Hashes die gleiche Zeit beanspruchen sollten.

Die am häufigsten verwendete Methode zum Vergleich zweier Hashes dürfte wohl wie folgt (oder ähnlich) aussehen:

string hash1 = "e22a63fb76874c99488435f26b117e37";
string hash2 = "0a4ff18a7d23f8b3ded5eaf93104ac88";
if (hash1 == hash2) { 
  return true; 
}

Der “==”-Vergleichsoperator bewirkt jedoch, dass der Vergleich bei der ersten unterschiedlichen Stelle beendet und mit einem “false” quittiert wird. In obigem Beispiel wäre das direkt nach der ersten Stelle. Bei identischen Hashes hingegen würde der “==”-Operator alle Stellen der Hashes durchlaufen. Hieraus ergibt sich ein Zeitunterschied, den sich Angreifer zunutze machen können.

Attackiert der Angreifer einen Webservice und probiert alle Hashes für alle 256 Byte-Werte an der ersten Position des Hashes durch, kann er durch den Zeitunterschied feststellen, welches das richtige Byte an der ersten Position ist. Danach nimmt er sich die zweite Position vor und kann sich somit nach und nach an den richtigen Hash bzw. die richtige Eingabe herantasten.

Dieses Szenario mag auf den ersten Blick unwahrscheinlich klingen, ist jedoch technisch und praktisch umsetzbar, wie ein Paper der Universtität Stanford belegt.

Um diesen Angriffsvektor zu eliminieren, ist es nötig, eine Zeit-konstante Vergleichsroutine zu nutzen. Diese könnte wiederum wie folgt aussehen:

string hash1 = "e22a63fb76874c99488435f26b117e37";
string hash2 = "0a4ff18a7d23f8b3ded5eaf93104ac88";
bool valid = true;
if (hash1.Length == hash2.Length) 
{ 
  for (int i = 0; i < hash1.Length; i++) 
  { 
    if (hash1[i] != hash2[i]) 
      valid = false; 
  } 
} 
else 
{ 
  valid = false; 
} 
return valid;

In obigem Code wird in jedem Fall jede Stelle durchlaufen, unabhängig davon, ob die beiden Hashes gleich oder ungleich sind. Somit kann der Angreifer keine Rückschlüsse aus der benötigten Zeit auf die Richtigkeit seines Hashes erhalten.

Fazit

Auch wenn der Artikel nun etwas ausführlicher geworden ist, lässt sich festhalten, dass das Passwort Hashing und Salting kein “Hexenwerk” ist. Wenn man sich auf die wesentlichen Punkte begrenzt, ist es ohne großen Aufwand möglich, ein sicheres System zu implementieren. Und gerade weil es eben kein unüberschaubarer Aufwand ist, sollte man bei der Implementierung eines Login-/Passwort-basierten Systems auch immer die oben genannten Techniken wie Hashing, Salting und Key-Stretching einsetzen.

Über Feedback und Erfahrungen würde ich mich (wie immer) freuen. Wie implementiert Ihr euer Hash-System? Habt ihr Erfahrung mit Key-Stretching, Salting und den oben genannten Vorgehensweisen? Ist euer oder ein von euch betreutes System bereits einmal von einem Hack betroffen gewesen?

22 Kommentare

  1. Between me and my husband we’ve owned more MP3 players over the years than I can count, including Sansas, iRivers, iPods (classic & touch), the Ibiza Rhapsody, etc. But, the last few years I’ve settled down to one line of players. Why? Because I was happy to discover how well-designed and fun to use the underappreciated (and widely mocked) Zunes are.

  2. Aendrewsays:

    Hallo Raffi,

    ein wirklich sehr umfangreicher und fachlich interessanter Artikel.

    Was mich etwas verwundert ist die Dokumentation bei php.net:
    http://php.net/manual/de/faq.passwords.php#faq.passwords.bestpractice
    Hash erzeugen: $hash = password_hash(“geheim”, PASSWORD_DEFAULT);
    Passwort prüfen: password_verify(“geheim”, $hash); // TRUE=Passwort stimmt

    Einziger Kritikpunkt den ich aktuell sehe ich die Länge das Salts.
    Übersehe ich dabei etwas? Wäre wirklich zu leicht wenn es so einfach wäre…

    • Hallo Aendrew,

      danke für deinen Kommentar. Leider kann ich dir nicht ganz folgen. Was meinst du mit “Einziger Kritikpunkt den ich aktuell sehe ich die Länge das Salts. Übersehe ich dabei etwas? Wäre wirklich zu leicht wenn es so einfach wäre…”?

      • Aendrewsays:

        Hallo Raffi,

        ich beziehe mich dabei auf folgenden Teil deines Artikels:
        > Weiter sollte der Salt mindestens so lang sein wie das Ergebnis der verwendeten
        > Hash-Funktion. Wenn der Salt zu kurz ist, ergeben sich zu wenig Variationen,
        > sodass der Angreifer trotz Verwendung eines Salts seine Lookup-Tables für alle
        > Salt-Variationen in annehmbarer Zeit vorberechnen könnte.

        Wenn es immer so ist wie hier beschrieben, dann ist der Salt kürzer als das Ergebnis:
        http://php.net/manual/de/faq.passwords.php#faq.password.storing-salts
        Salt: 22 Zeichen – Hash: 31 Zeichen

        Aber die genaue Länge des Salts kann ich nicht ermitteln und daher kann ich auch nicht sicher sagen, ob dieser länger ist.

        • Ok, sagen wir es so. Dass der Salt länger als das Passwort sein soll, ist eher eine Empfehlung. Denn wäre der Salt beliebig kurz z.B. nur 2 Stellen. Dann gäbe es nur vier mögliche Salts. Das hieße man müsste “nur” 4 Rainbow-Tables vorberechnen. Ist der Salt entsprechend lang wird der Aufwand ungleich größer. “So lang wie das Passwort” ging hierbei davon aus, dass das Passwort schon entsprechend lang vorgegenen ist.

          • Aendrewsays:

            Zuerst vielen Dank für die schnellen Antworten.

            Bei der Länge des Salts muss man also nicht päpstlicher als der Papst sein.
            Dann fällt mir nichts mehr ein, was gegen die Standardfunktionen von PHP sprechen würde. Wäre aber auch nicht weiter schlimm :)

            Eine Frage hätte ich noch zu folgenden Teil aus deiner letzten Antwort:
            > Denn wäre der Salt beliebig kurz z.B. nur 2 Stellen.
            > Dann gäbe es nur vier mögliche Salts.

            Der Salt ist doch eine beliebige Zeichenfolge, die idealerweise durch einen sicheren Zufallszahlengenerator erstelt wird.
            Somit könnte doch an jeder Stelle mehr als nur ‘0’ und ‘1’ stehen was bedeuten würde, das es mehr als vier mögliche Salts geben würde oder was übersehe ich hier?

            • Das ist richtig. Da war ich wohl wirklich etwas zu schnell. Es gibt natürlich mehr als 4 Kombinationen. Worauf ich hinaus wollte, sollte dennoch klar sein. Ist der Salt zu kurz, ist der Mehraufwand durch den Salt nicht wirklich gegeben.

              • Aendrewsays:

                Worauf du hinaus wolltest ist klar, ich wollte nur auf sicherstellen, dass ich das mit den Salts nicht falsch verstanden habe.

                Vielen Dank für die Aufklärung.

  3. Ich erkundige mich derzeit, für mein Projekt, im punkto Sicherheit. Dank diesem qualitativen und umfassendem Artikels, weiß ich jetzt, warum man sich nicht nur auf HTTPS und sha1 verlassen sollte und kann nun einige Problemstellen ausmerzen.

    Diese Homepage wird tatsächlich aus Kaffee und Liebe geschmiedet!

  4. Ich bin gerade ein bisschen am recherchieren für die Passwort-Verschlüsselung meines eigenen kleinen Web-Projektes und bin dabei auch auf Deine dankenswerter Weise recht ausführliche Website gestoßen.

    Eine Frage bleibt aber dennoch: Immer wieder liest man, dass der Salt mit einem kryptografisch sicheren Zufallszahlengenerator erzeugt werden sollte. Nur liest man nirgends, warum?
    Den Salt muss man ja, da er pro Benutzer eindeutig ist, zusammen mit dem Passwort in der Datenbank hinterlegen, so dass also jemand, der Zugriff auf die Passwörter in der DB hat, den Salt einfach mit auslesen kann. Wozu dann noch diese hochzufällig erzeugten Zahlen? Warum die Angst, dass jemand “Muster erkennen” könnte – die Hashes sind doch kein “Geheimnis” sondern stehen im Klartext in der DB?
    Kannst Du mir da weiterhelfen?

    • Hallo Uwe,

      das ist eine gute Frage, dessen Beantwortung mir jedoch nicht ganz leicht fällt. Ich gebe mein Bestes. Sollte etwas unklar sein, frag’ noch mal nach!

      Spontan fallen mir zwei Fehlerquellen/Angriffsszenarien ein, die bei der Verwendung eines nicht kryptographisch sicheren Zufallszahlengenrators auftreten können.

      1) Selbst wenn ein Angreifer Zugriff auf PW-Hash und Salt hat, muss er diese noch Brute-Forcen um an das Klartext-Passwort zu kommen. Kennt er jedoch eine beliebig große Anzahl an Salts (=Ergebnissen des Zufallszahlengenerators), könnte er daraus ggf. auf zukünftige, vom Generator erzeugte Werte (=Salts) schließen und schon im Vorhinein für diese Salts Rainbow-Tables errechnen, um zukünftig abgelegte Passwörter schneller zu knacken.

      2) Je nach Implementierung, kann es zum Beispiel bei Verwendung eines normalen RNG im Zusammenhang mit Multi-Threading dazu kommen, dass der RNG zweimal hintereinander bzw. in zwei Threads die gleiche Zufallszahl ausspuckt. Hätten beide Threads/Nutzer nun auch noch das gleiche Passwort gewählt, so hätten wir den Fall, dass zwei identische Passwörter den gleichen Salt haben. Die wiederspricht dem Kozenpt von Salts und untergräbt den dahinterliegenden Sicherheitsansatz.

      Zugegeben – beide Szenarien klingen mehr oder weniger unwahrscheinlich. Jedoch sollte man sich nicht in Sicherheit wägen, nur, weil etwas unwahrscheinlich scheint. Vorallem nicht, wenn es doch technisch möglich ist, einen CSPRNG anstelle eines normalen RNG zu nutzen. Im Auto schnallst du dich ja auch an, obwohl du nicht davon ausgehst, dass gerade du heute einen Unfall haben könntest.

      • Hallo Raffi,

        danke für deine schnelle Antwort. Ich muss doch nochmal nachfragen.

        Ich finde gerade Punkt 1 nicht logisch, evtl. habe ich das auch falsch verstanden: der Angreifer muss, wenn er Salt und PW-Hash vorliegen hat, Brute-Forcen (also er erzeugt keine Rainbow-Tables dafür sondern versucht das durch “ausprobieren” zu knacken?) – wenn er aber evtl. einen Salt _im Voraus erraten_ könnte (also noch nicht mal sicher weiß, ob der auch je zum Einsatz kommt), dann würde er den Aufwand treiben, dafür auf gut Glück Rainbow-Tables zu erzeugen?

        Dein zweiter Punkt (parallele Schlüsselerzeugung) ist mir klar geworden, bringt mich aber zu der Überlegung, dass diese Vorhersagbarkeit eines nicht-kryptografischen Zufallswertes nur innerhalb kurzer Zeiträume gegeben sein müsste, oder liege ich da falsch? Dass sich z.B. in einem durchschnittlichen Forum oder Online-Shop (so was ähnliches ist mein Projekt) mehrere User in derselben Sekunde oder gar Millisekunde anmelden halte ich für weniger wahrscheinlich als dass im Schnitt jede Woche mal einer oder zwei kommen.
        Wenn sich heute ein User anmeldet und nach mehreren Stunden oder Tagen der nächste, dann müsste doch auf dem Server zwischenzeitlich genug passiert sein, um die Vorhersagbarkeit zu torpedieren?

        Ich weiß, dass mich jetzt jeder Krypto-Experte steinigen wird, der das Folgende liest, aber nach allem was ich bisher gelesen habe müsste eine Kombination aus zwei oder mehreren bekannten, User-abhängigen Variablen doch ähnlich sicher sein wie ein kryptografisch erzeigter Salt. Ich denke da z.B. an den Timestamp der Account-Registrierung (User-individuell und nicht vorhersagbar) kombiniert mit der Mailadresse oder dem Account-Namen (z.B. mind. 3 Zeichen) des Users (Unterscheidungskriterium bei zufällig paralleler Anmeldung zweier User). Diese Kombination kann man im Voraus nicht erraten (kein Muster) und es ist sicher, dass diese Kombination nur dieser eine User hat – mindestens in meiner DB, sehr wahrscheinlich sogar weltweit über alle Datenbanken hinweg.
        Zusammen mit einem ensprechend lang laufenden Hash-Verfahren dürfte da jedem die Lust vergehen, auf gut Glück Rainbow-Tables erstellen zu wollen.

        Wo liegt mein Denkfehler?

        Danke&Gruß, Uwe

        • Ich weiß nicht, wie fit du in Englisch bist, aber lies dir mal diesen Artikel durch: http://franklinta.com/2014/08/31/predicting-the-next-math-random-in-java/ Er zeigt sehr schön, wie ein normaler Random-Generator funktioniert und wo dessen Schwachstellen sind bzw. wie er sich angreifen lässt.

          Das Problem, sowohl bei einem normalen RNG als auch bei deiner Idee mit Timestamp ist doch folgendes. In beiden Fällen lässt sich ggf. auf den Salt schließen. Als Szenario stell dir vor, dass ein Angreifer nur zeitlich begrenzten Zugriff hat.

          Beispiel) Dein System ist dank eines System-Bugs angreifbar. Der Angreifer schafft es einen Dump von deiner Datenbank und vielleicht sogar deines Sourcecodes zu ziehen. Die Sicherheitslücke, die er nutzte, wird mit dem nächsten Update geschlossen. Dennoch kann er mit dem Wissen über die bereits generierten Salts und den verwendeten Random-Generator auf zukünftige Salts schließen. Zudem stellt er beim Sichten deines Sourcodes fest, dass du schlecht programmiert hast, und sich die Passwort-Hashs per Funktionsaufruf ausgeben lassen. Obwohl das System nun vermeintlich zu ist, kann der Angreifer weiter angreifen.

          Ähnliches gilt auch für deinen Vorschlag mit timestamp+E-Mail. Die E-Mail ist oftmals im Frontend sichtbar und der Timestamp lässt sich durch Annäherung erraten. (In vielen Foren und Community steht zum Beispiel der Tag, wenn nicht sogar auch noch die Uhrzeit, wann sich ein Mitglied registriert hat.)

          Das Ziel eines Salts ist es doch möglichst einzigartig in Bezug auf das Passwort zu sein. Und einzigartig meint hier nicht systemweit, sondern weltweit. Würdest du den Nutzernamen nehmen, wäre diese Einzigartigkeit gefährdet, da es z.B. auf fast jedem Unix-System einen root-User gibt. Also streben wir nach etwas einzigartigem. Und da ist ein CSPRNG nunmal noch besser geeignet als dein Timestamp+XYZ. Die Chance auf eine Kollision sollte bei einem CSPRNG geringer sein.

          Ja, in der Praxis könnte dein Timestamp+XYZ klappen und ausreichend sein, mit einem CSPRNG wärst du aber “sicherer”. Nun ist die Frage, was dagegenspricht eine bewährte Technik wie CSPRNG nicht einzusetzen, sondern eine Eigenlösung nutzen zu wollen? Mir fällt spontan kein guter Grund ein. ;-)

    • Juliansays:

      Der Salt verhindert, dass zwei gleiche Passwörter mit dem selben Hashwert in einer DB auftauchen – indem der Salt auch noch kryptographisch sicher erzeugt wurde, erhöht man wesentlich den Zufallsfaktor des (hoffentlich) schon ordentlichn gewählten Passwortes. Es geht dabei nicht um die Hashwerte selber, sondern um die Urbildmenge der Hashfunktion, die der Angreifer möglichst nicht bilden können soll. Wenn zwei Hashwerte in einer Db stehen und der Angreifer weiß, dass keine Salts existieren, sind auch die Passwörter (mit allerhöchster Wahrscheinlichkeit) identisch. Wenn man einen nicht-kryptographisch sicheren Salt hinzufügt, sind zwar die Hashwerte identisch, aber der Angreifer wird die Berechnung der unsicheren Salts relativ leicht nachvollziehen können. Wenn die Salts kryptographisch sicher sind, haben wir unterschiedliche Hashwerte für unterschiedliche Passwörter (wie vorher) und ebenso für gleiche Passwörter (wesentlich wichtiger, da so keine Mustererkennung über RAinbow-Tables etc mehr möglich).

      • Hallo Julian,

        Danke für Deine Antwort.

        Was ich daran nicht verstehe ist, wie ich Raffi schon geschrieben habe, dass die Hashes doch gar nicht erraten werden müssen. Die stehen in der Datenbank direkt neben dem Hash.

        Gehen wir vielleicht von unterschiedlichen Szenarien aus?
        Ich denke mir, die Angreifer bekommen irgendwann Zugriff auf meine Datenbank und denken sich: “Hey, da sind so viele User, lass uns doch mal deren Passwort knacken und diese Passwörter auch woanders ausprobieren”.
        Eventuell haben die sogar meinen Quellcode abgezogen, wissen also, ob der Salt vorne oder hinten steht oder ob ich das PW vielleicht irgendwo mittenrein gesetzt habe.

        So. Jetzt haben die also einen Teil (den Salt) schon auf dem Tisch und müssen jetzt entweder über Rainbow Tables oder gleich über Brute Force den Rest herausfinden. Und das muss ich ihnen so schwer wie möglich machen, indem der Hash-Algorithmus lange rechnet. Also ist jeder Salt nützlich, der mit an Sicherheit grenzender Wahrscheinlichkeit bisher in keiner Rainbow-Table verwendet wird.

        Ist das das Szenario worüber wir sprechen? Oder hab’ ich da was übersehen? Wie gesagt bin ich kein Experte sondern eher zufällig in die Thematik reingeschlittert. ;-)

        • Juliansays:

          Niemand will den Hash erraten – sei es vom Passwort oder vom Salt ;-) sondern das Passwort selbst bzw. den Ursprungswert des Salts. Stell dir Hashing als mathematische Funktion wie f(x) = x*2 vor. x ist das Passwort, f(x) ist der Hash. Der Hash kann ja ruhig gesehen / gelesen werden. Angenommen, ich hashe “Julian” zu “jklfajsd5”, dann steht “jklfajsd5” in der Spalte Passwort in der DB. Nehmen wir an, der Wert wurde mit MD5 oder SHA1 gebildet und ist unbekannt, dann kann ein Angreifer mithilfe von BruteForce (mit oder ohne RainbowTables) versuchen, den Ursprungswert von “jklfajsd5”, also “Julian”, zu erraten. Das macht er, indem er alle Realwert-Kombinationen hasht und die Hashes mit “jklfajsd5” vergleicht. Kommt ein Salt zum Ursprungswert dazu, also meinetwegen “Julian” + “42kjlfsd”, wobei “42kjlfsd” kryptographisch zuäfllig erzeugt wurde, lautet der Hash für das PW meinetwegen “097g90ds”. Nimmt nun jemand anderes wiederrum den Wert “Julian”, kommt ein neuer Salt dazu, also meinetwegen “0q423hjf”, sodass aus “Julian” + “0q423hjf” ein vollkommen anderer Hash wird, also im vorherigen Fall. Der Salt steht zwar mit in der DB, aber nur, um bei Eingabe des Passworts auf ihn zugreifen zu können, das Passwort mit Salt zu hashen und dann den Hash der beiden mit dem in der DB gespeicherten Wert zu vergleichen.

          Der Angreifer muss also den Ursprungswert des Salts erraten und den Ursprungswert des Passworts + Salt erraten. Beides ist nur per BruteForce möglich.

          Es ändert übrigens wenig, an welcher Stelle der Salt eingefügt wird. Vorne oder hinten ist lediglich eine Multiplikation aller Möglichkeiten mal 2. Bei einer tendenziell exponentiellen Laufzeit der BruteForce-Angriffe fällt das so gut wie gar nicht ins Gewicht.

          • Christiansays:

            Fu schreibst: “Der Angreifer muss also den Ursprungswert des Salts erraten und den Ursprungswert des Passworts + Salt erraten. Beides ist nur per BruteForce möglich.”

            Es ist doch aber so, dass der Salt in Klartext gespeichert wird?
            Also den Ursprungswert des Salts kennt der Angreifer doch.

            Ich habe in anderen Foren nachgelesen und dort wird der Sinn des Salts, wie folgt beschrieben:
            – zu kurze Passwörter verlängern
            – und so vermeiden, dass Rainbow Tables angewendet werden können

            Gruß

  5. Sunnysays:

    Danke für diesen kompetenten und sehr gut strukturierten Artikel.
    Hatte zuvor bereits 2 Stunden diverse Diskussionen und Meinungen gelesenen, die mir zu subjektiv anstatt sachorientiert waren.

    Dein Artikel hebt sich davon deutlichste ab.

  6. Supahupesays:

    Sehr guter Artikel! Habe vor ca. einem Jahr noch eine Vorlesung über IT-Sicherheit gehört und war überrascht, wieviel der Inhalte ich mich noch erinnern konnte. Beschäftige mich oft (recherchemäßig) mit dem Thema, habe codierungstechnisch aber vergleichsweise wenig Erfahrungen zu Security-Themen gesammelt. (Außer mal mit der Java-Webserver-Implementierung; rudimentäres Session-Login in Spaghetti-Code-Form :-) )

    Zum Abschluss des Artikels fand ich das Beispiel sehr schön, auch mit dem Verweis auf das Paper. Habe ähnliches mal in C gesehen, da ging es um Side-Channel-Attacken auf (Mikro-)Controller-Ebene. Letztlich geht es da ja um ein ähnliches Prinzip.

    Eventuell passend zum Thema: Vielleicht könnte man ja mal einen Artikel über Web-Of-Trust bzw. Transitives Vertrauen schreiben und auf aktuelle Technologien / Anbieter hinweisen bzw. Vor- und Nachteile gegenüber der PKI-Infrastruktur aufzeigen. =)

  7. Thomas Drebersays:

    Danke für den Beitrag. Es fehlt im Internet gute Blogs, wo es alles klar geschrieben wird.

  8. Danke für die ausführliche Erklärung!

    Angesichts der zur Verfügung stehenden Methoden, doch umso erstaunlicher, das immer wieder Listen mit Klartext Passörtern im Internet auftauchen.

Hinterlasse einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Sie dient nur dem Spamschutz.