Menu

password_hash() i password_verify() w PHP: hasła

Haszuj hasło przez password_hash($password, PASSWORD_DEFAULT), zapisz 60-znakowy wynik i sprawdzaj logowanie przez password_verify($password, $hash). Dowiedz się, dlaczego hash za każdym razem się zmienia, czym jest koszt, password_needs_rehash i Argon2id oraz dlaczego md5 i sha1 nie nadają się do haseł.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Przechowuj hasła przez password_hash($password, PASSWORD_DEFAULT), a logowanie sprawdzaj przez password_verify($password, $hash), które zwraca true albo false. Nigdy nie przechowuj samego hasła i nigdy nie używaj do niego md5() ani sha1().

Uruchom to dwa razy: hash za każdym razem jest inny, a obie wersje przechodzą weryfikację. To samo dotyczy każdego hasha na tej stronie, więc twój wynik nie będzie zgodny z niczyim innym.

Logowanie przez password_verify

Logowanie sprawdza wysłane hasło z hashem zapisanym przy tworzeniu konta. Ten blok „rejestruje” jednego użytkownika, a potem pokazuje formularz logowania; spróbuj właściwego hasła (hunter2), a potem złego.

Komunikat błędu jest taki sam dla nieznanego e-maila i złego hasła. Powiedzenie, co było źle, zdradza atakującemu, które e-maile mają konta. Po udanym logowaniu prawdziwa strona zapisuje identyfikator użytkownika w sesji, jak pokazuje strona o sesjach.

Co jest w hashu

Hash bcrypt to jeden string zawierający wszystko, czego potrzebuje password_verify(): algorytm, koszt i sól, a po nich sam hash. password_get_info() odczytuje pierwsze części z powrotem.

Ponieważ sól jest zapisana w hashu, potrzebujesz tylko jednej kolumny w bazie danych, a dwóch użytkowników z tym samym hasłem i tak dostaje różne hashe. Niech ta kolumna ma VARCHAR(255): bcrypt ma 60 znaków, ale PASSWORD_DEFAULT może zmienić się na algorytm z dłuższym wynikiem.

Wybór kosztu

Koszt określa, jak celowo wolne jest każde haszowanie, a każdy krok w górę podwaja pracę. PHP 8.3 używa dla bcrypt kosztu 10 (PHP 8.4 podniosło domyślną wartość do 12). Logowanie powinno zajmować mniej więcej od 100 do 300 ms haszowania; zmierz to na własnym serwerze:

Liczby zależą od maszyny, ale każda linia powinna być mniej więcej dwa razy większa od poprzedniej. Wyższy koszt jest bezpieczniejszy wobec łamania i wolniejszy przy każdym logowaniu, więc wybierz najwyższy, na jaki stać twoją stronę logowania.

Aktualizacja starych hashy przez password_needs_rehash

Gdy podnosisz koszt albo PHP zmienia PASSWORD_DEFAULT, istniejące hashe nadal działają, a password_needs_rehash() mówi, które są nieaktualne. Jedyna chwila, w której masz czyste hasło, to logowanie, więc tam aktualizujesz:

Prefiks $2y$10$ zmienia się na $2y$12$, a przy następnym logowaniu password_needs_rehash() zwraca false.

Nigdy nie używaj md5 ani sha1 do haseł

md5() i sha1() to szybkie hashe ogólnego przeznaczenia bez soli. Szybkość to przy hasłach dokładnie to, czego nie chcesz: atakujący z wykradzioną tabelą może sprawdzać miliardy prób na sekundę na GPU, a niesolony hash popularnego hasła jest już w publicznych tablicach wyszukiwania. Porównaj szybkość:

W czasie, w którym password_hash() tworzy jeden hash, md5 tworzy ich setki tysięcy, więc każda próba łamiącego jest tyle razy tańsza. Wyszukaj 5f4dcc3b5aa765d61d8327deb882cf99, a od razu znajdziesz słowo „password”.

Jeśli odziedziczysz tabelę hashy md5, nie da się ich bezpośrednio przekonwertować. Zamiast tego je opakuj: zapisz password_hash($oldMd5) i weryfikuj przez password_verify(md5($input), $stored), a potem zastępuj każdy zwykłym hashem przy następnym logowaniu danego użytkownika.

Argon2id i limit 72 bajtów

PASSWORD_ARGON2ID zużywa więcej pamięci na hash, co czyni łamanie przez GPU dużo droższym. Jest dostępny, gdy PHP zbudowano z libargon2 (tak jest w większości pakietów dla Linuksa); sprawdź przez defined('PASSWORD_ARGON2ID').

Ostatnia linia wypisuje bool(true): dwa różne hasła pasują, bo bcrypt ignoruje wszystko po 72. bajcie. Prawdziwe hasła rzadko tyle osiągają, ale frazy po japońsku lub w innych pismach wielobajtowych dochodzą do 72 bajtów przy około 24 znakach (polskie litery z ogonkami też zajmują po 2 bajty). Jeśli pozwalasz na bardzo długie hasła, użyj Argon2id, który nie ma takiego limitu. password_verify() działa z oboma algorytmami, więc możesz przejść na inny przez password_needs_rehash(), jak wyżej.

Najczęściej zadawane pytania

Dlaczego password_hash za każdym razem daje inny wynik?

Każde wywołanie generuje nową losową sól i zapisuje ją wewnątrz hasha, więc to samo hasło daje za każdym razem inny string. Tak ma być: password_verify() odczytuje sól z zapisanego hasha, więc dla właściwego hasła nadal zwraca true.

Jak długa powinna być kolumna na hasło z password_hash?

Hash bcrypt z PASSWORD_DEFAULT ma dziś 60 znaków, ale domyślny algorytm może się zmienić w przyszłej wersji PHP. Użyj VARCHAR(255), żeby dłuższy hash zmieścił się bez migracji.

Dlaczego password_verify zawsze zwraca false?

Zwykłe przyczyny to obcięty hash (kolumna VARCHAR(50) ucina 60-znakowy hash), przekazanie jako drugiego argumentu świeżego password_hash() czegoś zamiast zapisanego hasha, porównywanie z hashem zrobionym przez md5() albo dodatkowe białe znaki w zapisanej wartości. Sprawdź, czy strlen($hash) to 60, i przekazuj czyste hasło jako pierwszy argument.

Czy można odszyfrować wartość password_hash w PHP?

Nie. Hash jest jednokierunkowy: nie ma funkcji, która zamieni go z powrotem w hasło. Aby sprawdzić hasło, haszujesz próbę i porównujesz, i to właśnie robi password_verify(). Przy zapomnianym haśle wysyłaj link do resetu zamiast starego hasła.

Używać PASSWORD_DEFAULT, PASSWORD_BCRYPT czy PASSWORD_ARGON2ID?

PASSWORD_DEFAULT to zwykły wybór: dziś to bcrypt, a PHP może później przejść na silniejszy algorytm, który password_needs_rehash() pozwala wdrażać stopniowo. PASSWORD_ARGON2ID jest odporniejszy na łamanie przez GPU i można go używać, gdy twoja kompilacja PHP go zawiera.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ