Przejdź do treści

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.

JAVA HasherHasel.java 98 linii Pobierz .java
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

  1. Zapisz klasę w pliku HasherHasel.java — nazwa pliku to nazwa klasy publicznej — i uruchom poleceniem java HasherHasel.java. Zobaczysz rekord gotowy do zapisania w bazie oraz wyniki weryfikacji poprawnego, błędnego i uszkodzonego wejścia.
  2. Podczas rejestracji zamień hasło na char[], wywołaj HasherHasel.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.
  3. 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żej MIN_ITERACJI (dawne systemy używały nawet 10 000) dostanie false nie do odróżnienia od złego hasła — jeśli przenosisz takie skróty, najpierw obniż tę stałą.
  4. 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ą.
#bezpieczeństwo #hasła #kryptografia #securerandom #base64

Podobne skrypty