Hashowanie i weryfikacja haseł przez PBKDF2
- Przydatne narzędzia
- poziom: zaawansowany
- aktualizacja: 16 sierpnia 2026
Załóż z góry, że kopia Twojej bazy kiedyś wypłynie — i zapisuj hasła tak, żeby ten wyciek nic nie dał. Jawny tekst odpada, szyfrowanie też: klucz wykradnie się razem z danymi. Zostaje celowo wolna funkcja wyprowadzania klucza. Ten przykład używa dostępnego w JDK algorytmu PBKDF2WithHmacSHA256, losuje osobną sól dla każdego hasła i trzyma w jednej kolumnie identyfikator algorytmu, liczbę iteracji, sól oraz wynik. Przy logowaniu odtwarza parametry prosto z rekordu i porównuje bajty przez MessageDigest.isEqual, czyli w stałym czasie — bez podpowiadania atakującemu, na którym bajcie się rozjechało.
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
/**
* Hashowanie i weryfikacja haseł algorytmem PBKDF2 — skryptyJAVA.pl
* Zapisz jako: HasherHasel.java
*/
public class HasherHasel {
private static final String ALGORYTM = "PBKDF2WithHmacSHA256";
private static final String IDENTYFIKATOR = "pbkdf2_sha256";
private static final int ITERACJE = 600_000;
private static final int MIN_ITERACJI = 100_000;
private static final int MAX_ITERACJI = 2_000_000;
private static final int DLUGOSC_BITOW = 256;
private static final int BAJTY_SOLI = 16;
private static final SecureRandom LOSOWE = new SecureRandom();
private static final Base64.Encoder KODER = Base64.getUrlEncoder().withoutPadding();
private static final Base64.Decoder DEKODER = Base64.getUrlDecoder();
/**
* Tworzy rekord: algorytm$iteracje$sól$skrót. Sól i skrót są zapisane jako Base64 URL-safe.
* Hasło przekazujemy jako char[], aby kod wywołujący mógł później wyzerować tablicę.
*/
public static String hashuj(char[] haslo) throws GeneralSecurityException {
if (haslo == null || haslo.length == 0) {
throw new IllegalArgumentException("Hasło nie może być puste.");
}
byte[] sol = new byte[BAJTY_SOLI];
LOSOWE.nextBytes(sol);
byte[] skrot = wyprowadzKlucz(haslo, sol, ITERACJE, DLUGOSC_BITOW);
try {
return IDENTYFIKATOR + "$" + ITERACJE + "$"
+ KODER.encodeToString(sol) + "$" + KODER.encodeToString(skrot);
} finally {
Arrays.fill(skrot, (byte) 0);
}
}
/** Zwraca false również dla uszkodzonego lub nieobsługiwanego rekordu z bazy. */
public static boolean weryfikuj(char[] haslo, String zapis) {
if (haslo == null || zapis == null) { return false; }
try {
String[] czesci = zapis.split("\\$", -1);
if (czesci.length != 4 || !czesci[0].equals(IDENTYFIKATOR)) { return false; }
int iteracje = Integer.parseInt(czesci[1]);
if (iteracje < MIN_ITERACJI || iteracje > MAX_ITERACJI) { return false; }
byte[] sol = DEKODER.decode(czesci[2]);
byte[] oczekiwany = DEKODER.decode(czesci[3]);
if (sol.length < 16 || sol.length > 64
|| oczekiwany.length < 16 || oczekiwany.length > 64) { return false; }
byte[] obliczony = wyprowadzKlucz(haslo, sol, iteracje, oczekiwany.length * 8);
try {
// Porównanie nie kończy się na pierwszym różnym bajcie, co ogranicza wyciek czasowy.
return MessageDigest.isEqual(oczekiwany, obliczony);
} finally {
Arrays.fill(obliczony, (byte) 0);
}
} catch (GeneralSecurityException | IllegalArgumentException blad) {
return false;
}
}
/** Uruchamia PBKDF2 i czyści wewnętrzną kopię hasła przechowywaną przez PBEKeySpec. */
private static byte[] wyprowadzKlucz(char[] haslo, byte[] sol, int iteracje, int bity)
throws GeneralSecurityException {
PBEKeySpec parametry = new PBEKeySpec(haslo, sol, iteracje, bity);
try {
return SecretKeyFactory.getInstance(ALGORYTM).generateSecret(parametry).getEncoded();
} finally {
parametry.clearPassword();
}
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) throws GeneralSecurityException {
char[] dobreHaslo = "Kawa-i-Java-2026!".toCharArray();
char[] zleHaslo = "Kawa-i-Java-2025!".toCharArray();
try {
String zapis = hashuj(dobreHaslo);
System.out.println("Rekord do bazy: " + zapis);
System.out.println("Poprawne hasło: " + weryfikuj(dobreHaslo, zapis));
System.out.println("Błędne hasło: " + weryfikuj(zleHaslo, zapis));
System.out.println("Uszkodzony zapis: " + weryfikuj(dobreHaslo, "niepoprawny"));
} finally {
// Kod wywołujący odpowiada za wyzerowanie własnych tablic z hasłami.
Arrays.fill(dobreHaslo, '\0');
Arrays.fill(zleHaslo, '\0');
}
}
}
Jak użyć tego skryptu
- Zapisz klasę w pliku
HasherHasel.java— nazwa pliku to nazwa klasy publicznej — i uruchom poleceniemjava HasherHasel.java. Zobaczysz rekord gotowy do zapisania w bazie oraz wyniki weryfikacji poprawnego, błędnego i uszkodzonego wejścia. - Podczas rejestracji zamień hasło na
char[], wywołajHasherHasel.hashuj(haslo)i zapisz zwrócony tekst w jednej kolumnie. Sól nie jest sekretem — musi znajdować się przy skrócie, aby późniejsza weryfikacja była możliwa. - Podczas logowania pobierz cały rekord użytkownika i przekaż go wraz z wpisanym hasłem do
weryfikuj(...). Metoda użyje liczby iteracji zapisanej przy starym skrócie, więc zwiększenie stałej dla nowych kont nie zablokuje istniejących użytkowników. Uważaj przy migracji z innego systemu: rekord z liczbą iteracji poniżejMIN_ITERACJI(dawne systemy używały nawet 10 000) dostaniefalsenie do odróżnienia od złego hasła — jeśli przenosisz takie skróty, najpierw obniż tę stałą. - Zmierz czas PBKDF2 na serwerze produkcyjnym i dobierz koszt, który jest odczuwalny dla atakującego, lecz akceptowalny dla logowania. Po udanym logowaniu możesz przeliczyć starszy rekord z większą liczbą iteracji i zastąpić go w bazie.
Warto wiedzieć
- PBKDF2 jest funkcją jednokierunkową: nie ma czegoś takiego jak „odszyfruj hasło”. Przy odzyskiwaniu konta wygeneruj krótkotrwały token jednorazowy i pozwól ustawić nową wartość. Administrator też nie powinien móc odczytać starego hasła — jeśli może, to znaczy, że gdzieś leży ono zbyt czytelnie.
- Każdy rekord musi mieć własną, kryptograficznie losową sól. Bez niej jedna gotowa tablica skrótów rozbraja wszystkie konta naraz; z nią atakujący musi liczyć od zera dla każdego użytkownika osobno. Sól nie jest sekretem i może spokojnie leżeć w tej samej kolumnie co wynik PBKDF2.
- Liczbę iteracji podnoś wraz z szybkością sprzętu. Weryfikacja odrzuca rekordy spoza przedziału 100 tysięcy – 2 miliony: górna granica chroni przed spreparowanym wpisem, który zająłby serwer na kilka sekund, dolna przed skrótem policzonym niemal za darmo. Pamiętaj przy tym, że PBKDF2 nie jest memory-hard, więc karta graficzna liczy go tanio — jeśli możesz dołożyć zależność, OWASP stawia na pierwszym miejscu Argon2id, dalej scrypt i bcrypt. PBKDF2 wybieraj tam, gdzie bibliotek dołożyć się nie da albo obowiązuje FIPS.
- Zanim w ogóle uruchomisz PBKDF2, ogranicz liczbę prób logowania. 600 tysięcy iteracji to blisko sekunda pracy rdzenia, więc niezalogowany napastnik wysyci Ci serwer jednym żądaniem na sekundę — rate limiting i blokada po serii nieudanych prób to tu część zabezpieczenia, nie dodatek. Nie wypisuj też rekordu ani hasła do logów i nie trzymaj wejścia w
String, którego nie da się wyzerować;char[]wyczyścisz, ale kopie zrobione wcześniej przez framework formularzy zostają.
Podobne skrypty
Generator bezpiecznych haseł
Generator haseł oparty na klasie SecureRandom: wybór długości i zestawów znaków, gwarancja użycia każdego zestawu i opcja pomijania mylących znaków (l, I, 1, O, 0).
początkujący Zobacz kod → Przydatne narzędziaSzyfrowanie i odszyfrowanie przez AES-GCM
Uwierzytelnione szyfrowanie tekstu algorytmem AES-GCM: klucz 256-bitowy, losowy 96-bitowy IV, dane powiązane AAD i przenośny zapis Base64. Ktoś przestawi jeden bit szyfrogramu — odszyfrowanie się nie uda, zamiast po cichu zwrócić przekłamaną treść.
zaawansowany Zobacz kod → Przydatne narzędziaOdczyt argumentów wiersza poleceń
Parser tablicy args: opcje --nazwa=wartosc i --nazwa wartosc, łączone flagi -vf, separator --, argumenty pozycyjne, wartości domyślne oraz czytelny błąd, gdy brakuje opcji obowiązkowej.
średni Zobacz kod →