PHP Manual
/
Bezpečnost

Hashování řetězců a hesel

11. 09. 2019

Proces hashování (na rozdíl od šifrování) ze vstupu vytvoří takový výstup, z kterého již nelze odvodit původní řetězec.

Výborně se proto hodí pro ochranu citlivých řetězců, hesel a kontrolní součty.

Další příjemná vlastnost hashovacích funkcí je, že generují výstupy vždy se stejnou délkou a malá změna na vstupu vždy kompletně změní celý výstup.

Hashovací funkce

V PHP existuje mnoho hashovacích funkcí, ty důležité jsou:

  • Bcrypt: password_hash() - Nejbezpečnější hashování hesel, výpočetně pomalá, používá vnitřní salt a hashuje iterativně.
  • md5() - Velice rychlá funkce vhodná pro hashování souborů. Výstup má vždy 32 znaků.
  • sha1() - Rychlá hashovací funkce pro hashování souborů, používá ji interně Git pro hashovací commitů. Výstup má vždy 40 znaků.

Zahashování

$password = 'tajne-heslo';
echo password_hash($password); // Bcrypt
echo md5($password);
echo sha1($password);

Pozor: Funkcemd5() ani sha1() není vhodná k hashování hesel, protože je výpočetně jednoduché původní heslo odhalit, nebo si hesla aspoň předpočítat. Mnohem lepší je použít bcrypt, který pro hashování hesel vznikl.

Web md5cracker.com obsahuje databázi kontrolních součtů (hashů), zkuste vyhledat třeba hash: 79c2b46ce2594ecbcb5b73e928345492, jak je vidět, tak čisté md5() není zas tak bezpečné pro častá slova a hesla.

Jediné správné řešení: Bcrypt + salt

Na přednášce Jak to nepokazit v cílové rovince řešil David Grudl způsoby, jak správně hashovat a ukládat hesla.

Jediné správné řešení je: Bcrypt + salt.

Konkrétně:

$password = 'hash';
// Vygeneruje bezpečný hash
echo password_hash($password, PASSWORD_BCRYPT);
// Případně s vyšší složitostí (výchozí je 10):
echo password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

Výhoda Bcrypu je hlavně v jeho rychlosti a automatickém solení.

Díky tomu, že generování trvá dlouho, třeba 100 ms, tak je pro útočníka velmi drahé testovat mnoho hesel.

Výstupní hash je navíc automaticky ošetřen pomocí náhodné sole, díky které při opakovaném hashování stejného hesla je na výstupu vždy jiný hash. Útočník proto nebude moci použít předpočítanou tabulku hashů.

Správnost hesla proto nebudeme moci ověřit opakovaným hashováním, ale bude potřeba zavolat specializovanou funkci:

if (password_verify($password, $hash)) {
// Heslo je správně
} else {
// Heslo není správně
}

Solení hesel

Aby bylo prolomení hashů složitější, je dobrý nápad do originálního vstupu vkládat nějaký další řetězec. V ideálním případě náhodný. Tomuto procesu se říká solení hesel.

Bezpečnost je založena na myšlence, že útočník nebude moci použít předpočítanou tabulku hesel a hashů, protože nebude znát sůl a bude muset hesla lámat jednotlivě.

Například:

$password = 'tajne_heslo';
$salt = 'fghjgtzjjhg';
$hash = md5($password . $salt);
echo $password; // vypíše původní heslo
echo $hash; // vypíše hash hesla včetně soli

Složené hashovací funkce

Možná vás napadá, že by byl dobrý nápad hashovací funkci provést opakovaně a tím zvednout složitost prolomení, protože bude potřeba originální heslo opakovaně hashovat.

Například:

$password = 'heslo';
for ($i = 0; $i <= 1000; $i++) {
$password = md5($password);
}
echo $password; // 1000x zahashováno přes md5()

Tímto postupem se náročnost prolomení paradoxně snižuje, nebo zůstává téměř stejná.

Důvod je ten, že funkce md5() je extrémně rychlá a na běžné počítači lze spočítat přes milion hashů za sekundu, proto se postupné zkoušení hesel o moc nezpomalí.

Druhý důvod je spíše teorie, a to možnost narazit na tzv. kolizi. Pokud budeme heslo opakovaně hashovat, časem se může stát, že se trefíme do hashe, který už útočník zná a díky tomu bude moci heslo promolit pomocí databáze.

Lepší je proto použít pomalou bezpečnou hashovací funkci a provádět hashování pouze jednou, přičemž se finální výstup ještě ošetří solením.

Mimořádně bezpečné porovnávání dvou hashů / řetězců

Věděli jste, že operátor === není pro porovnání hashů při ověřování hesla nejbezpečnější volba?

Při porovnávání řetězců se prochází oba řetězce znak po znaku, dokud se nedojde na konec (úspěch, jsou stejné), nebo nenastane rozdíl (stringy jsou různé).

A toho se dá při útoku využít. Když budete dostatečně přesně měřit čas, lze odhadnout, kolik znaků ještě zbývá doplnit, abyste měli přesnou shodu a došlo se na konec, resp. lze odhadnout, jak daleko se při porovnání řetězců došlo.

Řešením je použít funkci hash_equals() všude, kde se porovnávají řetězce a vadilo by, kdyby útočník mohl zjistit pozici, kde porovnání selhalo.

A jak to funkce dělá? Zajistí, aby porovnání libovolných 2 řetězců trvalo vždy stejně, takže nelze měřením času zjistit, kde došlo k rozdílu. Některé typy útoků mi přijdou opravdu velmi nepravděpodobné a těžko proveditelné. Tohle zrovna patří mezi ně.

Jan Barášek   Více o autorovi

Autor článku pracuje jako seniorní vývojář a software architekt v Praze. Navrhuje a spravuje velké webové aplikace, které znáte a používáte. Od roku 2009 nabral bohaté zkušenosti, které tímto webem předává dál.

Rád vám pomůžu:

Související články

1.
5.

Potřebujete poradit s PHP?

Nabízím trénink vývojářů, konzultace, školení a analýzu návrhových vzorů. Osobně v Praze nebo online.

Napište mi, pokud si nevíte rady.

Lektor: Jan Barášek

Status:
All systems normal.
2024