hešování heslaTakéHašování hesel, Password hash, Ukládání hesel hashemPokročilý
Definice
Hashování hesla je převod hesla na neobrátitelný otisk pomocí záměrně pomalé funkce, například bcrypt, scrypt nebo Argon2, aby se do databáze nikdy neukládalo heslo v čitelné podobě. Ke každému heslu se přidává náhodná sůl, takže shodná hesla dají různé otisky a útočník nemůže použít předpočítané tabulky.
Než se na to spolehnete: Doporučené konkrétní parametry (paměť, iterace, cost faktor) se v čase mění, proto je v textu záměrně neuvádím číselně kromě obvyklého cílového času ověření; ten je orientační. Odkaz na cheatsheetseries.owasp.org je subdoména owasp.org, prosím ověřit, zda projde kontrolou povolených hostitelů.
Proč se hesla neukládají tak, jak je uživatel napsal
Databáze uniká častěji, než se čeká: přes chybu v aplikaci, špatně nastavenou zálohu nebo zaměstnance s příliš velkým oprávněním. Pokud v tabulce leží hesla v čitelné podobě, útočník má okamžitě přístup nejen k dané službě, ale i ke všem účtům, kde si uživatel heslo zopakoval. Hashování hesla tuhle jedinou chybu mění z katastrofy na zdržení: z otisku se původní heslo nedá spočítat zpět, jde ho jen hádat.
Co dělá sůl a proč nestačí SHA-256
Sůl je náhodná hodnota (typicky 16 bajtů), která se ke každému heslu vygeneruje zvlášť a uloží se vedle otisku, klidně otevřeně. Bez soli mají dva uživatelé se stejným heslem stejný otisk a útočník rozpozná opakování i použije předpočítané duhové tabulky. Se solí musí lámat každý účet samostatně.
Běžné hashovací funkce jako SHA-256 jsou navržené, aby byly rychlé. To je pro hesla přesně obráceně: moderní grafická karta zvládne miliardy SHA-256 operací za sekundu, takže slovníkový útok proběhne během hodin. Funkce určené pro hesla jsou proto záměrně pomalé a nastavitelné parametrem ceny. Argon2id navíc vynucuje spotřebu paměti, čímž znevýhodňuje útok na GPU a specializovaném hardwaru.
Dnes doporučované volby
- Argon2id: výchozí volba pro nové projekty, ladí se paměť, počet iterací a paralelismus.
- scrypt: rovněž paměťově náročná, dobrá alternativa tam, kde Argon2 není v knihovnách dostupný.
- bcrypt: prověřená a všude dostupná, pozor na limit délky vstupu kolem 72 bajtů.
- PBKDF2: přijatelná hlavně kvůli certifikacím, ale bez paměťové náročnosti, tedy nejslabší z uvedených.
Co k tomu potřebuje aplikace navíc
Hashování hesla řeší jen následek úniku databáze. Neochrání účet, jehož heslo si uživatel napsal do podvodného formuláře, tam pomáhá osvěta o phishingu a hlavně dvoufaktorové ověření. Neochrání ani proti hádání hesel přímo přes přihlašovací formulář, na to je rate limiting a zpožďování opakovaných pokusů.
Do praxe patří i pár drobností, které se snadno opomíjejí. Otisk se porovnává funkcí odolnou proti časovým útokům, ne obyčejným ==. Parametry ceny se časem zvyšují, takže knihovna má umět poznat zastaralý otisk a při úspěšném přihlášení heslo přehashovat. Hesla se logují nikdy a nikde: ani v ladicím výpisu, ani v hlášení chyby. A samotný přenos hesla mezi prohlížečem a serverem chrání HTTPS, hashování na straně klienta ho nenahradí.
Hashování versus šifrování
Šifrování je obousměrné: kdo má klíč, dostane původní text. Hashování hesla je jednosměrné záměrně, protože server nikdy nepotřebuje heslo přečíst, stačí mu ověřit, že zadaný vstup dá stejný otisk. Služba, která umí poslat zapomenuté heslo e-mailem, ho tedy neháše, a to je vážná chyba návrhu.
Příklady z praxe
Registrace a přihlášení v Node.js s Argon2
Při registraci se heslo předá knihovně, která si sama vygeneruje sůl a výsledný řetězec obsahuje algoritmus, parametry, sůl i otisk. Do databáze se ukládá celý tento řetězec do jednoho sloupce. Při přihlášení se nic nedešifruje, jen se ověří, že zadané heslo odpovídá uloženému otisku.
import argon2 from 'argon2'; // registrace const hash = await argon2.hash(plainPassword, { type: argon2.argon2id }); await db.users.insert({ email, passwordHash: hash }); // přihlášení const user = await db.users.findByEmail(email); const ok = user && await argon2.verify(user.passwordHash, plainPassword); if (!ok) return unauthorized();Zvýšení ceny hashe u starých účtů
Projekt začínal na bcryptu s cost faktorem 10, po třech letech je serverům tenhle výpočet příliš levný. Nové otisky se rovnou počítají s vyšším faktorem a u starších uživatelů se otisk přepočítá při prvním úspěšném přihlášení, protože jen v tu chvíli aplikace heslo v čitelné podobě má. Uživatel nic nepozná a migrace proběhne postupně.
if (verify(user.hash, password)) { if (needs_rehash(user.hash, cost=12)): user.hash = hash_password(password, cost=12) save(user) login(user)
Časté omyly
- MýtusPoužíváme SHA-256, to je přece kryptografická funkce, takže je to bezpečné.
- Ve skutečnostiSHA-256 je bezpečná jako hashovací funkce, ale je extrémně rychlá, což je pro hesla nevýhoda. Útočník s běžnou grafickou kartou vyzkouší miliardy kandidátů za sekundu. Pro hesla patří Argon2id, scrypt nebo bcrypt.
- MýtusSůl musí být tajná, jinak je k ničemu.
- Ve skutečnostiSůl se běžně ukládá vedle otisku a její prozrazení nic nekazí. Jejím úkolem je znemožnit předpočítané tabulky a zajistit, aby stejná hesla měla různé otisky. Tajná hodnota sdílená napříč účty se nazývá pepper a je to doplněk, ne náhrada soli.
- MýtusKdyž heslo zahashujeme v prohlížeči, server ho už nemusí hashovat.
- Ve skutečnostiHash odeslaný z prohlížeče se stává skutečným heslem: kdo ho odposlechne nebo získá z databáze, přihlásí se s ním. Server proto musí přijatou hodnotu vždy zahashovat pomalou funkcí se solí.
Časté dotazy
- Jak dlouho má výpočet hashe hesla trvat?
- Výpočet hashe hesla se obvykle ladí tak, aby jedno ověření trvalo zhruba 0,2 až 1 sekundy na produkčním serveru. Kratší čas usnadňuje útok hrubou silou, delší zhoršuje odezvu přihlášení a otevírá prostor pro zahlcení serveru opakovanými pokusy. Správný postup je změřit dobu přímo na cílovém hardwaru a podle toho nastavit paměť a počet iterací u Argon2id nebo cost faktor u bcryptu. Hodnoty je vhodné jednou za čas revidovat, protože výkon serverů i útočníků roste.
- Kam se ukládá sůl, když je součástí hashe?
- Sůl se u bcryptu, scryptu i Argon2 ukládá přímo do výsledného řetězce spolu s identifikátorem algoritmu a parametry ceny. Aplikace tedy potřebuje jediný textový sloupec, typicky délky kolem 100 znaků, a nemusí sůl spravovat zvlášť. Ověřovací funkce si sůl i parametry z řetězce sama přečte a použije je při přepočtu. Vlastní sloupec pro sůl je nutný jen u nízkoúrovňových implementací, například u ručně sestaveného PBKDF2.
- Co je pepper a potřebuje ho každá aplikace?
- Pepper je tajná hodnota společná pro celou aplikaci, která se přimíchá k heslu před hashováním nebo se výsledný otisk ještě zašifruje. Na rozdíl od soli se ukládá mimo databázi, ideálně v HSM nebo správci tajemství. Přínos spočívá v tom, že samotný únik databáze bez úniku serverového tajemství útočníkovi nestačí. Většina aplikací pepper nepotřebuje a jeho zavedení komplikuje rotaci klíčů, takže dává smysl spíš u služeb s vysokou hodnotou účtů.
- Co dělat po úniku databáze s hashi hesel?
- Po úniku databáze s hashi hesel je nutné incident nahlásit podle GDPR, vynutit reset hesel a zneplatnit všechny existující relace a tokeny. Pomalý hash se solí získává čas, ale slabá a opakovaně používaná hesla útočník prolomí i tak. Uživatelům je vhodné doporučit změnu hesla i na jiných službách a nabídnout dvoufaktorové ověření. Zároveň patří k nápravě revize parametrů hashování a přehashování otisků při dalším přihlášení.
Zdroje
- OWASP Cheat Sheet Series(otevře se v novém okně)
- NIST Special Publications (SP) 800 Series(otevře se v novém okně)
- RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications(otevře se v novém okně)
- RFC 7914: The scrypt Password-Based Key Derivation Function(otevře se v novém okně)
- Crypto: Node.js documentation(otevře se v novém okně)