adesso Blog

Passwörter dürfen nicht im Klartext in der Datenbank gespeichert werden. Das weiß jeder Developer. Die Gefahr, dass der Inhalt der Datenbank und damit die Passwörter, die eventuell auch anderswo verwendet werden, abgegriffen werden, ist einfach zu groß. Trotzdem werden bei der konkreten Umsetzung häufig Fehler gemacht, so dass immer wieder Passwort-Leaks an die Öffentlichkeit gelangen, die auf eine unsachgemäße Speicherung der jeweiligen Passwörter zurückzuführen sind. Um nicht selbst zu einem Eintrag in der Reihe der Passwort Leaks zu werden, soll dieser Blog-Beitrag bei der Auswahl eines sicheren Hashverfahrens unterstützen und Hinweise für die richtige Parametrisierung geben. Die Beispiele werden in Java und Spring-Security dargestellt.

Hashfunktionen

Um Passwörter sicher in der Datenbank speichern zu können, benötigen wir eine Lösung, die einen Wert erzeugt, der zwar immer noch die Korrektheit des Passworts verifiziert, aus dem aber das Passwort im Klartext nicht wiederhergestellt werden kann. Sichere Hashfunktionen bieten genau diese Eigenschaften:

  • Einwegfunktion: Aus dem Ergebnis der Funktion kann nicht mehr auf die Eingabe geschlossen werden.
  • Kollisionsresistenz: Wenn zwei Hashes gleich sind, muss auch der Eingabewert gleich sein. Für unterschiedliche Eingaben berechnet eine sichere Hashfunktion also unterschiedliche Ergebnisse.
  • Effizienz: Die Überprüfung, ob das Passwort den gleichen Hash ergibt wie das gespeicherte, soll schnell erfolgen. Diese Eigenschaft wird uns aber im Folgenden Probleme bereiten.

Die Angabe, welche Hashfunktionen als sicher gelten, muss ständig überprüft werden, da potentielle Angreifer und Sicherheitsforscher regelmäßig Angriffe auf bekannte Verfahren untersuchen. Angriff bedeutet in diesem Zusammenhang vor allem, dass Kollisionen effizient berechnet werden können oder dass aus der Ausgabe die zugehörige Eingabe berechnet werden kann. Beispielsweise gelten MD5 und SHA-1 nicht mehr als sicher, während SHA-256 und SHA-512 im Jahr 2020 sicher sein werden.

Allerdings ist die Verwendung dieser als sicher geltenden Hashfunktionen allein noch keine sichere Technik, um Passwörter zu persistieren. Es gibt zwei relevante Angriffe auf diese Hashes, die uns Probleme bereiten. Der erste Angriff ist die Wörterbuch-Attacke, auch Rainbow-Table-Attacke genannt. Dabei werden für die anzugreifende Hashfunktion alle denkbaren Eingaben vorberechnet und in einem Wörterbuch oder effizienter in der Rainbow-Table gespeichert. Werden nun die Passwort-Hashes geleakt, kann der Angreifer in diesem Wörterbuch nachschauen, welches Passwort jeweils verwendet wurde. Für die bekannten Verfahren gibt es bereits Wörterbücher im Netz, so dass wir nicht einmal ein Wörterbuch erstellen müssen. Der zweite Angriff ist ein einfacher Brute-Force-Angriff, bei dem alle Eingaben durchprobiert werden, ohne vorher eine Datenbank anzulegen. Ermöglicht wird dies durch die bereits erwähnte Effizienz der klassischen Hashfunktionen und die geringe Entropie, d.h. Verteilung, der vom Benutzer generierten Passwörter. Da beispielsweise GPUs sehr viele Berechnungen parallel durchführen können, können mit einer Hashcat-Implementierung bis zu 23 Milliarden SHA-256 Hashes pro Sekunde (!) durchprobiert werden. Man kann sich also vorstellen, dass bei einem Brute-Forcing der meistgenutzten Passwörter in den meisten Fällen bereits nach wenigen Sekunden das passende Klartext-Passwort vorliegt.

Sicheres Passworthashing

Zwei Maßnahmen sollen diese Angriffe nun deutlich erschweren. Um das Erstellen von Wörterbüchern für Angreifer unpraktikabel zu machen, wird das Passwort mit einem öffentlichen Zufallswert, dem Salt, kombiniert. Üblich ist zum Beispiel das Anhängen dieses Wertes an den Passwort-String. Um einen Wörterbuchangriff durchzuführen, müsste nun für jedes mögliche Salt ein Wörterbuch mit allen möglichen Passwörtern erstellt werden. Bei Verwendung eines Salts mit einer Länge von n Bit würde dies 2 hoch n mal mehr Wörterbücher erfordern als ohne Verwendung eines Salts. Dies macht den Angriff unpraktikabel. Das Brute-Force-Problem wird dadurch gelöst, dass Verfahren verwendet werden, die künstlich verlangsamt werden. Dadurch dauert das Ausprobieren wesentlich länger und ist somit nicht mehr effizient durchführbar. Als Anhaltspunkt, wie lange der Prozess dauern soll und darf, kann gelten, dass ein Login auf einem Backend-Server 0,5 - 1 Sekunde dauern darf.

In den folgenden Abschnitten stelle ich nun drei verschiedene Verfahren vor, die diese Gegenmaßnahmen mitbringen und veranschauliche sie anhand eines Java-Code-Schnipsels mit der auf Bouncy-Castle basierenden Spring-Security.

PBKDF2

Die Passwort-Based Key Derivation Function 2 wurde im Jahr 2000 von den RSA Laboratories als Teil der PKCS#5 Spezifikation veröffentlicht. Diese Funktion ermöglicht es, aus benutzergenerierten Passwörtern beispielsweise Schlüssel zu erzeugen, die für symmetrische Verschlüsselung verwendet werden können.

Sie bietet aber auch die Eigenschaften, die wir für Passwort-Hashing benötigen. Um die oben genannten Angriffe zu vereiteln, wird zunächst ein zufälliges Salt erzeugt. Dieser Salt wird mit einem Zähler verkettet und dann wird ein Keyed-Hash Message Authentication Code (HMAC) mehrmals auf diese Zeichenfolge angewendet. Ein HMAC erzeugt einen Hash, der mit einem Schlüssel authentifiziert ist, das heißt, nur mit Kenntnis des Passworts kann derselbe Hash berechnet werden. Früher wurde hier SHA-1 als interne Hashfunktion verwendet, heute ist mindestens SHA-256 üblich. Zusätzlich besitzt die Funktion einen Iterationszähler. Dieser bestimmt, wie oft das Verfahren wiederholt wird. Je größer dieser ist, desto ressourcenintensiver ist die Ausführung und somit werden Brute-Force-Angriffe erschwert, allerdings dauert dann auch die Überprüfung beim Login länger. Durch Parametrisierung kann dieser Wert je nach Anwendungsfall angepasst werden, wenn sich die zur Verfügung stehende Rechenleistung weiterentwickelt. Der ursprüngliche RFC 2898 aus dem Jahr 2000 empfahl noch einen Iteration Count von mindestens 1.000, während aktuell nach einer Richtlinie des National Institute of Standards and Technology (NIST) aus dem Jahr 2016 das Minimum bei 10.000 liegt. Der Standardwert in Spring-Security liegt sogar bei 185.000 Iterationen.

In Spring-Security existiert PBKDF2 seit Version 4.1 und wird wie folgt verwendet:

	
	Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
		// erzeuge den Hash
		String encoded = encoder.encode("geheim");
		// validiere den Hash
		boolean isValidPassword = encoder.matches("geheim", encoded);
	

Mit encode() wird im Default ein Hex-String in folgender Form erstellt:

	
	8d616f6522e36dff7627149c17f77ddc59efacedd22d47ffc95073f9f2159c23f27b1cc959ec7bf4.
	

Dieser enthält ein 64 Bit Salt, das jedes Mal automatisch generiert wird, sowie den eigentlichen Hashwert. Das bedeutet, dass wir uns um die Speicherung des Salts keine Gedanken mehr machen müssen, da es direkt mitgespeichert wird.

Auf einem Entwicklerlaptop mit Intel i9 benötigt die einmalige Ausführung ca. 700 ms, was gut zu unserer Vorbedingung von 0,5 - 1 Sekunde Ausführungszeit passt. Brute-Force-Angriffe gegen PBKDF2 sind vor allem mit spezialisierter Hardware wie GPUs und FPGAs möglich, da die verwendeten Hashfunktionen keinen großen Speicherbedarf haben. Das NIST empfiehlt PBKDF2 dennoch, wenn ein hoher Iterationszähler verwendet wird. Verfahren mit höherem Speicherbedarf haben jedoch den Vorteil, dass sie mit spezialisierter Hardware nicht so einfach bruteforced werden können, da beispielsweise FPGAs meist nur über wenig Speicher verfügen. Diesen höheren Speicherbedarf hat zum Beispiel Bcrypt.

Bcrypt

Bcrypt wurde 1999 auf der Usenix-Konferenz vorgestellt und explizit für das Hashing von Passwörtern entwickelt. Es verwendet die Blowfish-Verschlüsselung. Dabei handelt es sich um ein symmetrisches Verschlüsselungsverfahren mit mehreren Runden. Für jede Runde wird ein neuer Schlüssel abgeleitet. Bcrypt nutzt nun aus, dass diese Schlüsselableitung der Rundenschlüssel ressourcenintensiv ist. Es verschlüsselt den festen Klartext “OrpheanBeholderScryDoubt” mit Rundenschlüsseln, die aus dem zu haschenden Passwort und dem Salt abgeleitet werden. Dazu wird das zu haschende Passwort zuvor mit einem 128 Bit langen Salt konkateniert. Die Anzahl der Runden wird über einen Parameter cost gesteuert, so dass auch hier auf eine Verbesserung der Rechenleistung reagiert werden kann. Das Verfahren wird dann 2 hoch cost wiederholt. Zusätzlich wurde die Blowfish-Verschlüsselung so angepasst, dass mehr Speicher benötigt wird und Optimierungen auf GPUs und FPGAs entsprechend schlechter anwendbar sind, was einen Vorteil gegenüber PBKDF2 darstellt.

Die Parameterempfehlung im Jahr 1999 war ein Wert zwischen 6 und 8, während heute (Stand Ende 2020) der Standardwert 10 ist und die OWASP Foundation eine Erhöhung auf 12 empfiehlt. Diese einfache Parametrisierbarkeit mit klaren Empfehlungen macht Bcrypt zu einem empfehlenswerten und einfach anzuwendenden Verfahren.

Auch in Spring-Security sollte der Arbeitszyklus standardmäßig erhöht werden. Analog zu PBKDF2 wird Bcrypt dort so verwendet:

	
	BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
	// erzeuge den Hash
	String encoded = encoder.encode("geheim");
	// validiere ein Passwort anhand des Hashes
	boolean isValidPassword = encoder.matches("geheim", encoded);
	

Der String, den Bcrypt erzeugt, hat allerdings eine etwas andere Form als von PBKDF2 bekannt und sieht so aus:

	
	$2a$12$7Dj5dRTbzw/9YiaeJnGRrezIw4YcdoD2PTyE22xBIolQonzzPx02u.
	

Der String enthält jeweils $-separiert die Version 2a des verwendeten Bcrypts, den verwendeten Working Factor und dann Base64-codiert das Salt und den eigentlichen Hash. Er enthält also alles, um ein Passwort anhand des Hashes verifizieren zu können und kann auch so in der Datenbank gespeichert werden.

Der Speicherbedarf ist mit 4 KB zwar größer als bei PBKDF2, aber immer noch relativ gering. Ein Nachfolger von Bcrypt nahm am Password Hashing Competition teil. Dieser Wettbewerb wurde jedoch von Argon2 gewonnen.

Argon2

Der Gewinner der Password Hashing Competition 2015 war Argon2. Das Verfahren sorgt durch die Bildung eines großen internen Vektors für eine bessere Speichernutzung, so dass Brute-Force-Angriffe mit spezialisierter Hardware weniger Optimierungspotenzial haben. Intern wird die Hashfunktion Blake2b verwendet. Es gibt mehrere Versionen von Argon2:

  • Argon2d: Der Indexzugriff auf den internen Vektor erfolgt passwort- bzw. salt-abhängig und ist daher anfällig für Cache-Timing Side-Channel Angriffe. Dabei wird anhand der Laufzeit gemessen, welche Einträge des Vektors gecacht werden konnten (etwas Anwendungen in Backend-Servern und Kryptowährungen).
  • Argon2i: Der Indexzugriff erfolgt unabhängig vom mitgegebenen Geheimnis und ist daher Side-Channel resistent, aber auf spezialisierter Hardware besser optimierbar (Anwendungen beispielsweise in der Festplattenverschlüsselung).
  • Argon2id: Hybride Version aus dem Jahr 2017, weniger anfällig für Side-Channel Leakage als Argon2d und besser gegen Optimierung durch spezialisierte Hardware geschützt als Argon2i.

Alle Varianten bieten drei verschiedene Möglichkeiten der Parametrierung:

  • Speicher m: der zu verwendende Speicher (in KB)
  • Parallelität p: Anzahl der parallel nutzbaren Threads
  • Iterationen t: beeinflusst die Rechenzeit

Das Originalpapier gibt keine konkrete Empfehlung für diese Parameter. Alle Parameterwerte sollten so hoch wie möglich gewählt werden.

Die Standardparameter von Argon2id (das einzige Argon2-Verfahren in Spring-Security) liefern jedoch nur eine Laufzeit von 80 ms auf dem Entwicklerlaptop. Daher habe ich den Parameter t so weit erhöht, dass eine Laufzeit von ca. 0,5 Sekunden erreicht wird (gewählte Parametrisierung: m = 4096, p = 1, t = 90). Damit liegt die Laufzeit mit den gewählten Parametern im Mittelfeld (PBKDF2: 700 ms, Bcrypt: 200 ms). Ein Hinweis noch zum Parallelisierungsparameter p. Da die Bouncy-Castle-Implementierung die Parallelisierung derzeit nicht ausnutzt, lohnt es sich nicht, diesen Parameter in Spring-Security zu erhöhen.

Die Verwendung in Spring-Security funktioniert analog zu den beiden anderen Verfahren für einen 32 Byte Hash zusammen mit einem 16 Byte Salt:

	
	Argon2PasswordEncoder encoder = new Argon2PasswordEncoder(16, 32, 1, 4_096, 90);
	// erzeuge den Hash
	String encoded = encoder.encode("geheim");
	// validiere eine Passwort mithilfe des Hashes
	boolean isValidPassword = encoder.matches("geheim", encoded);
	

Der Hash des Passworts beinhaltet, wie bei Bcrypt, die Parameter, das Salt, die Argon2-Version und den Hash an sich:

	
	$argon2id$v=19$m=4096,t=90,p=1$AGyqya19qixSfWIXiCNkWg$eqvMYr17qyvvOHeksMc0WNN7jgwphxt0ugiCzbxusVk
	

Das höhere Sicherheitsniveau wird durch den höheren Ressourcenaufwand erreicht, da Brute-Force-Angriffe durch den höheren Speicherbedarf erschwert werden. Das Bundesamt für Sicherheit in der Informationstechnik (BSI) empfiehlt ab 2020 Argon2id als Passwort-Hashing-Mechanismus (verweist aber zur Parametrisierung auf “Experten”).

Fazit

Es reicht nicht aus, Passwörter einfach zu “hashen”. Brute-Force- und Wörterbuch-Attacken sind effektive Angriffe gegen diese Hashes. Durch den Einsatz eines Salts in Verbindung mit einer künstlichen Verlangsamung des verwendeten Hash-Verfahrens können diese Angriffe vereitelt werden. Mit Spring-Security kann jedes der drei vorgestellten Verfahren einfach und schnell implementiert werden. Dabei lösen die Implementierungen in der Regel bereits das Problem der Salt- und Parameterspeicherung. Eine zusätzliche Versionierung, beispielsweise in einer zusätzlichen Spalte der Datenbank, ist jedoch sinnvoll, um veraltete Hashes schneller zu finden. Zusammenfassend kann zu den Verfahren folgendes gesagt werden:

  • PBKDF2 wird derzeit vom NIST als ausreichend sicher eingestuft, hat aber eine sehr geringe Speicherauslastung, die es anfällig für Brute-Force-Angriffe macht.
  • Die Parametrisierung von Bcrypt ist einfach und relativ sicher.
  • Argon2 ist aufgrund seiner ausgefeilten Parametrisierbarkeit auch im Hinblick auf die Speichernutzung das derzeit vom BSI empfohlene Verfahren.

Weitere spannende Themen aus der adesso-Welt findet ihr in unseren bisher erschienenen Blog-Beiträgen.

Bild Torsten Böttinger

Autor Torsten Böttinger

Torsten ist Software Engineer bei adesso und im IT-Security Umfeld unterwegs. Besonders interessiert ist er dabei an der Kryptographie sowie Deep-Learning gestützten Cyber-Angriffen.

Diese Seite speichern. Diese Seite entfernen.