bcrypt und Argon2: Warum Passwörter anders gehasht werden

Im letzten Beitrag habe ich einen bcrypt-Hash auseinandergenommen und dabei behauptet, Passwort-Hashing funktioniere grundlegend anders als das Hashing, das man sonst so kennt. Das will ich einlösen.

Denn wer Hashfunktionen aus der Ausbildung oder dem Studium kennt, hat meistens SHA-256 oder MD5 im Kopf. Und wer damit im Kopf an Passwörter herangeht, baut zuverlässig etwas Kaputtes.

Das Missverständnis: Geschwindigkeit

SHA-256 ist ein ausgezeichneter Hash. Er ist schnell, er ist gut untersucht, er verteilt sauber. Wenn ich prüfen will, ob eine heruntergeladene Datei unterwegs beschädigt wurde, ist genau das die richtige Eigenschaft: Ich will die Prüfsumme über ein paar Gigabyte in Sekunden haben, nicht in Stunden.

Bei Passwörtern kehrt sich diese Eigenschaft um.

Man muss sich dafür den Angriffsfall vor Augen führen. Der interessante Fall ist nicht, dass jemand am Anmeldeformular Passwörter durchprobiert – das merkt man, und dagegen hilft eine Sperre nach zu vielen Fehlversuchen. Der interessante Fall ist: Die Datenbank ist weg. Ein Backup lag offen, eine SQL-Injection, ein kompromittierter Dienstleister. Der Angreifer hat die Hashes und rät ab jetzt offline, auf seiner eigenen Hardware, ohne Zeitdruck und ohne dass es jemand mitbekommt.

Und jetzt wird die Geschwindigkeit zum Problem. Eine gewöhnliche Grafikkarte rechnet SHA-256 in einer Größenordnung von Milliarden Hashes pro Sekunde. Gegen eine Liste der gängigsten Passwörter ist eine solche Datenbank in Minuten durch. Nicht, weil SHA-256 kaputt wäre – sondern weil es genau das tut, wofür es gebaut wurde, nur eben zugunsten des Angreifers.

Warum der Salt nur das halbe Problem löst

Der übliche nächste Schritt ist ein Salt: ein Zufallswert pro Passwort, der mitgehasht und im Klartext daneben gespeichert wird.

Der hilft auch – nur gegen etwas anderes, als viele denken. Ein Salt verhindert, dass vorberechnete Tabellen funktionieren, und er sorgt dafür, dass zwei Nutzer mit demselben Passwort nicht denselben Hash bekommen. Der Angreifer kann also nicht mehr die ganze Datenbank auf einen Schlag angreifen, sondern muss jeden Eintrag einzeln vornehmen.

Was der Salt nicht tut: einen einzelnen Angriff verlangsamen. Wer es auf ein bestimmtes Konto abgesehen hat, rechnet weiterhin mit Milliarden Versuchen pro Sekunde. Der Salt vervielfacht die Arbeit, aber er macht den einzelnen Versuch nicht teurer.

Genau da setzen die Verfahren an, um die es hier geht.

bcrypt: absichtlich langsam

bcrypt dreht die Anforderung um. Es ist nicht trotz, sondern wegen seiner Langsamkeit gut. Und diese Langsamkeit ist einstellbar – über den Kostenfaktor, den ich im letzten Beitrag schon im Hash gezeigt hatte:

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPj/rCWDfe1Nu
    └┬┘
     └─ Kostenfaktor: 2¹² = 4096 Durchläufe

Die 12 ist ein Exponent, kein Faktor. Sie bedeutet 2¹² – also 4096 Durchläufe der internen, bewusst aufwendig gestalteten Schlüsselaufbereitung. Der entscheidende Punkt daran: Jede Erhöhung um eins verdoppelt den Aufwand. Von 12 auf 13 ist keine kleine Nachjustierung, das ist die doppelte Rechenzeit – für mich beim Anmelden genauso wie für den Angreifer beim Raten.

Das ist der eigentliche Kniff. Hardware wird schneller, und der Parameter wächst einfach mit. Ein Hash, der heute mit Kostenfaktor 12 gespeichert wurde, lässt sich in fünf Jahren mit 14 neu berechnen, ohne dass am Verfahren etwas geändert werden muss.

Zwei Dinge sollte man bei bcrypt allerdings wissen.

Das erste ist ein praktischer Stolperstein: bcrypt verarbeitet maximal 72 Byte. Alles darüber wird stillschweigend abgeschnitten. Wer eine Passphrase mit 90 Zeichen wählt, ist damit nicht sicherer als mit den ersten 72 – und bekommt davon nichts mit. Wer Passwörter vorher noch durch einen anderen Hash schickt, um diese Grenze zu umgehen, sollte genau wissen, was er tut, denn dabei lässt sich einiges falsch machen.

Das zweite ist der Grund, warum es einen Nachfolger gibt: bcrypt braucht nur etwa 4 KB Arbeitsspeicher. Das ist wenig genug, dass es bequem in den schnellen Cache einer GPU passt – und damit kann ein Angreifer sehr viele Berechnungen gleichzeitig laufen lassen. bcrypt macht den einzelnen Versuch teuer, aber es hindert niemanden daran, zehntausend davon parallel zu rechnen.

Argon2: teuer im Speicher

Hier setzt Argon2 an, der Gewinner der Password Hashing Competition von 2015 und seit 2021 in RFC 9106 beschrieben.

Die Idee: Wenn sich Rechenzeit durch mehr Silizium erschlagen lässt, dann muss das Verfahren eben etwas kosten, das sich nicht so einfach vervielfachen lässt. Und das ist Speicher. Argon2 füllt beim Hashen einen konfigurierbaren Speicherbereich und greift wiederholt darauf zu. Wer tausend Berechnungen parallel fahren will, braucht tausendmal diesen Speicher – und Speicher ist auf einer Grafikkarte oder einem Spezialchip die teure, nicht beliebig vermehrbare Ressource.

Entsprechend hat Argon2 drei Parameter statt einem:

Und es gibt drei Varianten, die gern für Verwirrung sorgen:

Der fertige Hash trägt seine Parameter genauso vor sich her wie bei bcrypt, nur ausführlicher:

$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
 └──┬───┘ └┬─┘ └──────┬──────┘ └────┬────┘ └──────────────┬───────────────┘
    │      │          │             │                     └─ Hash
    │      │          │             └─ Salt
    │      │          └─ 19 MiB Speicher, 2 Durchgänge, 1 Strang
    │      └─ Version 1.3 (0x13 = 19)
    └─ Variante

Auch hier gilt wieder: Alles, was zum Prüfen nötig ist, steht im Hash selbst. Deshalb funktioniert die Anmeldung auch dann noch, wenn die Parameter längst hochgesetzt wurden – der alte Hash bringt seine eigenen mit.

Was nehme ich denn nun?

Die OWASP-Empfehlung für Passwortspeicherung ist in ihrer Rangfolge deutlicher, als viele erwarten:

  1. Argon2id – erste Wahl
  2. scrypt – wenn Argon2id nicht verfügbar ist
  3. bcrypt – ausdrücklich für Altsysteme
  4. PBKDF2 – wenn FIPS-140-Konformität gefordert ist

bcrypt ist also nicht kaputt, aber es ist nicht mehr das, womit man Neues anfängt.

Als Startwerte nennt OWASP für Argon2id unter anderem m=19456 (19 MiB), t=2, p=1 als Minimum, alternativ m=47104 (46 MiB) mit t=1. Für bcrypt gilt ein Kostenfaktor von mindestens 10, in der Praxis nimmt man heute eher 12.

Diese Zahlen sind allerdings nur der Ausgangspunkt. Die eigentliche Regel lautet: so teuer wie möglich, ohne dass die Anmeldung unzumutbar wird. Und das lässt sich nicht aus einer Tabelle ablesen, das muss man auf der eigenen Hardware messen – mit dem Serverblech, das man tatsächlich hat, und unter Last, nicht auf dem Entwicklungsrechner im Leerlauf. Ein guter Zielwert liegt im Bereich von etwa 100 Millisekunden pro Prüfung; wer mehr Luft hat, nimmt mehr. Dabei nicht vergessen: Dieser Aufwand fällt bei jeder einzelnen Anmeldung an, und bei einem Ansturm auf die Anmeldemaske entsprechend oft.

Vom Alten zum Neuen

Bleibt die Frage, was mit den Hashes passiert, die schon in der Datenbank liegen. Umrechnen lassen sie sich nicht – das ist ja gerade der Sinn der Sache.

Der übliche Weg ist, es beim Anmelden zu erledigen: Der Nutzer gibt sein Passwort ein, es wird wie bisher gegen den alten Hash geprüft, und wenn es stimmt, liegt das Klartext-Passwort für einen Moment ohnehin vor. Genau dann wird es mit dem neuen Verfahren neu gehasht und ersetzt. Wer sich anmeldet, wandert mit, ohne etwas zu merken. Dasselbe Vorgehen funktioniert übrigens auch, wenn man nur den Kostenfaktor anhebt – die Bibliotheken bringen dafür meist eine fertige Abfrage mit, ob ein vorhandener Hash noch den aktuellen Parametern entspricht.

Für alle, die jahrelang nicht wiederkommen, bleibt am Ende nur, die Reste irgendwann zu entwerten und über „Passwort vergessen” neu setzen zu lassen.

Und das Wichtigste zum Schluss

Nichts davon selbst bauen. Keine Eigenkonstruktion aus SHA-256 in einer Schleife, kein selbst ausgedachtes Salt-Schema. Jede ernstzunehmende Plattform bringt geprüfte Implementierungen mit, und die richtige Aufgabe besteht darin, die passenden Parameter zu wählen und sie regelmäßig zu überprüfen – nicht darin, Kryptografie nachzubauen.

Wer sich die beiden Formate einmal in Ruhe ansehen will: bcrypt.rellit.de und argon2.rellit.de erzeugen und prüfen die Hashes im Browser und zerlegen dabei jeden Bestandteil so, wie ich es hier beschrieben habe. Am anschaulichsten ist es, einmal den Kostenfaktor schrittweise hochzudrehen und zuzusehen, wie die Berechnung spürbar träge wird. Genau dieser Effekt ist der Schutz.