Sichere Passwort-Hashes in Java und Kotlin

Dass Passwörter nicht im Klartext gespeichert werden dürfen, sollte mittlerweile Allgemeinwissen sein: Sollte aus irgendeinem Grund die Benutzerdatenbank kompromittiert werden, bekäme der Angreifer Zugriff auf sämtliche Passwörter.

Lange Zeit lautete die Empfehlung, zuerst MD5-, dann SHA1- und schließlich SHA2-Hashes mit einem zufälligen Salt zu verwenden. Allerdings gelten die MD5- und SHA1-Algorithmen mittlerweile als unsicher. Bei SHA2 gibt es zwar noch keine fundamentalen Sicherheitsprobleme, aber der Algorithmus ist zu schnell. Sofern die Implementierung der Hash-Funktion bekannt ist, können CPUs/GPUs Millionen wenn nicht Milliarden von Passwörter pro Sekunde testen. Das ist besonders dann fatal, wenn unsichere Passwörter in der Art von 123456 oder topsecret zum Einsatz kommen, die in entsprechenden Passwort-Wörterbuchdateien an den ersten Stellen enthalten sind.

Gegen schlechte Passwörter helfen auch Key Derivation Functions nicht. KDFs verlangsamen aber Cracking-Angriffe um Größenordnungen. Dabei handelt es sich um Algorithmen, die im Prinzip wie Hash-Funktionen funktionieren, aber in zweierlei Hinsicht für den Umgang mit Passwörtern optimiert sind:

  • Während normale Hash-Algorithmen in der Regel für große Datenmengen gedacht sind (z.B. um rasch einen eindeutigen Hash-Code für ein ISO-Image mit 3 GiB errechnen), sind Key Derivation Functions für winzige Datenmengen optimiert (also Passwörter, die nur wenige Byte lang sind).

  • Die Algorithmen sind bewusst langsam konzipiert und erfordern viele serielle Durchläufe. Es mutet absurd an, das als Vorteil zu sehen — aber je schneller (GPU-unterstützte) Passwort-Cracker sind, desto wichtiger ist es, algorithmisch eine Bremse einzubauen.

Bekannte Key Derivation Functions bzw. Bibliotheken mit KDFs sind Argon2, Bcrypt, libsodium, PBKDF2 sowie Scrypt.

Bcrypt

Mit Bcrypt empfehle ich Ihnen hier nicht das kryptographisch beste Projekt, sondern das in seiner Anwendung einfachste. (Manche Experten sind sogar der Meinung, dass Bcrypt streng genommen gar keine KDF ist.)

Anders formuliert: Es gibt technisch gesehen noch bessere Lösungen, aber Bcrypt sollte zum gegenwärtigen Stand selbst bei hohen Anforderungen ausreichend gut sein — und auf jeden Fall besser als traditionelle Verfahren auf der Basis von MD5/SHA/SHA2. Der entscheidende Vorteil von Bcrypt ist seine Einfachheit:

  • Um den Hash eines Passworts zu erzeugen, reicht eine Zeile Code.

  • Um ein Passwort mit dem Hashes zu vergleichen, reicht noch eine Zeile Code.

Damit ist es eigentlich unmöglich, Fehler bei der Anwendung von Bcrypt zu machen. (Je länger ich mit IT zu tun habe, desto vehementer bin ich ein Anhänger der KISS-Regel: Keep it simple, stupid!, also: Mach‘ die Dinge nicht komplizierter, als sie sind!)

jBcrypt

JBcrypt ist eine Java-Implementierung von Bcrypt. Im Folgenden setze ich voraus, dass Sie zur Entwicklung Ihres Programms das Build-Tool Gradle einsetzen. Damit Sie die winzige Bibliothek jBcrypt in Ihrem Java- oder Kotlin-Projekt verwenden können, bauen Sie in den dependencies-Block von build.gradle eine Zeile ein:

// in build.gradle
dependencies {
    ...
    implementation "org.mindrot:jbcrypt:0.4"
}

Die Anwendung von jBcrypt in Kotlin sieht so aus:

// Kotlin-Code
import org.mindrot.jbcrypt.BCrypt
...
val bchash = BCrypt.hashpw("geheim", BCrypt.gensalt())
println(bchash)  // z.B. $2a$10$CpvZJVOLMeNgnyXhZAluGe3/sUrhULxOgSPA94VcqNO70TZM/yEtG
println(BCrypt.checkpw("geheim", bchash))  // true
println(BCrypt.checkpw("Geheim", bchash))  // false

Wenn Sie (noch) mit Java arbeiten, sieht der Code so aus:

// Java-Code
import org.mindrot.jbcrypt.BCrypt;
...
String bchash = BCrypt.hashpw("geheim", BCrypt.gensalt());
System.out.println(bchash);
System.out.println(BCrypt.checkpw("geheim", bchash));  // true
System.out.println(BCrypt.checkpw("Geheim", bchash));  // false

Quellen/Links