bcrypt vs Argon2 vs scrypt: hashowanie haseł w 2026
Krótka odpowiedź: w nowym projekcie w 2026 roku użyj Argon2id z parametrami m=19456, t=2, p=1. To bazowa rekomendacja OWASP Password Storage Cheat Sheet, a zarazem algorytm hashowania haseł o najlepszej dziś dostępnej odporności na GPU i kanały boczne.
Jeżeli Argon2 nie jest dostępny w stosie technologicznym (rzadko, ale zdarza się to w niektórych środowiskach embedded lub starszych runtime’ach), wybierz scrypt z N=2^17, r=8, p=1. Bcrypt z cost=12 ma sens tylko wtedy, gdy siedzisz w starym systemie, który już mówi po bcrypt, i nie możesz dorzucić nowej zależności. Zostań przy PBKDF2-HMAC-SHA-256 z 600 000 iteracji, gdy zgodność z FIPS-140 jest obowiązkowa.
| Algorytm | Parametry OWASP 2026 | Kiedy wybrać |
|---|---|---|
| Argon2id | m=19456 KiB, t=2, p=1 | Domyślnie dla nowych projektów |
| scrypt | N=2^17, r=8, p=1 | Argon2 niedostępny |
| bcrypt | cost=12 (min. 10) | Tylko systemy legacy |
| PBKDF2 | HMAC-SHA-256, 600k iteracji | Wymagane FIPS-140 |
Reszta artykułu wyjaśnia, dlaczego akurat te liczby, jak je dostroić do własnego sprzętu i jak migrować bez wymuszania resetu haseł. Potrzebujesz wygenerować silne hasła testowe do benchmarku? Skorzystaj z generatora haseł. Po szerszy kontekst sięgnij do przewodnika po bezpieczeństwie aplikacji webowych.
Dlaczego hashowanie haseł różni się od ogólnego hashowania
Z zewnątrz funkcje hashujące wyglądają tak samo: na wejściu dane, na wyjściu skrót o stałej długości, którego nie da się odwrócić. Ale cele projektowe „policz hash 4 GB obrazu ISO” i „policz hash 12-znakowego hasła” są dokładnie przeciwstawne. Pierwszy ma być tak szybki, jak pozwala krzem. Drugi ma być tak wolny, jak tolerancja budżetu czasu logowania na to pozwala.
Pomylenie tych dwóch celów to dokładnie ten moment, w którym wyciek bazy zamienia się w przejęcie kont.
Dlaczego MD5 i SHA-256 nie wystarczą do haseł
Funkcje ogólnego przeznaczenia, takie jak MD5, SHA-1 i SHA-256, projektowano pod przepustowość. Na zwykłym CPU przerabiają gigabajty na sekundę, na GPU dziesiątki gigabajtów na sekundę. Do sum kontrolnych i adresowania treści są w sam raz. Do haseł są katastrofalne.
Benchmarki Hashcata na pojedynczym RTX 4090 pokazują w 2024 roku w przybliżeniu 164 GH/s dla MD5 i 22 GH/s dla SHA-256. Ośmioznakowe hasło z małych liter i cyfr (36^8 ≈ 2,8 × 10^12 kandydatów) pada przed pojedynczym GPU w mniej niż minutę przeciwko MD5 i poniżej dwóch minut przeciwko SHA-256. Wyciekła baza, która trzyma sha256(password), to praktycznie tekst jawny.
Salt też tu nie pomoże. Salt blokuje wstępnie obliczone tablice tęczowe, ale nie spowalnia ataku na pojedyncze konto: atakujący po prostu liczy hash z każdego kandydata sklejonego z wyciekniętym saltem.
W zastosowaniach niezwiązanych z bezpieczeństwem MD5 i SHA-256 wciąż są przydatne, od tego są narzędzia takie jak generator hashy ogólnego przeznaczenia. Pogłębione porównanie tego, kiedy który algorytm pasuje, znajdziesz w porównaniu algorytmów MD5 vs SHA-256. Ale do haseł potrzebny jest hash celowo wolny.
Trzy własności nowoczesnego hasha hasła
Hash hasła wart wdrożenia w 2026 roku ma trzy własności:
- Celowo wolny, z konfigurowalnym współczynnikiem pracy. Logowanie powinno zająć 100–500 ms — wystarczająco szybko, żeby użytkownik nie zauważył, wystarczająco wolno, żeby atakujący offline marnował dni na milion prób. Współczynnik pracy musi być parametrem, by można go było podkręcać wraz z postępem sprzętu.
- Salt na rekord. Unikalny losowy salt na każde hasło rozbija tablice tęczowe i zmusza atakującego do osobnej walki z każdym kontem. Nowoczesne algorytmy generują salt i wbudowują go w wynikowy ciąg za ciebie.
- Memory-hard. GPU i ASIC są szybkie w obliczeniach, ale drogie w pamięci o wysokiej przepustowości. Algorytm, który wymaga dziesiątek MiB na hash, wymusza na atakującym alokację RAM proporcjonalną do równoległości — a to zabija ekonomię farm GPU.
Bcrypt ogarnia (1) i (2), ale nie (3). Scrypt jako pierwszy trafił w trójkę. Argon2 dopracował projekt i wygrał Password Hashing Competition. W kolejnej sekcji rozpakowuję każdy z nich.
Trzy algorytmy: architektura i kompromisy
bcrypt: oparty na Blowfish, time-hard
Bcrypt zaprojektowali w 1999 roku Niels Provos i David Mazières dla OpenBSD. Stoi na szyfrze Blowfish, z kosztowną fazą key-setup („EksBlowfish”) powtarzaną 2^cost razy. Jedynym strojonym parametrem jest cost factor (zwany też „log rounds”): każdy przyrost o 1 podwaja pracę. Hash z cost=10 wykonuje 1024 key schedule, a z cost=14 — 16 384.
Hash bcrypt wygląda tak:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
│ │ │ │
│ │ │ └─ 31-znakowy base64 hash
│ │ └─ 22-znakowy base64 salt
│ └─ cost factor (12)
└─ identyfikator algorytmu ($2b$ = bcrypt v2)
Format jest samoopisowy: verify() odczytuje cost i salt ze składowanego ciągu, osobne kolumny nie są potrzebne.
Wady są realne. Pamięciowy ślad bcrypt to mniej więcej 4 KiB — na tyle mało, że topowe GPU może równolegle puścić tysiące rdzeni bcrypt. Do tego bcrypt po cichu obcina wejście do 72 bajtów. Stuznakowe passphrase ma takie samo bezpieczeństwo jak jego pierwsze 72 bajty. Maksymalny cost to 31, ale wszystko powyżej ~16 zaczyna boleć w opóźnieniu logowania na typowym sprzęcie.
scrypt: pionier memory-hardness
Scrypt opublikował w 2009 roku Colin Percival na potrzeby usługi backupów Tarsnap, a w 2016 roku ustandaryzowano go jako RFC 7914. Wprowadził pojęcie memory-hardness: algorytm wypełnia duży bufor pseudolosowymi danymi, a potem czyta z losowych pozycji, przez co każda implementacja musi realnie zaalokować pamięć.
Scrypt przyjmuje trzy parametry:
- N — koszt CPU/pamięci (musi być potęgą dwójki)
- r — rozmiar bloku w bajtach (mnożnik pamięci i rund mieszania)
- p — równoległość (niezależne obliczenia, używane głównie do skalowania czasu CPU bez skalowania pamięci)
Zużycie pamięci wynosi w przybliżeniu 128 × N × r bajtów. Przy zalecanych przez OWASP N=2^17, r=8 daje to 128 × 131072 × 8 = 134 217 728 bajtów, czyli dokładnie 128 MiB na hash.
Scrypt to też funkcja wyprowadzania kluczy, nie tylko hash hasła. Stosuje się go w portfelach kryptowalut, szyfrowaniu pełnodyskowym i w oryginalnym proof-of-work Litecoina. Ta podwójna rola jest wygodna, gdy w jednej bibliotece potrzebujesz zarówno przechowywania haseł, jak i wyprowadzania kluczy.
Argon2 (id/i/d): zwycięzca Password Hashing Competition
Password Hashing Competition trwał od 2013 do 2015 roku i oceniał 24 kandydatów pod kątem memory-hardness, odporności na ataki side-channel i prostoty implementacji. Wygrał Argon2. W 2021 roku ustandaryzowano go jako RFC 9106.
Argon2 ma trzy warianty. Różnice sprowadzają się do tego, jak adresowana jest pamięć podczas mieszania:
- Argon2d stosuje adresy zależne od danych. To maksymalizuje odporność na ataki GPU i ASIC, ale wycieka informację przez kanały boczne związane z czasem cache’a. Pasuje do proof-of-work w kryptowalutach, nie do uwierzytelniania.
- Argon2i stosuje adresy niezależne od danych. Jest bezpieczny względem side-channel, ale nieco słabszy wobec ataków GPU typu tradeoff.
- Argon2id to hybryda: pierwsza połowa pierwszego przejścia używa indeksowania Argon2i (bezpieczeństwo side-channel), reszta używa indeksowania Argon2d (odporność na GPU). RFC 9106 wprost zaleca Argon2id do hashowania haseł, podobnie jak OWASP.
Argon2 przyjmuje trzy parametry:
- m — pamięć w KiB
- t — koszt czasu (liczba przejść po buforze pamięci)
- p — równoległość (liczba pasów przetwarzanych jednocześnie)
Hash Argon2id korzysta z formatu PHC string i wygląda tak:
$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
Tak jak w bcrypt, wszystkie parametry są wbudowane w ciąg, więc verify() nie potrzebuje osobnej tabeli parametrów.
Parametry rekomendowane przez OWASP 2026
OWASP Password Storage Cheat Sheet jest źródłem kanonicznym. Liczby poniżej odpowiadają jego aktualnym wytycznym. Są zachowawcze (dobrane pod typowy serwer WWW z budżetem opóźnień logowania 100–500 ms) i mimo wszystko warto przed wdrożeniem zrobić własny benchmark na docelowym sprzęcie.
Parametry Argon2id: pierwszy wybór
Bazowa rekomendacja OWASP: m=19456 (19 MiB), t=2, p=1.
Jeśli na serwerze masz zapas RAM, możesz przerzucić część pracy między pamięć a czas. RFC 9106 publikuje równoważne profile; OWASP dopuszcza dowolny z poniższych:
| memoryCost (m) | timeCost (t) | parallelism (p) | RAM na hash |
|---|---|---|---|
| 47104 | 1 | 1 | 46 MiB |
| 19456 | 2 | 1 | 19 MiB (bazowy) |
| 12288 | 3 | 1 | 12 MiB |
| 9216 | 4 | 1 | 9 MiB |
| 7168 | 5 | 1 | 7 MiB |
Praktyczna reguła strojenia. Najpierw dobierz m na podstawie budżetu RAM przy szczytowych równoległych logowaniach. Jeśli spodziewasz się 100 jednoczesnych logowań i masz w zapasie 4 GiB, to wychodzi 40 MiB na hash. Następnie zwiększaj t, aż jedno verify zajmie 100–500 ms na produkcyjnym CPU. Zostaw p=1, chyba że masz konkretny powód związany z wielordzeniowością (większość frameworków webowych i tak daje każdemu żądaniu osobny wątek).
Parametry scrypt: gdy Argon2 niedostępny
Rekomendacja OWASP: N=2^17 (131072), r=8, p=1, co daje 128 MiB na hash.
Jeżeli 128 MiB na każde równoległe logowanie to za dużo dla serwera, OWASP dopuszcza słabsze profile:
| N | r | p | RAM na hash |
|---|---|---|---|
| 2^17 | 8 | 1 | 128 MiB (preferowany) |
| 2^16 | 8 | 1 | 64 MiB |
| 2^15 | 8 | 1 | 32 MiB |
N musi być potęgą dwójki. Zwiększanie r proporcjonalnie zwiększa pamięć i pracę CPU; zwiększanie p zwiększa pracę CPU bez zwiększania pamięci na instancję. Dla hashowania haseł zostaw r i p na wartościach domyślnych i strój wyłącznie N.
bcrypt: cost factor 10+ tylko dla legacy
OWASP nie poleca już bcrypt dla nowych projektów, ale jest on wszędzie. Devise, Spring Security, ASP.NET Identity i niezliczone domowe systemy auth domyślnie z niego korzystają.
Jeżeli siedzisz z bcrypt, zasady są takie:
- Minimalny cost factor bcrypt: 10. Poniżej 10 jest wystarczająco szybko, by jedno GPU zamknęło wyciekniętą bazę w kilka dni.
- Zalecane: 12 do 14, w zależności od sprzętu. Na nowoczesnym serwerze x86
cost=12zajmuje około 250 ms na hash, acost=13— 500 ms. - Celuj w 100–300 ms na verify na produkcyjnym sprzęcie. Mierz, nie zgaduj.
- Pamiętaj o limicie 72 bajtów na wejściu. Jeśli użytkownicy mogą wybierać passphrase’y, wstępnie oblicz hash z SHA-256 (zob. FAQ).
Odporność bcrypt na GPU jest ograniczona przez 4 KiB pamięci. Żaden cost factor bcrypt nigdy nie dorówna memory-hardness Argon2id. Jeżeli możesz, wybierz Argon2id.
Dla porównania: na serwerze EPYC z 2024 roku bcrypt(cost=12) chodzi w okolicach 250 ms, na topowym laptopie bliżej 350 ms. Jeśli twoje wyniki rozjeżdżają się z przedziałem 100–500 ms o rząd wielkości, sprawdź najpierw, czy biblioteka faktycznie korzysta z natywnego bcrypt, czy spada do wolnego polyfilla w JavaScripcie (część bundlerów ścina natywne zależności w buildach serverless).
PBKDF2: ścieżka zgodności z FIPS-140
PBKDF2 (RFC 8018) to algorytm ostatniego ratunku w wytycznych bezpieczeństwa. Jest starszy niż bcrypt, nie jest memory-hard i pada przed atakami GPU szybciej niż każdy z trzech powyższych. Jest jednak jedyną prymitywą hashowania haseł, która jest walidowana FIPS-140, a to ma znaczenie dla administracji federalnej, sektora zdrowia (HIPAA) i niektórych wdrożeń finansowych.
Gdy potrzebujesz PBKDF2, użyj:
- HMAC-SHA-256 jako PRF (nie używaj SHA-1; nie używaj samego SHA-256 bez HMAC)
- 600 000 iteracji minimum (bazowa wartość OWASP 2026)
- Co najmniej 16-bajtowy losowy salt na każde hasło
Jeżeli FIPS cię nie dotyczy, wybierz Argon2id. Stała pamięć i stałe wyjście PBKDF2 oznaczają, że każdy dolar, który atakujący wydaje na krzem GPU, przekłada się wprost na więcej prób na sekundę.
Dokument NIST SP 800-63B określa PBKDF2-HMAC jako „approved” do hashowania haseł, ale powstrzymuje się przed rekomendowaniem go ponad alternatywami memory-hard. Czytaj to tak: NIST dopuszcza PBKDF2, bo jego wycofanie unieważniłoby każde stare wdrożenie rządowe, a nie dlatego, że to najlepszy wybór dla nowego projektu.
Schemat decyzyjny: który algorytm wybrać
Tabela porównawcza
| Wymiar | bcrypt | scrypt | Argon2id | PBKDF2 |
|---|---|---|---|---|
| Memory-hard | Nie | Tak | Tak | Nie |
| Odporność na GPU | Średnia | Wysoka | Bardzo wysoka | Niska |
| Odporność side-channel | Średnia | Średnia | Wysoka (id) | Średnia |
| Złożoność parametrów | 1 (cost) | 3 (N, r, p) | 3 (m, t, p) | 1 (iteracje) |
| Dojrzałość bibliotek | Doskonała | Dobra | Dobra | Doskonała |
| Limit długości wejścia | 72 bajty | Brak | Brak | Brak |
| Standaryzacja | de facto | RFC 7914 | RFC 9106 | RFC 8018 |
| Status OWASP 2026 | Tylko legacy | Alternatywa | Pierwszy wybór | Tylko FIPS |
Domyślnie używaj Argon2id
W nowym projekcie (typowa aplikacja webowa, nowoczesny stos Node/Python/Go/Rust/JVM, brak wymogu FIPS) użyj Argon2id z m=19456, t=2, p=1. Dostajesz najlepszą dostępną odporność na GPU i ataki side-channel, format z wbudowanymi parametrami, który przeżywa upgrade’y biblioteki, i żadnych niespodzianek z długością wejścia. Ekosystem bibliotek jest dojrzały: argon2 na npm, argon2-cffi na PyPI, golang.org/x/crypto/argon2, crate argon2 na crates.io. Wszystko aktywnie utrzymywane i obenchmarkowane.
Kiedy zamiast tego wybrać scrypt lub bcrypt
Wybierz scrypt, gdy Argon2 nie jest dostępny w twoim runtime’ie (naprawdę rzadkie w 2026 — nawet Cloudflare Workers i Deno już go mają) albo gdy masz produkcyjny system oparty na scrypt i koszt migracji przebija różnicę w bezpieczeństwie. Scrypt to nadal solidny algorytm, brakuje mu tylko dopracowania side-channel z Argon2id.
Wybierz bcrypt, gdy utrzymujesz system legacy, masz twardy wymóg minimalizacji zależności (żadnego natywnego kodu, żadnych dodatkowych pakietów), a limit 72 bajtów na wejściu jest do zaakceptowania w przypadku twojej bazy użytkowników. Bcrypt jest wdrożony na skalę internetową od dwóch dekad, jego tryby awarii są dobrze poznane.
Wybierz PBKDF2, gdy każe regulator. Innego powodu nie ma. Jeżeli twój audytor akceptuje Argon2id (a coraz więcej akceptuje przy obciążeniach poza FIPS), użyj Argon2id.
Częste błędy do uniknięcia
Większość wycieków haseł z ostatniej dekady da się sprowadzić do małego zestawu powtarzających się błędów inżynierskich. Żaden nie jest egzotyczny — wszystkie wyłapie review kodu auth z poniższą listą pod ręką.
- Hashowanie haseł surowym SHA-256 lub MD5. To największa pojedyncza wpadka w przechowywaniu haseł. Powody, dla których to nie pasuje do haseł, opisuje MD5 vs SHA-256.
- Używanie jednego globalnego salta dla wszystkich użytkowników. Salt musi być unikalny na rekord. Argon2 i bcrypt generują go za ciebie, nie nadpisuj tego.
- Ustawianie czasu hashowania poniżej 50 ms. Zamieniłeś bezpieczeństwo na przyspieszenie, którego żaden użytkownik nie zauważy. Celuj w 100–500 ms.
- Ustawianie czasu hashowania powyżej 1 sekundy. Stworzyłeś wektor DoS na własny endpoint logowania. Zatrzymaj się przy ~500 ms.
- Hashowanie haseł po stronie klienta i wysyłanie skrótu na serwer. Hash staje się wtedy hasłem. Każdy, kto ukradnie bazę, zaloguje się bez odwracania czegokolwiek. Hashuj zawsze na serwerze.
- Trzymanie parametrów algorytmu w osobnej kolumnie. Format PHC string osadza je w hashu. Korzystaj z niego.
- Logowanie haseł lub hashy w obsłudze błędów. Jedno i drugie należy do użytkownika, a nie do agregatora logów. Wyczyść je w warstwie parsowania żądania, zanim trafią do jakiegokolwiek loggera.
- Traktowanie wyjątków
verify()jak błędu uwierzytelnienia. Biblioteka, która rzuca wyjątkiem na uszkodzony hash, powinna ujawnić błąd, a nie cicho udawać „złe hasło”. Rozdziel „złe hasło” (zwróć 401) od „uszkodzony hash w bazie” (zwróć 500 i zapal pager).
Implementacja w praktyce
Argon2id w Node.js
Pakiet argon2 (natywne bindingi do referencyjnej implementacji) to kanoniczny wybór w Node:
import argon2 from 'argon2';
// Hashowanie przy rejestracji lub zmianie hasła
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
// → '$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>'
// Weryfikacja przy logowaniu
const ok = await argon2.verify(hash, candidate);
if (!ok) throw new Error('Invalid credentials');
// Wykryj nieaktualne parametry i przelicz hash po udanym logowaniu
if (argon2.needsRehash(hash, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 })) {
const upgraded = await argon2.hash(candidate, {
type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1,
});
await db.users.update({ id: user.id }, { password_hash: upgraded });
}
Krok needsRehash jest sekretem długoterminowej migracji: każde udane logowanie staje się okazją, by podbić zapisany hash do aktualnych parametrów, bez angażowania użytkownika.
Ten sam wzorzec w Pythonie z argon2-cffi:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(memory_cost=19456, time_cost=2, parallelism=1)
# Hash
stored = ph.hash(password)
# Weryfikacja
try:
ph.verify(stored, candidate)
except VerifyMismatchError:
raise ValueError('Invalid credentials')
# Przeliczenie po podbiciu parametrów
if ph.check_needs_rehash(stored):
stored = ph.hash(candidate)
W Go z golang.org/x/crypto/argon2:
import (
"crypto/rand"
"golang.org/x/crypto/argon2"
)
func hashPassword(password string) ([]byte, []byte) {
salt := make([]byte, 16)
rand.Read(salt)
hash := argon2.IDKey([]byte(password), salt, 2, 19456, 1, 32)
return hash, salt
}
Standardowa biblioteka Go nie dostarcza enkodera formatu PHC; jeśli używasz prymitywu argon2.IDKey bezpośrednio, sam odpowiadasz za zakodowanie parametrów i salta razem z hashem. Większość projektów Go sięga w tym celu po wrapper w stylu github.com/alexedwards/argon2id.
W Rust z crate’em argon2 jest podobnie idiomatycznie:
use argon2::{Argon2, PasswordHasher, PasswordVerifier, password_hash::{SaltString, rand_core::OsRng}};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default(); // Argon2id, m=19456, t=2, p=1 domyślnie
let hash = argon2.hash_password(password.as_bytes(), &salt)?.to_string();
// Weryfikacja
let parsed = argon2::password_hash::PasswordHash::new(&hash)?;
argon2.verify_password(candidate.as_bytes(), &parsed)?;
We wszystkich trzech runtime’ach wynikowy ciąg jest wymienialny. Hash zrobiony w Node weryfikuje się czysto w Pythonie albo w Rust. Ta krzyżowa zgodność czyni z Argon2 bezpieczniejszy wybór dla architektur poliglotycznych niż wrappery pisane pod konkretny algorytm.
Wzorzec migracji bcrypt → Argon2id
Prawie nigdy nie ma się luksusu wyczyszczenia tabeli użytkowników i zaczęcia od zera. Właściwy wzorzec migracji jest taki sam jak ten ze sekcji MD5 → bcrypt w FAQ naszego generatora hashy: miękki upgrade napędzany logowaniami.
Dodaj kolumnę śledzącą algorytm:
ALTER TABLE users ADD COLUMN password_algo VARCHAR(16) NOT NULL DEFAULT 'bcrypt';
Przy logowaniu rozdziel ruch do właściwego weryfikatora:
async function verifyAndMaybeRehash(user, candidate) {
let ok;
if (user.password_algo === 'argon2id') {
ok = await argon2.verify(user.password_hash, candidate);
} else if (user.password_algo === 'bcrypt') {
ok = await bcrypt.compare(candidate, user.password_hash);
if (ok) {
// Udane verify legacy → przelicz hash z Argon2id
const newHash = await argon2.hash(candidate, {
type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1,
});
await db.users.update({ id: user.id }, {
password_hash: newHash,
password_algo: 'argon2id',
});
}
}
return ok;
}
Ustaw okno wygaszania na 6–12 miesięcy. W okolicach 9. miesiąca wyślij maila „twoje hasło jest przechowywane przestarzałą metodą, zaloguj się, aby je zaktualizować”. Po 12 miesiącach konta nadal na bcrypt wymagają wymuszonego resetu hasła przy najbliższym logowaniu. Aktywni użytkownicy migrują transparentnie, nieaktywne konta dostają jednorazową niedogodność.
Ten sam wzorzec działa przy migracji ze scrypt albo PBKDF2. Jedyny stan, jakiego potrzebujesz, to kolumna password_algo.
Pepper, limity długości i pułapki kodowania
Kilka ostrych krawędzi, które tną realne wdrożenia.
Pepper. Pepper to sekret na poziomie aplikacji, dodawany do każdego hasła przed hashowaniem i przechowywany niezależnie od bazy (w KMS, zmiennej środowiskowej, Hashicorp Vault). Jeśli baza wycieknie, a sekret aplikacji nie, wyciekłe hashe są nie do zaatakowania bez peppera. Stosuj go jako HMAC, nie jako konkatenację:
import { createHmac } from 'crypto';
const peppered = createHmac('sha256', process.env.PEPPER).update(password).digest();
const hash = await argon2.hash(peppered, { type: argon2.argon2id, /* ... */ });
Rotuj pepper rzadko (wymaga przeliczenia hashy), ale wspieraj rotację przez wersjonowanie: PEPPER_V2, z fallbackiem na PEPPER_V1 przy verify.
Limit 72 bajtów w bcrypt. Jeżeli musisz używać bcrypt i chcesz wspierać hasła dowolnej długości, wstępnie policz hash SHA-256 i zakoduj base64 (omijając wbudowane bajty NUL, które bcrypt też obsługuje niespójnie):
import { createHash } from 'crypto';
const prepped = createHash('sha256').update(password, 'utf8').digest('base64');
const hash = await bcrypt.hash(prepped, 12);
Ta sama transformacja prepped musi pójść również przy verify. Udokumentuj to w kodzie auth ogromnym komentarzem. Przyszły ty podziękuje teraźniejszemu tobie.
Normalizacja UTF-8. Ciąg "café" można zakodować jako c-a-f-é (4 codepoints, NFC) albo c-a-f-e + combining acute (5 codepoints, NFD). Wyglądają identycznie, a dają różne hashe. Zawsze normalizuj do NFC przed hashowaniem:
const normalized = password.normalize('NFC');
To gryzie klawiatury mobilne i wklejanie z plików PDF częściej, niż mogłoby się wydawać.
Nigdy nie pre-hashuj po stronie klienta. Hash policzony u klienta, wysłany do serwera, jest nowym hasłem. Każdy, kto przeczyta bazę, zaloguje się. Hashuj na serwerze, kropka. JWT tu niczego nie zmienia. Zobacz jak zdekodować JWT token, aby sprawdzić, co JWT uwierzytelnia, a czego nie.
Benchmark na sprzęcie produkcyjnym, nie na laptopie. Laptop z Intelem 13. generacji uruchamia Argon2id z m=19456, t=2, p=1 w okolicach 35 ms. Te same parametry na instancji t3.small w EC2 zajmują bliżej 180 ms, na Raspberry Pi 4 ponad 600 ms. Wybierz sprzęt, na którym faktycznie pojedzie produkcja, zmierz czas 1000 verify i strój od mediany. Warto też zmierzyć rozrzut opóźnień logowania przy zimnych startach kontenerów serverless — cold starty Lambdy potrafią dorzucić 200–800 ms niezwiązanych z hashowaniem.
FAQ
Jaka jest różnica między hashowaniem haseł a szyfrowaniem?
Hashowanie jest jednokierunkowe: liczysz odcisk palca o stałej długości, którego nie da się odwrócić, by odzyskać wejście. Szyfrowanie jest dwukierunkowe: z właściwym kluczem da się odszyfrować i wrócić do oryginału. Hasła trzeba hashować, nie szyfrować. Serwer nie powinien być w stanie odzyskać hasła żadnego użytkownika. Dzięki temu wyciek bazy nie staje się wyciekiem poświadczeń.
Dlaczego nie mogę po prostu użyć SHA-256 do haseł?
SHA-256 zaprojektowano pod prędkość. Nowoczesne GPU liczy 22 miliardy hashy SHA-256 na sekundę, co oznacza, że ośmioznakowe hasło z małych liter z wyciekniętej bazy padnie w minuty. Hashe haseł potrzebują trzech własności, których SHA-256 nie ma: celowo wolnego wykonania, salta na rekord i memory-hardness. Ta sama zasada kompromisu jest opisana w wytycznej „Don’t Use MD5 for Security” naszego generatora hashy. O tym, jak atakujący zamieniają słabe hashe na tekst jawny, więcej przeczytasz w artykule entropia hasła.
Czy bcrypt jest nadal bezpieczny w 2026?
Sam bcrypt nie został złamany. Key schedule oparty na Blowfish pozostaje kryptograficznie zdrowy. Zmienił się model zagrożeń: GPU i ASIC sprawiają, że brak memory-hardness w bcrypt jest realną słabością wobec Argon2id. Stanowisko OWASP w 2026 jest takie, że bcrypt jest akceptowalny dla systemów legacy z cost ≥ 10, a nowe projekty powinny brać Argon2id.
Argon2i vs Argon2d vs Argon2id — którego użyć?
Użyj Argon2id. RFC 9106 wskazuje go jako rekomendowany wariant do hashowania haseł. Argon2i jest niezależny od danych (bezpieczny side-channel, ale słabszy wobec ataków GPU typu tradeoff). Argon2d jest zależny od danych (mocny wobec GPU, ale wrażliwy na kanały boczne związane z czasem cache’a). Argon2id to hybryda, która łączy oba atuty w cenie jednego.
Jak dobrać parametry Argon2id pod swoją aplikację?
Zacznij od bazy OWASP: m=19456, t=2, p=1. Potem zrób benchmark na produkcyjnym CPU i koryguj:
- Zdecyduj o budżecie RAM na jedno logowanie (np. 50 MiB przy szczycie równoległości).
- Ustaw
mna tę wartość lub mniej. - Uruchom
argon2.hash()w pętli i zmierz wall time. - Zwiększaj
t, aż mediana znajdzie się między 100 a 500 ms.
Zostaw p=1, chyba że masz wyniki z profilera pokazujące, że wielopasmowa równoległość pomaga twojemu runtime’owi. Dla serwerów uwierzytelniających pod dużym ruchem przesunięcie w stronę wyższego t i niższego m często daje lepszy zapas RAM.
Co to za limit 72 bajtów w bcrypt i jak obsłużyć długie passphrase’y?
Bcrypt podaje wejście do key schedule Blowfisha, który obcina je do 72 bajtów. Stupięćdziesięcioznakowe passphrase ma takie samo bezpieczeństwo jak jego pierwsze 72 bajty, reszta jest ignorowana. Rozwiązanie: policz wstępnie hash z SHA-256 (32 bajty) lub SHA-512 (64 bajty), zakoduj skrót base64 (żeby uniknąć bajtów NUL) i nakarm tym bcrypt. Argon2id i scrypt nie mają takiego limitu, przyjmują wejście dowolnej długości.
Czy mogę zmigrować z bcrypt na Argon2 bez wymuszania resetu haseł?
Tak. Wzorzec wygląda tak: trzymaj oba algorytmy za kolumną password_algo, kieruj weryfikację do właściwej biblioteki, a po każdym udanym verify w bcrypt natychmiast przelicz hash w Argon2id i zaktualizuj wiersz. Aktywni użytkownicy migrują po cichu w naturalnym rytmie logowań. Ustaw okno wygaszania 6–12 miesięcy dla nieaktywnych kont, a potem wymuś reset hasła dla każdego rekordu wciąż na bcrypt. Ten sam wzorzec działa przy każdej migracji algorytm-do-algorytmu.
Czy PBKDF2 to nadal dobry wybór w 2026?
Tylko wtedy, gdy zgodność z FIPS-140 nie zostawia wyboru. Typowe dla administracji federalnej, regulowanego sektora zdrowia (HIPAA) i pewnych systemów finansowych. Użyj HMAC-SHA-256 jako PRF z minimum 600 000 iteracji. PBKDF2 nie jest memory-hard, więc pada przed atakami GPU szybciej niż Argon2id przy równoważnym budżecie opóźnień. Jeżeli FIPS cię nie dotyczy, wybierz Argon2id i daruj sobie gimnastykę zgodności.
Odpowiedź na pytanie o hashowanie haseł w 2026 jest krótka: domyślnie Argon2id z bazowymi parametrami OWASP, scrypt jako fallback gdy Argon2 jest niedostępny, bcrypt tylko tam, gdzie wymaga tego legacy, a PBKDF2 zarezerwowany dla systemów związanych FIPS. Dołóż do hasha salt na rekord (każda nowoczesna biblioteka robi to automatycznie), pepper na poziomie aplikacji trzymany poza bazą oraz pętlę przeliczania hasha sterowaną logowaniami, która pozwala podnosić współczynniki pracy wraz z postępem sprzętu.
Wygeneruj reprezentatywny zestaw haseł generatorem haseł, zbenchmarkuj ścieżkę verify na produkcyjnym CPU i wpisz parametry do pliku ze stałymi, żeby kolejny inżynier wiedział dokładnie, co podbić w 2028 roku. Pełen kontekst bezpieczeństwa (TLS, zarządzanie sesjami, rate limiting, MFA) opisuje przewodnik po bezpieczeństwie aplikacji webowych. Wybierz właściwy hash dla swojej aplikacji.