Secure Coding: Passwort-Hashing zum Schutz vor Brute-Force und Rainbow-Tabellen

Seite 2: Anwendung von Argon2 in Java

Inhaltsverzeichnis

Um Argon2 in Java zu verwenden, bietet sich der Einsatz von Bibliotheken wie Jargon2 oder Bouncy Castle an, da Java von Haus aus keine Unterstützung für Argon2 mitbringt. Mit Jargon2 lässt sich Argon2 sehr einfach einbinden und verwenden, wie das folgende Codebeispiel verdeutlicht, das mit dem Hinzufügen einer Maven-Abhängigkeit beginnt:

<dependency>
    <groupId>com.kosprov</groupId>
    <artifactId>jargon2-api</artifactId>
    <version>AKTUELLE VERSIONS NUMMER</version>
</dependency>

Der Code zum Erstellen eines Hash und zum Verifizieren eines Passworts könnte wie folgt aussehen:

import com.kosprov.jargon2.api.Jargon2;

public class Argon2Example {
    public static void main(String[] args) {
        String password = "DeinSicheresPasswort";

        Jargon2.Hasher hasher = Jargon2.jargon2Hasher()
                .type(Jargon2.Type.ARGON2id)
                .memoryCost(65536)
                .timeCost(3)
                .parallelism(4)
                .saltLength(16)
                .hashLength(32);

        String hashedPassword = hasher
.password(password.getBytes())
.encodedHash();
        System.out.println("Gehashtes Passwort: " + hashedPassword);

        boolean matches = Jargon2.jargon2Verifier()
                .hash(hashedPassword)
                .password(password.getBytes())
                .verifyEncoded();
        System.out.println("Passwort korrekt: " + matches);
    }
}

Soll die umfassendere Kryptografie-Bibliothek Bouncy Castle verwendet werden, lautet die benötigte Maven-Abhängigkeit:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>AKTUELLE VERSIONS NUMMER</version>
</dependency>

Der Beispielcode fĂĽr die Nutzung von Argon2 mit Bouncy Castle sieht dann folgendermaĂźen aus:

import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import java.security.SecureRandom;

public class Argon2BouncyCastleExample {
    public static void main(String[] args) {
        String password = "DeinSicheresPasswort";
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);

        Argon2Parameters.Builder builder = new Argon2Parameters
.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withMemoryPowOfTwo(16)
                .withParallelism(4)
                .withIterations(3);

        Argon2BytesGenerator generator 
= new Argon2BytesGenerator();
        generator.init(builder.build());

        byte[] hash = new byte[32];
        generator.generateBytes(password.getBytes(), hash);

        System.out.println("Gehashtes Passwort: " 
+ bytesToHex(hash));
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : bytes) {
            hexString.append(String.format("%02x", b));
        }
        return hexString.toString();
    }
}

Während Jargon2 speziell für Argon2 entwickelt wurde und sich daher in der Praxis einfacher verwenden lässt, eignet sich Bouncy Castle vor allem für komplexere kryptografische Anforderungen.

Die Wichtigkeit, einen guten Salt-Wert zu erzeugen, wurde bereits aufgezeigt. Doch wie geht man dabei sinnvoll vor? Der folgende Abschnitt beschreibt dazu eine grundsätzliche erste Implementierung. Eine tiefergehende, detaillierte Behandlung des Themas folgt in einem späteren Secure-Coding-Beitrag.

Codebeispiel fĂĽr eine grundlegende Salt-Implementierung

public String generateSalt(int length) { 
    SecureRandom random = new SecureRandom(); 
    byte[] salt = new byte[length]; 
    random.nextBytes(salt); 
    return Base64.getEncoder().encodeToString(salt); 
} 

Zu beachten gilt es dabei, dass das Erzeugen eines Salts mit einem neu instanziierten SecureRandom in jeder Methode zwar nicht grundsätzlich falsch ist, aber es erhöht die Wahrscheinlichkeit, dass in sehr kurzen Zeitabständen oder unter bestimmten Umständen gleiche Seeds verwendet werden könnten. In der Praxis ist das selten ein Problem, dennoch sollte stets eine einzige (statische) Instanz von SecureRandom pro Anwendung (bzw. pro Klasse) verwendet werden.

Denn SecureRandom bezieht seinen Seed unter anderen vom Betriebssystem (z. B. aus /dev/urandom in Linux). Jedes erneute Erzeugen eines SecureRandom kann zu unnötiger Systemlast und theoretisch minimal erhöhtem Risiko von Wiederholungen führen. Bei einer einzigen SecureRandom-Instanz wird intern ein Pseudo-Zufallszahlengenerator weitergeführt und die Wahrscheinlichkeit von Duplikaten deutlich reduziert.

import java.security.SecureRandom;
import java.util.Base64;

public class SaltGenerator {

    // Einmalig in der Klasse angelegte Instanz von SecureRandom
    private static final SecureRandom RANDOM 
                = new SecureRandom();

    public static String generateSalt(int length) {
        byte[] salt = new byte[length];
        RANDOM.nextBytes(salt);
        return Base64.getEncoder().encodeToString(salt);
    }
}

Auf diese Weise wird (a) nicht bei jedem Aufruf erneut ein SecureRandom erzeugt und (b) das Risiko verringert, dass zufällig identische Salts generiert werden. Natürlich kann es rein statistisch trotzdem zu Kollisionen kommen, doch die Wahrscheinlichkeit ist bei genügend großer Salt-Länge ausreichend gering. (Die eingehendere Betrachtung folgt in einem späteren Beitrag).

Um bei einem Login-Prozess die Kombination aus Benutzername und Passwort zu überprüfen, folgt man einem standardisierten Vorgehen, das sicherstellt, dass die Daten sicher verarbeitet werden. Zunächst gibt der Benutzer seine Anmeldedaten über ein Formular ein, das über HTTPS übertragen wird. Auf dem Server wird dann der Benutzername in der Datenbank gesucht, um die relevanten Informationen wie Passwort-Hash und Salt abzurufen. Dabei ist darauf zu achten, keine Informationen preiszugeben, ob der Benutzer existiert.

Anschließend wird der gespeicherte Passwort-Hash mit einem neu berechneten Hash des eingegebenen Passworts unter Verwendung des gespeicherten Salts verglichen. Stimmen die Werte überein, gilt der Login als erfolgreich, andernfalls wird eine generische Fehlermeldung zurückgegeben, um keine zusätzlichen Informationen preiszugeben.

Nach erfolgreicher Verifizierung wird eine Session oder ein JSON Web Token (JWT) erstellt, um die Authentifizierung des Benutzers aufrechtzuerhalten. Hierbei enthält das Token keine sensiblen Informationen wie etwa Passwörter.

Noch ein paar Worte zu Strings in Java…

Sobald ein Passwort in einem Textfeld einer (Web)-Anwendung steht, liegt es als String vor. Warum das problematisch sein kann, hängt mit der Art und Weise zusammen, wie Strings in der JVM behandelt werden, und welche Angriffsvektoren sich dadurch ergeben können.

In Java sind Strings unveränderlich (immutable). Das bedeutet, dass ein einmal erstelltes String-Objekt nicht mehr geändert werden kann. Wird ein String manipuliert, beispielsweise durch Konkatenation, entsteht ein neues String-Objekt im Speicher, während das alte weiterhin existiert, bis der Garbage Collector (GC) es entfernt. Dieses Verhalten kann problematisch sein, wenn sensible Daten, wie Passwörter oder kryptografische Schlüssel, in Strings gespeichert werden. Da sie nicht direkt überschrieben werden können, bleiben sie unter Umständen länger als nötig im Speicher und sind potenziell in einem Speicher-Dump sichtbar oder für einen Angreifer auslesbar. Ein weiteres Problem besteht darin, dass Entwickler keine Kontrolle darüber haben, wann genau der Garbage Collector die sensiblen Daten entfernt. Dadurch kann es vorkommen, dass solche Daten über einen längeren Zeitraum im Speicher verbleiben und möglicherweise in Logs oder Debugging-Tools sichtbar werden.

Ein sicherer Ansatz besteht darin, statt Strings char[] zu verwenden, da diese veränderlich sind und der Speicher gezielt überschrieben werden kann. Dadurch lässt sich die Verweildauer sensibler Daten minimieren, insbesondere in sicherheitskritischen Anwendungen, in denen Speicher-Dumps oder Debugging-Tools Zugriff auf den Speicher gewähren könnten.

Der Vorteil von char[] liegt also in der direkten Speicherverwaltung: Während der Garbage Collector selbst entscheidet, wann er Objekte entfernt, kann ein char[]-Array explizit überschrieben und somit sofort gelöscht werden. Dies verringert das Risiko eines unbefugten Zugriffs.

import java.util.Arrays;

public class SensitiveDataExample {
    public static void main(String[] args) {
        char[] password = {'s', 'e', 'c', 'r', 'e', 't'};
        try {
            processPassword(password);
        } finally {
            // Ăśberschreiben des Speichers mit Dummy-Daten
            Arrays.fill(password, '\0');
        }
    }
    private static void processPassword(char[] password) {
        // Beispielhafte Verarbeitung
        System.out.println("Passwort verarbeitet: " 
                 + String.valueOf(password));
   }
}

Neben char[] sollten zusätzliche Sicherheitsmaßnahmen wie die Verschlüsselung sensibler Daten und die Nutzung von SecretKeySpec aus dem Paket javax.crypto.spec für kryptografische Schlüssel berücksichtigt werden. Diese Klasse ermöglicht die Handhabung kryptografischer Schlüssel in Byte-Arrays, die nach Gebrauch überschrieben werden können.

Die sichere Speicherung von Passwörtern ist ein essenzieller Bestandteil jeder Anwendung, um Benutzerdaten vor Missbrauch und unautorisiertem Zugriff zu schützen. Klartext-Passwörter stellen ein erhebliches Risiko dar und sollten niemals direkt gespeichert werden. Stattdessen helfen bewährte Hashing-Algorithmen wie PBKDF2, BCrypt oder Argon2, die Sicherheit zu gewährleisten.

Brute-Force- und Rainbow-Tabellen-Angriffe veranschaulichen die Wichtigkeit von Salts und ausreichend komplexer Hashing-Mechanismen. Durch den Einsatz von zufälligen Salts und iterativen Hashing-Methoden lässt sich das Risiko solcher Angriffe erheblich reduzieren. Besonders Argon2id bietet durch seine Speicher- und Rechenintensität eine hohe Widerstandsfähigkeit gegen ausgefeilte Angriffsmethoden und wird als bevorzugte Lösung empfohlen.

Darüber hinaus sollte auch die Verarbeitung von Passwörtern innerhalb der Anwendung mit Bedacht erfolgen. Der Einsatz von char[] anstelle von String für die Speicherung sensibler Daten kann dazu beitragen, ungewollte Speicherlecks zu vermeiden. Ebenso ist es wichtig, Passwörter niemals ungesichert im Speicher oder in Logs abzulegen.

Sichere Passwort-Hashing-Verfahren sind zudem nicht nur technische Best Practice, sondern in vielen Bereichen inzwischen auch eine gesetzliche Anforderung.

Happy Coding
Sven

(map)