Dienstag, 29. März 2011

Passwörter hashen, salzen, speichern

Vor einiger Zeit habe ich mich ja schon mal über Passwörter in Webanwendungen ausgelassen. Eigentlich ist ohnehin Konsens, dass Passwörter nicht im Klartext in die Datenbank geschrieben werden sollten. Besser ist es einen Hash aus dem Passwort zu erzeugen, der dann nur noch beim Authentifizieren verglichen wird. Weil beim Knacken von Passwörtern aber auch auf Fertig-Hashes zurückgegriffen werden kann, macht es Sinn einen weiteren, zufälligen Wert zum Hashen des Passworts hinzuzuziehen - den Salt. Der Salt muss zwar ebenfalls bei jeder Authentifizierung bereit stehen und gespeichert werden, trotzdem erschwert der Salt das Knacken. Der Salt wird für jedes Passwort zufällig erzeugt, so das selbst die Hashes identischer Passwörter unterschiedlich sind. Um das an einem Beispiel zu zeigen, habe ich mal was vorbereitet (Die Hilfsmethoden- und Klassen folgen am Ende des Beitrags):

public SaltedPass flavorWithSalt(String passwd) throws Exception {
  final Random rand = new Random();
  final MessageDigest m = MessageDigest.getInstance("MD5");
  final byte[] salt = new byte[12];

  rand.nextBytes(salt);
  m.update(salt);
  m.update(passwd.getBytes("UTF8"));
  byte hash[] = m.digest();

  return new SaltedPass(toHex(salt), toHex(hash));
}

Der Methode flavorWithSalt wird das gewünschte Benutzerpasswort im Klartext übergeben. Die Methode generiert einen Zufallswert, der als Salt verwendet und beim Hashen einbezogen wird, sowie den Hash selbst. Die Anwendung darf nur den zurückgegeben Hash und den Salt speichern.

Für die Authentifizierung ist die Passworteingabe im Klartext und der Salt erforderlich. Beides ergibt wieder einen Hash, der mit dem gespeicherten Hash verglichen wird. Sind die Hashes identisch, ist die Authentifizierung erfolgreich.

public boolean validate(String passwd, SaltedPass flavored) throws Exception {
  final MessageDigest m = MessageDigest.getInstance("MD5");
  final byte[] salt = fromHex(flavored.salt);

  m.update(salt);
  m.update(passwd.getBytes("UTF8"));
  byte hash[] = m.digest();
  if(toHex(hash).equals(flavored.hash)) {
    return true;
  }
  return false;
}

Damit das ganze compiliert, sind noch die Methoden zum Wandeln der Byte-Arrays in ihre hexadezimale String-Representation und zurück, sowie die Klasse SaltedPass, die als Dataholder dient, erforderlich.

private String toHex(byte[] bytes) {
  final StringBuilder result = new StringBuilder(bytes.length * 2);
  for (int i = 0; i < bytes.length; i++) {
    result.append(Integer.toHexString((0x000000ff & bytes[i]) |   0xffffff00).substring(6));
  }
  return result.toString();
}

public static byte[] fromHex(String hexString) {
  final char[] hex = hexString.toCharArray();
  final int length = hex.length / 2;
  final byte[] raw = new byte[length];

  for (int i = 0; i < length; i++) {
    int high = Character.digit(hex[i * 2], 16);
    int low = Character.digit(hex[i * 2 + 1], 16);
    int value = (high << 4) | low;
    if (value > 127) {
      value -= 256;
    }
    raw[i] = (byte) value;
  }
  return raw;
}

private class SaltedPass {
  private String salt;
  private String hash;

  SaltedPass(String salt, String hash) {
    this.salt=salt;
    this.hash = hash;
  }

  public String toString() {
    return "salt:" + salt + " hash:" + hash;
  }
}

Und zuletzt noch die Main-Methode zum Ausprobieren:

public static void main(String[] args) throws Exception {
  String pass = "testpass";
  SaltedPass flavorWithSalt = new SaltTrialout().flavorWithSalt(pass);
  System.out.println(flavorWithSalt);

  boolean validate = new SaltTrialout().validate(pass, flavorWithSalt);
  System.out.println(validate);
}

Update 30.3.11
Source zum Download

Kommentare:

  1. Hallo,
    Schöner Artikel und gut erklärt. Noch besser wäre noch der Download einer fertigen .java-Datei, so wäre ein besserer Überblick über die Import-Statements möglich.

    Gruß Maik

    AntwortenLöschen
  2. Super Artikel von Heise zu dem Thema: http://www.heise.de/security/artikel/Passwoerter-unknackbar-speichern-1253931.html?artikelseite=1

    AntwortenLöschen