Warum Passwörter eine besondere Behandlung benötigen
Jedes andere Geheimnis in Ihrem System lässt sich rotieren: API keys, Tokens, Zertifikate. Passwörter sind anders. Sie liegen niemals in einer Form vor, die Sie regenerieren können, Nutzer verwenden sie auf verschiedenen Seiten immer wieder, und in dem Moment, in dem Ihre Datenbank leakt, hat der Angreifer alle Zeit der Welt, sie offline zu erraten.
Password Hashing ist die Praxis, eine Einweg-Transformation des Passworts anstelle des Passworts selbst zu speichern. Wenn sich ein Nutzer anmeldet, wenden Sie dieselbe Transformation an und vergleichen das Ergebnis. Sie erfahren niemals das Passwort, und das tut auch niemand, der die Tabelle stiehlt.
Dies ist kein Ort für Eigenkreationen. Die Regeln sind simpel und gut etabliert, und ihr Bruch ist der Grund, warum Datenlecks katastrophal werden.
Hashing ist keine Verschlüsselung
Verschlüsselung ist reversibel. Wenn Sie Passwörter verschlüsseln, befindet sich der Schlüssel irgendwo in Ihrem System. Jeder, der ihn in die Hände bekommt – durch ein Leak, ein Backup, eine falsch konfigurierte Umgebungsvariable oder einen Insider – kann sofort jedes Passwort lesen. Es gibt kein Szenario, in dem Sie das ursprüngliche Passwort benötigen, daher stellt die Reversibilität ein reines Risiko dar.
Eine Hash-Funktion nimmt einen Input entgegen und erzeugt einen Digest mit fester Länge; es gibt keinen praktischen Weg zurück. Für Passwörter reicht ein einfacher Hash jedoch nicht aus: Schnelle Funktionen wie SHA-256 sind darauf ausgelegt, schnell zu sein – genau das, was ein Angreifer will. Eine GPU kann Milliarden von Kandidaten pro Sekunde testen.
Sie benötigen eine Funktion, die absichtlich langsam und memory-hard ist, damit das Erraten von Passwörtern in großem Stil zu teuer wird.
Salt: Gleiches Passwort, unterschiedlicher Hash
Ohne einen Salt erzeugen zwei Benutzer mit dem Passwort hunter2 denselben Digest. Ein Angreifer kann eine riesige Tabelle mit gängigen Passwörtern und deren Hashes vorab berechnen – eine sogenannte Rainbow Table – und Treffer sofort finden. Zudem ist auf einen Blick erkennbar, welche Accounts dasselbe Passwort verwenden.
Ein Salt ist ein eindeutiger Zufallswert, der pro Passwort generiert und zusammen mit dem Digest gespeichert wird. Er ist nicht geheim; seine einzige Aufgabe ist es, jeden Hash einzigartig zu machen. Nun muss der Angreifer jeden Account einzeln angreifen, und Vorberechnungen sind nutzlos.
Moderne Bibliotheken generieren den Salt automatisch für Sie und kodieren ihn zusammen mit den Parametern in einem einzigen String. Diesen String speichern Sie einfach wortwörtlich ab.
Den richtigen Algorithmus wählen
Drei Funktionen sind wichtig zu kennen. Alle sind adaptiv: Die Kosten können erhöht werden, sobald die Hardware leistungsfähiger wird.
Argon2id
Der Gewinner der Password Hashing Competition 2015 und die aktuelle Empfehlung. Er ist „memory-hard“, was bedeutet, dass ein Angreifer neben Rechenleistung auch Speicher kaufen muss, wodurch der Vorteil von GPUs und ASICs abgeschwächt wird. Argon2id bietet eine gute Balance zwischen der Resistenz gegen Side-Channel- und GPU-Angriffe und ist der richtige Standard für neue Systeme.
scrypt
Standardisiert in RFC 7914 und ebenfalls memory-hard, ist scrypt eine solide Wahl, wenn Argon2 nicht verfügbar ist. Er bietet Parameter für Kosten, Blockgröße und Parallelität, sodass er feinjustiert werden kann, allerdings lassen sich diese Parameter leichter falsch konfigurieren als die von Argon2.
bcrypt
Veröffentlicht im Jahr 1999 und nach wie vor weit verbreitet, ist bcrypt gut verstanden und praxiserprobt. Sein Kostenfaktor verdoppelt den Aufwand mit jeder Erhöhung. Der wichtigste Vorbehalt ist, dass er die Eingabe bei 72 Bytes abschneidet, sodass sehr lange Passphrasen stillschweigend gekürzt werden – in diesem Fall sollten Sie die Eingabe vor dem Hashing vorverarbeiten.
Egal, wofür Sie sich entscheiden: Greifen Sie niemals zu MD5, SHA-1 oder einfachem SHA-256. Sie sind das falsche Werkzeug, und das Stapeln von Runden darauf ist ein Eigenbau-Schema, dem niemand vertrauen sollte.
Den Work Factor optimieren
Die Kosten müssen zwei gegensätzliche Anforderungen in Einklang bringen. Ist der Wert zu niedrig, ist der Hash anfällig für Brute-Force-Angriffe; ist er zu hoch, kann eine Flut von Login-Versuchen zu einem Denial-of-Service-Vektor gegen die eigene CPU werden.
Optimieren Sie den Wert so, dass ein einzelner Hash auf Ihrer Produktionshardware etwa 100 bis 500 Millisekunden benötigt. Messen Sie den Login-Endpoint unter realistischer Last und nicht nur in einer Benchmark-Schleife, und legen Sie den Wert basierend auf diesen Daten fest. Überprüfen Sie dies anschließend regelmäßig: Da die Hardware stetig besser wird, verlieren dieselben Parameter jedes Jahr an Sicherheit.
// Argon2id parameters (memory in KiB, iterations, parallelism)
await hash(password, { memoryCost: 19456, timeCost: 2, parallelism: 1 });
Speichern und Verifizieren
Die Registrierung und der Login sind die einzigen beiden Stellen, an denen Sie ein Passwort verarbeiten.
import { hash, verify } from "@node-rs/argon2";
// Registration: hash before the password ever reaches the database.
const digest = await hash(password);
await db.users.insert({ email, passwordHash: digest });
// Login: verify against the stored digest.
const user = await db.users.findByEmail(email);
const ok = user && (await verify(user.passwordHash, password));
if (!ok) return res.status(401).json({ error: "invalid_credentials" });
Geben Sie denselben Fehler zurück, unabhängig davon, ob die E-Mail oder das Passwort falsch war. Eine Nachricht wie „Benutzer nicht gefunden“ ermöglicht es einem Angreifer, Accounts durch Enumeration zu identifizieren.
Timing-Attack und sicherer Vergleich
Der Vergleich von zwei Hashes mit === bricht beim ersten unterschiedlichen Byte ab. Die benötigte Zeit hängt daher davon ab, wie viele Teile der Vermutung korrekt waren. Ein geduldiger Angreifer kann dies messen, um einen Wert zu rekonstruieren. Es ist zwar ein schmaler Kanal, aber gerade beim Passwortvergleich ist dies entscheidend.
Nutzen Sie die Verifizierungsfunktion der Library, die den Vergleich in konstanter Zeit (constant time) durchführt. Vergleichen Sie Digests niemals selbst und vergleichen Sie niemals das Passwort im Klartext.
Hashes im Laufe der Zeit aktualisieren
Ihre Anforderungen werden sich ändern: Die Rechenkosten steigen oder Sie migrieren von bcrypt zu Argon2. Da Sie den Klartext niemals gespeichert haben, können Sie das Re-Hashing nur beim Login durchführen, wenn das Passwort kurzzeitig im Speicher liegt.
Speichern Sie den Algorithmus und die Parameter zusammen mit jedem Digest – der kodierten Zeichenfolge gelingt dies bereits – und prüfen Sie diese bei jedem erfolgreichen Login:
if (await verify(stored, password)) {
if (needsRehash(stored)) {
await db.users.update(user.id, { passwordHash: await hash(password) });
}
// continue the login
}
Benutzer, die sich nie wieder anmelden, behalten ihren alten Hash, was völlig in Ordnung ist; sie sind dadurch immer noch geschützt. Im Laufe der Zeit migrieren die aktiven Konten ganz natürlich.
Passwortrichtlinien in der Praxis
Der Algorithmus schützt Sie nach einem Datenleck. Eine gute Richtlinie verringert die Wahrscheinlichkeit, dass eines überhaupt passiert.
- Bevorzugen Sie Länge gegenüber Komplexität. Eine Passphrase schlägt
P@ssw0rd!. - Prüfen Sie neue Passwörter gegen Listen bekannter Leaks, wie zum Beispiel die k-anonymity API von Have I Been Pwned.
- Erzwingen Sie keine willkürlichen Rotationen. Ein erzwungenes Ablaufdatum führt zu
Summer2026!Mustern. - Implementieren Sie Rate-Limiting für Login- und Reset-Versuche und fügen Sie ein exponentielles Backoff oder einen Lockout hinzu.
- Nutzen Sie einen generischen Reset-Flow mit einmaligen, auslaufenden Tokens und versenden Sie Passwörter niemals per E-Mail.
Best Practices
- Verwenden Sie Argon2id, scrypt oder bcrypt zum Hashen — niemals einen schnellen Allzweck-Hash.
- Überlassen Sie der Library das Generieren und Speichern des Salts.
- Optimieren Sie die Cost-Parameter auf 100–500 ms und erhöhen Sie diese im Laufe der Zeit.
- Verifizieren Sie Passwörter mit der Constant-Time-Funktion der Library.
- Führen Sie bei einem erfolgreichen Login ein Rehash durch, wenn sich die Parameter geändert haben.
- Geben Sie generische Fehlermeldungen zurück und implementieren Sie ein Rate-Limit für Versuche.
- Führen Sie das Hashen ausschließlich auf dem Server aus; niemals im Client.
Häufige Fehler
- Passwörter zu verschlüsseln oder sie “vorübergehend” im Klartext zu speichern.
- SHA-256 oder MD5 mit nur wenigen Durchläufen zu verwenden und dies als Hashing zu bezeichnen.
- Ein einziger globaler Salt oder die Wiederverwendung desselben Salts über mehrere Accounts hinweg.
- Das Vergleichen von Hashes mit
==oder===. - Passwörter in Logs zu schreiben oder sie in Fehlerberichten mitzuführen.
- Die maximale Passwortlänge zu niedrig anzusetzen, was die Verwendung von Passphrasen erschwert.
- Zu vergessen, den Cost Factor über Jahre hinweg zu aktualisieren.
Wie geht es weiter?
Anmeldedaten sind nur die erste Hürde. Sobald ein Benutzer verifiziert ist, benötigen Sie einen Ort, an dem dieser Zustand gespeichert wird: Lesen Sie den Guide zu Session Auth für cookie-basierte Sessions oder den JWT-Guide für zustandslose Tokens. Wenn Sie Passwörter lieber gar nicht selbst verwalten möchten, delegieren OAuth 2.0 und OpenID Connect den Login an einen Identity Provider.