Secure Coding: Sicherere Passwörter mit Salt, Pepper und Hashing
Seite 3: Eine beispielhafte Implementierung mittels Core Java
Die folgende Beispielimplementierung zum Generieren eines Salt-Werts in Java beschreibt die Umsetzung mit der Klasse SecureRandom. Tiefergehende Details zu den verschiedenen Aspekten von JCA und JCE sind Thema eines weiteren Artikels. Die zuvor angesprochenen Elemente werden nun schrittweise mit den JDK-eigenen Mitteln implementiert – und auf Drittbibliotheken vorerst verzichtet.
Verwenden der SecureRandom-Klasse
Der Einsatz der Klasse SecureRandom ist die empfohlene Methode, um kryptografisch sichere Zufallswerte zu erzeugen, die als Salt für Hashing-Operationen verwendet werden können. Auf das Verwenden von Random sollte explizit verzichtet werden, da diese Klasse wegen der Anzahl Kollisionen und der Vorhersehbarkeit der nächsten Zufallswerte als nicht sicher genug gilt.
Dies lässt sich anhand eines einfachen Beispiels verdeutlichen, das die Vorhersehbarkeit und Kollisionen bei geringer Bit-Länge aufzeigt. Hierbei handelt es sich zwar nicht um eine belastbare Analyse, die Vorgehensweise zeigt aber dennoch auf, wo die Probleme liegen.
Reproduzierbarkeit bei Random:
public static void main(String[] args) {
int seed = 12345; // Fester Seed fĂĽr deterministische Ausgabe
byte[] seedBytes = {1, 2, 3, 4, 5};
int bound = 100; // Obergrenze fĂĽr Zufallszahlen
int count = 5; // Anzahl der zu generierenden Werte
Random random1 = new Random(seed);
Random random2 = new Random(seed);
SecureRandom secureRandom1 = new SecureRandom(seedBytes);
SecureRandom secureRandom2 = new SecureRandom(seedBytes);
System.out.printf("%-15s %-15s %-15s %-15s%n",
"Random (1)", "Random (2)",
"SecureRandom (1)", "SecureRandom (2)");
System.out.println("-----------------------");
for (int i = 0; i < count; i++) {
int rand1 = random1.nextInt(bound);
int rand2 = random2.nextInt(bound);
int secRand1 = secureRandom1.nextInt(bound);
int secRand2 = secureRandom2.nextInt(bound);
System.out.printf("%-15d %-15d %-15d %-15d%n",
rand1, rand2, secRand1, secRand2);
}
}
Das Programm demonstriert die Unterschiede zwischen den Klassen Random und SecureRandom in Java, insbesondere in Bezug auf die Deterministik und die Initialisierung mit einem Seed.
Zu Beginn werden ein fester numerischer Seed (12345) sowie ein Byte-Array ({1, 2, 3, 4, 5}) fĂĽr die Seeding-Methoden von Random und SecureRandom definiert. AnschlieĂźend wird eine Obergrenze fĂĽr die Zufallszahlen (bound = 100) und die Anzahl der zu generierenden Werte (count = 5) festgelegt.
Daraufhin werden zwei Instanzen der Random-Klasse mit demselben Seed erstellt (random1 und random2). Da Random mit demselben Startwert (seed) deterministisch arbeitet, erzeugen beide Instanzen exakt die gleiche Abfolge von Zufallszahlen.
FĂĽr SecureRandom werden ebenfalls zwei Instanzen erstellt (secureRandom1 und secureRandom2). Diese erhalten ein Byte-Array als Seed. Anders als Random ist SecureRandom jedoch darauf ausgelegt, eine kryptografisch sichere Zufallsquelle zu nutzen. In der Regel ist SecureRandom daher weniger vorhersehbar, auch wenn hier ein explizites Seed-Array verwendet wird.
Die Ausgabe der Werte erfolgt tabellarisch, wobei jede Zeile eine Reihe von Zufallszahlen aus den jeweiligen Generatoren zeigt. Die Spalten eins und zwei enthalten die Zahlenfolgen von random1 und random2, die identisch sein sollten, da beide mit demselben Seed initialisiert wurden. Die Spalten drei und vier enthalten die Zahlen aus secureRandom1 und secureRandom2, die tendenziell unterschiedlich sind, da SecureRandom auch mit identischem Seed oft nicht deterministisch arbeitet.
Das Demo-Programm zeigt, dass Random vorhersehbare und reproduzierbare Ergebnisse liefert, wenn der gleiche Seed verwendet wird, während SecureRandom, selbst mit einem gemeinsamen Seed, eine weniger vorhersehbare Verteilung erzeugen kann.
Beispielhafte Ausgabe des Programms:
| Random (1) | Random (2) | SecureRandom (1) | SecureRandom (2) |
| 51 | 51 | 36 | 83 |
| 80 | 80 | 48 | 88 |
| 41 | 41 | 99 | 56 |
| 28 | 28 | 19 | 81 |
| 55 | 55 | 32 | 81 |
Kollisionen bei der Salt-Wert-Erzeugung
Die Kollisionswahrscheinlichkeit beim Erzeugen von Salt-Werten mit SecureRandom hängt von der Anzahl der generierten Werte und der Bit-Länge des Werts ab. Da Salt-Werte zufällig generiert werden, kann es theoretisch vorkommen, dass zwei identische erzeugt werden. Die Wahrscheinlichkeit dafür lässt sich jedoch mit dem Geburtstagsparadoxon abschätzen.
Das Geburtstagsparadoxon beschreibt das Phänomen, dass in einer zufällig ausgewählten Gruppe von Personen die Wahrscheinlichkeit, dass mindestens zwei Personen denselben Geburtstag haben, überraschend hoch ist. Obwohl es 365 mögliche Geburtstage gibt, genügt bereits eine Gruppe von 23 Personen, um mit über 50 Prozent Wahrscheinlichkeit eine Übereinstimmung zu finden. Der Grund dafür liegt in der quadratischen Natur der möglichen Vergleiche: Jeder neu hinzugefügte Wert kann mit allen vorherigen Werten eine Kollision verursachen.
Dieses Konzept lässt sich auch auf die Generierung von Salt-Werten anwenden. Wenn ein Salt beispielsweise 16 Byte (also 128 Bit) lang ist, gibt es 2^128 mögliche Kombinationen. Die Wahrscheinlichkeit, dass in einer Menge von zufällig generierten Salt-Werten eine Kollision auftritt, lässt sich durch die Geburtstagsformel approximieren. Setzt man darin für n eine große Zahl (etwa eine Milliarde, also 10^9) ein, ist die Kollisionswahrscheinlichkeit dennoch extrem gering. Erst bei einer Anzahl ab etwa 2^64 generierten Werten wird die Wahrscheinlichkeit einer Kollision signifikant.
In der Praxis gilt daher, dass bei ausreichend langen Salt-Werten (mindestens 128 Bit) die Kollisionswahrscheinlichkeit so gering ist, dass sie sich vernachlässigen lässt. Bei angemessener Salt-Länge schützt SecureRandom somit sicher gegen Kollisionen.
Der Salt-Wert-Generator Version 1:
public class SaltGeneratorV1 {
public static final int DEFAULT_SALT_LENGTH = 16;
private static final Random secureRandom = new SecureRandom();
public String generateSalt(int length) {
byte[] salt = new byte[length];
secureRandom.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
public String generateSalt(boolean secure) {
return generateSalt(DEFAULT_SALT_LENGTH);
}
}
public class SaltGeneratorV1Comparison {
protected static final int MAX_ROUND = 1;
protected static final int MAX_VALUES = 100_000_000; // Anzahl der zu generierenden Werte
protected static final int MAX_BYTE_COUNT = 10;
public static void main(String[] args) {
SaltGeneratorV1 saltGenerator = new SaltGeneratorV1();
Set<String> secureRandomSet = new HashSet<>();
int secureRandomCollisions = 0;
System.out.printf("%-20s %-20s%n",
"byte count",
"SecureRandom Collisions");
System.out.println("----------------------" +
"----------------------------" +
"----------------");
for (int byteCount = 0; byteCount < MAX_BYTE_COUNT; byteCount++) {
for (int round = 0; round < MAX_ROUND; round++) {
for (int valueCount = 0; valueCount < MAX_VALUES; valueCount++) {
String generatedSaltSec = saltGenerator.generateSalt(byteCount);
if (secureRandomSet.contains(generatedSaltSec)) secureRandomCollisions++;
else secureRandomSet.add(generatedSaltSec);
}
System.out.printf("%-20d %-20d%n",
byteCount,
secureRandomCollisions);
secureRandomSet.clear();
secureRandomCollisions = 0;
}
}
}
}
Das Beispiel zeigt, wie sich die Häufigkeit der Kollisionen im Verhältnis zur Salt-Wert-Länge in Bytes verhält. Hierbei werden für eine Byte-Länge (im Beispiel von 1-10) Salt-Werte generiert. Pro Durchlauf werden 100.000.000 Werte erzeugt und dann geprüft, wie viele Kollisionen in dem Bereich vorgekommen sind. Bei Bedarf kann man jeden Durchlauf n-mal wiederholen lassen. Die folgende Ausgabe zeigt das Ergebnis auf dem Rechner des Autors. Sie spiegelt die Tendenz wider, dass mit zunehmender Länge des Salt-Werts die Kollisionen abnehmen. (Auch hier gilt: Es handelt sich nicht um eine umfassende Analyse, sondern dient lediglich der Demonstration des allgemeinen Verhaltens).
| Byte Count | SecureRandom Collisions |
| 0 | 99999999 |
| 1 | 99999744 |
| 2 | 99934464 |
| 3 | 83265831 |
| 4 | 1157397 |
| 5 | 4604 |
| 6 | 14 |
| 7 | 0 |
| 8 | 0 |
| 9 | 0 |
Die Initialisierung von SecureRandom
FĂĽr die Implementierung von SecureRandom stehen verschiedene Algorithmen parat, mit denen sich die Zufallszahlengeneratoren betreiben lassen.
Der Deterministic Random Bit Generator (DRBG) basiert auf dem NIST-Standard SP 800-90A und gehört zu den modernsten und wohl auch sichersten Methoden zur Erzeugung kryptografisch sicherer Zufallszahlen. DRBG nutzt deterministische Algorithmen mit einer starken Ausgangsentropiequelle, um eine zuverlässige und vorhersagbare Ausgabe zu garantieren. Falls die Implementierung ordnungsgemäß konfiguriert ist und eine sichere Initialisierung erfolgt, bietet dieser Generator eine hohe Sicherheit. Allerdings hängt die tatsächliche Sicherheit stark von der verwendeten Entropiequelle ab. Falls die Quelle nicht ausreichend zufällige Bits liefert oder kompromittiert ist, kann dies die Sicherheit erheblich beeinträchtigen.
Ein ebenfalls sicherer, aber performanzkritischer Generator ist NativePRNGBlocking. Dieser Mechanismus arbeitet direkt mit der Entropiequelle des Betriebssystems und stellt sicher, dass wirklich zufällige Bits gesammelt werden, indem er so lange blockiert, bis genügend Entropie verfügbar ist. Dadurch ist dieser Pseudo Random Number Generator (PRNG) besonders geeignet für kryptografische Operationen, die eine hohe Unvorhersehbarkeit erfordern, wie etwa beim Generieren von asymmetrischen Schlüsselpaaren oder Sitzungsschlüsseln für sichere Kommunikation. Allerdings kann dies zu Verzögerungen führen, insbesondere in Umgebungen mit geringer Entropieproduktion, beispielsweise in Virtual Machines oder eingebetteten Systemen ohne ausreichende Hardwarequellen für zufällige Daten.
NativePRNG stellt eine gute Alternative dar, da er ebenfalls auf die Betriebssystem-Entropiequelle zurückgreift, allerdings nicht zwingend blockiert. Dies bedeutet, dass er in den meisten Fällen Zufallszahlen von hoher Qualität liefert, aber in Situationen, in denen die Entropie erschöpft ist, möglicherweise nicht die gleiche Sicherheit wie die blockierende Variante bietet. Dennoch ist dieser Generator in vielen Anwendungen eine bessere Wahl als ältere PRNGs, da er moderne Entropiequellen nutzt und nicht von schwachen algorithmischen Verfahren abhängig ist.
Die nicht blockierende Variante, NativePRNGNonBlocking, ist hingegen etwas weniger sicher, da sie keine Wartezeit einführt und auch dann Zufallszahlen liefert, wenn nur begrenzte Entropie vorhanden ist. Dies kann in Szenarien problematisch sein, in denen hochgradig zufällige Werte benötigt werden, beispielsweise für private Schlüssel oder einmalige Initialisierungsvektoren (IVs) für symmetrische Verschlüsselung. Wenn nicht sichergestellt ist, dass das System über eine ausreichend starke Entropiequelle verfügt, besteht die Gefahr, dass vorhersehbare Zufallszahlen generiert werden, was Angreifern unter Umständen die Rekonstruktion kryptografischer Schlüssel erleichtert.
Schließlich gibt es noch SHA1PRNG, der als älterer PRNG-Algorithmus auf dem kryptografisch als unsicher geltenden SHA-1 basiert. Dieser Pseudozufallszahlengenerator wurde ursprünglich als softwarebasierte Alternative für Umgebungen eingeführt, in denen keine nativen Entropiequellen zur Verfügung stehen. Da jedoch SHA-1 als kollisionsanfällig und nicht mehr vertrauenswürdig gilt, ist auch dieser PRNG keine empfehlenswerte Wahl für sicherheitskritische Anwendungen. Moderne Angriffe können die Zufallszahlen möglicherweise vorhersagen oder zurückrechnen, was insbesondere für kryptografische Schlüssel oder Einmaltoken problematisch ist.
Als die sichersten Optionen empfehlen sich daher DRBG und NativePRNGBlocking, da sie entweder auf bewährte deterministische Verfahren mit starker Entropiequelle oder auf blockierende Betriebssystemmechanismen setzen. NativePRNG ist als solide Mittelklasselösung einzustufen, während NativePRNGNonBlocking weniger sicher sein kann, wenn nicht genügend Entropie vorhanden ist. SHA1PRNG sollte hingegen aufgrund der bekannten Schwächen von SHA-1 nicht mehr zum Einsatz kommen.
Der Salt-Wert-Generator Version 2
Die zweite Version des Salt-Wert-Generators prĂĽft, ob bereits eine bestimmte Implementierung (Algorithmus) auf dem aktiven System vorliegt. Ist das der Fall, wird eine Instanz von SecureRandom erzeugt und zum nachfolgenden Generieren der Salt-Werte verwendet.
Ein explizites Vorgehen bei der Auswahl des eingesetzten Algorithmus ist aus der Sicherheitsperspektive vorzuziehen. In manchen Umgebungen sind bestimmte Algorithmen nicht erlaubt, sodass man von einer indirekten Auswahl durch Fallbacks im System absehen sollte. Auch hier gibt es ebenfalls wieder für jeden Algorithmus verschiedene Parameter, die das Verhalten in Bezug auf Zufälligkeit, Seedverwendung und produzierte Systemlast beeinflussen und daher individuell für die jeweiligen Bedingungen gewählt werden sollten.
public class SaltGeneratorV2 {
private static final String DRBG = "DRBG";
private static final String NATIVE_PRNG_BLOCKING = "NativePRNGBlocking";
private static SecureRandom secureRandom;
static {
boolean drgb = false;
boolean nativePrng = false;
for (Provider provider : Security.getProviders()) {
for (Provider.Service service : provider.getServices()) {
if ("SecureRandom".equals(service.getType())) {
String algorithm = service.getAlgorithm();
if (algorithm.equals(DRBG)) drgb = true;
if (algorithm.equals(NATIVE_PRNG_BLOCKING)) nativePrng = true;
try {
if (drgb) initDRGB();
if (nativePrng && !drgb) initPrngBlocking();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
}
}
private static void initPrngBlocking() throws NoSuchAlgorithmException {
if (secureRandom == null) {
synchronized (SaltGeneratorV2.class) {
if (secureRandom == null) {
secureRandom = SecureRandom.getInstance(NATIVE_PRNG_BLOCKING);
}
}
}
}
private static void initDRGB() throws NoSuchAlgorithmException {
if (secureRandom == null) {
synchronized (SaltGeneratorV2.class) {
if (secureRandom == null) {
secureRandom = SecureRandom.getInstance(DRBG, instantiation(
256, // Security Strength (128, 192, 256 Bit)
DrbgParameters.Capability.PR_AND_RESEED, // Reseed erlaubt
"CustomPersonalizationString".getBytes() // Optionaler Personalization String
));
}
}
}
}
public byte[] generateSalt(int length) throws NoSuchAlgorithmException {
byte[] salt = new byte[length];
secureRandom.nextBytes(salt);
return salt;
}
}
Fazit: Richtig Salzen und Pfeffern fĂĽr mehr Sicherheit
Der gezielte Einsatz von Salt- und Pepper-Werten beim Hashing von Passwörtern trägt unter Beachtung der vorgestellten Best Practices entscheidend zu mehr Sicherheit und wirkungsvollerem Schutz gegen Angriffe bei. Beim Verwenden von SecureRandom sollte der zum Einsatz kommende Algorithmus passend zu den individuellen Anforderungen der Betriebsumgebung gewählt werden. Die vorgestellte Beispielimplementierung liefert einen ersten Ansatz für eigene Umsetzungen, wenn man nicht auf schon bestehende Implementierungen zurückgreifen kann oder möchte.
Happy Coding
Sven
(map)