Passwörter speichern: Hashing und Salt in Java und PHP

Keine Zeit? Hier die Kurzversion in 30 Sekunden.
- Passwörter gehören nie im Klartext in die Datenbank, und MD5 ohne Salt ist auch keine echte Lösung.
- Ein zufälliger Salt pro Benutzer verhindert, dass fertige Tabellen (Rainbow Tables) funktionieren.
- Mehrfaches Hashen (Key Stretching) macht jeden einzelnen Versuch beim Raten teurer.
- PHP:
crypt()mit Blowfish ($2a$) erledigt Salt und Rechenaufwand in einem, ab PHP 5.3 ist es immer verfügbar.- Java 6: SHA-256 mit
SecureRandom-Salt und Iterationen, oder gleich PBKDF2 überSecretKeyFactory.
Ab und zu liest man wieder von einer geknackten Seite, bei der die Passwörter einfach so in der Datenbank lagen. Deshalb mal ein Überblick, wie man es besser macht. Zuerst die Theorie, dann Code für PHP und Java, weil ich gerade in beiden unterwegs bin.
Was man nicht tun sollte
Klartext ist klar tabu. Wer die Datenbank in die Finger bekommt, hat sofort alle Zugänge, und viele Leute benutzen dasselbe Passwort auch woanders.
Ein einfacher Hash wie md5($passwort) ist besser, aber nicht gut. Gleiche Passwörter ergeben gleiche Hashes, und für häufige Passwörter gibt es fertige Tabellen (Rainbow Tables) oder man probiert sie einfach durch. MD5 und SHA-1 sind außerdem sehr schnell, was beim Raten genau der falsche Vorteil ist.
Zwei Dinge helfen:
- Salt: Für jeden Benutzer ein eigener, zufälliger Wert, der vor dem Hashen zum Passwort kommt und im Klartext neben dem Hash gespeichert wird. Geheim muss er nicht sein, aber jeder Benutzer braucht einen anderen.
- Key Stretching: Man hasht nicht einmal, sondern ein paar tausend Mal. Für einen Login merkt man das nicht, für jemanden, der Milliarden Passwörter probieren will, schon.
PHP: crypt() mit Blowfish
In PHP muss man das Rad nicht neu erfinden. crypt() kann Blowfish, und seit PHP 5.3 ist das auf jedem System dabei, weil PHP eine eigene Implementierung mitbringt. Der Salt beginnt mit $2a$, dann kommen zwei Ziffern für den Aufwand und 22 Zeichen aus ./A-Za-z0-9:
<?php
function salt_erzeugen($aufwand = 10) {
$zufall = openssl_random_pseudo_bytes(16);
$zeichen = strtr(substr(base64_encode($zufall), 0, 22), '+', '.');
return sprintf('$2a$%02d$', $aufwand) . $zeichen;
}
// Registrieren
$hash = crypt($passwort, salt_erzeugen());
// $hash in die Datenbank speichern
// Login
if (crypt($eingabe, $hash_aus_db) === $hash_aus_db) {
echo 'Passwort stimmt';
}Der Trick beim Prüfen: Das Ergebnis von crypt() enthält den Salt und den Aufwand schon am Anfang. Übergibt man den gespeicherten Hash als Salt, nimmt PHP sich alles Nötige selbst heraus. Ihr braucht also nur eine Spalte, am besten VARCHAR(60). Die Zufallsfunktion openssl_random_pseudo_bytes braucht die OpenSSL-Erweiterung; mt_rand würde ich für Salts lieber nicht nehmen.
Java: SHA-256 mit Salt und Iterationen
In Java gibt es in der Standardbibliothek kein crypt(), dafür MessageDigest und SecureRandom. Selbst gebaut sieht das so aus:
import java.security.MessageDigest;
import java.security.SecureRandom;
import javax.xml.bind.DatatypeConverter;
public class PasswortHash {
private static final int ITERATIONEN = 5000;
public static byte[] neuerSalt() {
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
return salt;
}
public static byte[] hash(String passwort, byte[] salt) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt);
byte[] h = md.digest(passwort.getBytes("UTF-8"));
for (int i = 1; i < ITERATIONEN; i++) {
md.reset();
h = md.digest(h);
}
return h;
}
public static void main(String[] args) throws Exception {
byte[] salt = neuerSalt();
byte[] h = hash("geheim", salt);
System.out.println(DatatypeConverter.printBase64Binary(salt));
System.out.println(DatatypeConverter.printBase64Binary(h));
}
}Salt und Hash speichert ihr beide, zum Beispiel als Base64-Text in zwei Spalten. Beim Login rechnet ihr mit dem gespeicherten Salt neu und vergleicht die Ergebnisse, etwa mit java.util.Arrays.equals.
Besser: PBKDF2
Eigene Schleifen sind okay zum Verstehen, aber für Produktivcode würde ich lieber ein etabliertes Verfahren nehmen. Java 6 bringt PBKDF2 schon mit:
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
PBEKeySpec spec = new PBEKeySpec(passwort.toCharArray(), salt, 5000, 160);
byte[] hash = f.generateSecret(spec).getEncoded();Die Parameter sind Passwort, Salt, Iterationen und die Länge des Ergebnisses in Bit. Die Iterationszahl solltet ihr nicht zu klein wählen und von Zeit zu Zeit überdenken, weil Rechner schneller werden.
Wie macht ihr das in euren Projekten? Und habt ihr alte Anwendungen, in denen noch MD5 steckt? Dann wäre ein Umstieg beim nächsten Login eine Idee. Bis zum nächsten Mal!
Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (PHP logo: Colin Viebrock, gemeinfrei); die Marken gehören ihren Inhabern.
Christian Ackermann
Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.