~/blog/2012/06/linkedin-leak-bcrypt-statt-sha1.md

Nach dem LinkedIn-Leak: bcrypt statt SHA-1

5 min
Titelbild: Nach dem LinkedIn-Leak: bcrypt statt SHA-1

Keine Zeit? Hier die Kurzversion in 30 Sekunden.

  • Anfang Juni 2012 tauchten rund 6,5 Millionen SHA-1-Hashes von LinkedIn-Passwörtern auf, offenbar ohne Salt.
  • Schnelle Hashes wie SHA-1 oder MD5 lassen sich mit Grafikkarten und Wörterbüchern in großem Stil knacken; ohne Salt geht das für alle Konten gleichzeitig.
  • Passwörter gehören mit einem langsamen, gesalzenen Verfahren gespeichert: bcrypt, PBKDF2 oder scrypt.
  • Java: jBCrypt mit BCrypt.hashpw(pw, BCrypt.gensalt(12)) und checkpw, alternativ PBKDF2 aus dem JDK.
  • PHP: crypt() mit $2y$ ab PHP 5.3.7 (wegen eines Fehlers beim älteren $2a$).

Anfang Juni wurde bekannt, dass eine Datei mit rund 6,5 Millionen Passwort-Hashes von LinkedIn im Netz aufgetaucht ist. Nach allem, was berichtet wurde, handelte es sich um SHA-1-Hashes ohne Salt. Wie die Daten abgeflossen sind, war zum Zeitpunkt dieses Artikels noch nicht abschließend geklärt, und für die Frage, die mich hier interessiert, ist es auch zweitrangig. Wichtig ist, was ein Angreifer mit so einer Datei anfangen kann, und warum das mit einem besseren Verfahren deutlich schwerer wäre.

Warum SHA-1 ohne Salt nicht reicht

SHA-1 ist eine Hashfunktion, die für Geschwindigkeit entworfen wurde. Das ist für Prüfsummen und Signaturen gut, für Passwörter aber das Gegenteil dessen, was man will. Ein Angreifer rät Passwörter einfach: Er nimmt ein Wörterbuch, hängt gebräuchliche Abwandlungen daran, hasht alles und vergleicht mit der gestohlenen Liste. Grafikkarten können solche Hashes in enormer Zahl pro Sekunde berechnen, die Hardware dafür ist käuflich und nicht besonders teuer.

Ohne Salt kommt ein zweites Problem dazu. Zwei Nutzer mit dem gleichen Passwort haben den gleichen Hash, und ein einziger Rechenlauf deckt alle Konten ab. Fertige Tabellen mit vorberechneten Hashes (Rainbow Tables) machen es noch bequemer. Ein Salt, also ein zufälliger Wert pro Passwort, der mit gespeichert wird, verhindert beides: Jeder Hash muss einzeln angegriffen werden, und die vorberechneten Tabellen sind wertlos. Gegen das schnelle Raten hilft das Salt allerdings nicht. Dafür braucht man ein Verfahren, das absichtlich langsam ist.

Langsame Verfahren: bcrypt, PBKDF2, scrypt

Es gibt drei Kandidaten, die sich etabliert haben oder dabei sind. bcrypt stammt von 1999, baut auf der Blowfish-Schlüsselplanung auf und hat einen einstellbaren Kostenfaktor: Mit jedem Schritt verdoppelt sich der Aufwand. Das Salt steckt im Ergebnisstring, man muss sich darum nicht selbst kümmern. PBKDF2 ist ein Standard (RFC 2898) und wiederholt eine HMAC-Funktion sehr oft, die Anzahl ist frei wählbar. Und scrypt von Colin Percival braucht zusätzlich viel Arbeitsspeicher, was Angriffe mit Grafikkarten erschwert. Es ist aber recht neu, die Bibliotheken sind noch dünn gesät, und ich würde es in einem normalen Projekt derzeit nicht als Erstes einsetzen.

Mein Favorit ist bcrypt, weil es einfach zu benutzen ist und in nahezu jeder Sprache eine Implementierung existiert. PBKDF2 ist die Alternative, wenn man nur mit Bordmitteln arbeiten will.

Java: jBCrypt und PBKDF2

Mit jBCrypt (eine kleine Java-Portierung von bcrypt) sieht das Speichern und Prüfen so aus:

String hash = BCrypt.hashpw(passwort, BCrypt.gensalt(12));

// beim Login
if (BCrypt.checkpw(eingabe, hash)) {
    // Passwort stimmt
}

Der Parameter 12 ist der Kostenfaktor (Logarithmus der Runden). Je höher, desto langsamer, und man sollte den Wert so wählen, dass ein Login auf dem eigenen Server noch angenehm schnell ist, aber nicht trivial. Das misst man am besten selbst, und es lässt sich später erhöhen, weil der Faktor im Hash gespeichert ist. Ein Hinweis noch: bcrypt berücksichtigt nur die ersten 72 Bytes des Passworts, für normale Passwörter spielt das keine Rolle.

Wer keine Fremdbibliothek einbinden will, findet PBKDF2 schon im JDK:

SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);

PBEKeySpec spec = new PBEKeySpec(passwort.toCharArray(), salt, 10000, 160);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] hash = f.generateSecret(spec).getEncoded();

Gespeichert werden Salt, Iterationszahl und Hash zusammen. Die 10.000 Iterationen sind nur ein Beispielwert; sinnvoll ist eine Zahl, bei der die Berechnung auf aktueller Hardware spürbar Zeit kostet. Beide Methoden werfen geprüfte Exceptions (NoSuchAlgorithmException, InvalidKeySpecException), die man behandeln muss. Etwas mehr Handarbeit als bei jBCrypt, aber ohne zusätzliche Abhängigkeit.

PHP: crypt() mit $2y$

In PHP gibt es bcrypt über crypt(), wenn man dem Salt den passenden Präfix voranstellt. Ab PHP 5.3.7 sollte man $2y$ nehmen: In älteren Versionen steckte in der Implementierung hinter $2a$ ein Fehler beim Umgang mit Zeichen außerhalb von ASCII, der mit 5.3.7 behoben wurde, und $2y$ kennzeichnet die korrigierte Variante.

$salt = substr(strtr(base64_encode(openssl_random_pseudo_bytes(16)), '+', '.'), 0, 22);
$hash = crypt($passwort, '$2y$12$' . $salt);

// beim Login
if (crypt($eingabe, $hash) === $hash) {
    // Passwort stimmt
}

Die 12 ist wieder der Kostenfaktor, danach folgen 22 Zeichen Salt aus dem Alphabet ./A-Za-z0-9. Das ist umständlicher, als es sein müsste. Es gibt deshalb Überlegungen, PHP selbst eine Funktion password_hash mitzugeben, die das alles kapselt. Ein Vorschlag (RFC) dazu liegt vor, ob und in welcher Form er in eine künftige Version kommt, ist offen. Bis dahin bleibt crypt().

Und die eigenen Altlasten

Im Oktober 2010 hatte ich hier beschrieben, wie ich Passwörter mit SHA-256 und mehreren Iterationen speichere. Das war besser als ein einfacher Hash, aber im Grunde eine selbst gebaute Variante von PBKDF2, und die Verantwortung für die Details lag bei mir. Heute würde ich bcrypt nehmen. Bestehende Datenbanken lassen sich ohne Zwangs-Reset umstellen: Beim nächsten erfolgreichen Login prüft man das Passwort mit dem alten Verfahren, berechnet gleich danach den neuen Hash und überschreibt den alten Wert. Wer die Liste der Nutzer mit altem Hash im Blick behalten will, markiert sie mit einer zusätzlichen Spalte.

Der LinkedIn-Fall zeigt vor allem, dass das Speichern von Passwörtern kein Randthema ist. Auch ein sauber gehashtes Passwort schützt nicht vor schwachen Passwörtern, aber es entscheidet, ob ein Datenabfluss nur Ärger oder auch noch den Zugriff auf alle anderen Konten der Betroffenen bedeutet.

Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (LinkedIn-Logo: LinkedIn, gemeinfrei); die Marken gehören ihren Inhabern.

$ whoami
Christian Ackermann

Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.