Jak bezpečně ukládat hesla: Argon2id, bcrypt a co nepoužívat
Proč MD5 ani SHA-256 na hesla nestačí, jak funguje sůl a pomalé hashovací funkce a jak správně hashovat hesla v PHP, Pythonu a Node.js.
Když uniknou data z databáze, rozhoduje o škodě jediná věc: jak byla uložena hesla. Správně zahashovaná hesla útočník prakticky nezíská, špatně uložená prolomí během hodin. Dobrá zpráva je, že správné řešení zabere pár řádků kódu.
Co nedělat
- Hesla v čitelné podobě. Bez komentáře.
- Šifrování (AES apod.). Šifra je vratná – kdo získá klíč, má všechna hesla. Heslo nikdy nepotřebujete dešifrovat, jen ověřit.
- MD5, SHA-1, SHA-256, SHA-512. Jsou to kryptografické hashe, ale navržené pro rychlost. Moderní GPU jich spočítá miliardy za sekundu, takže běžná hesla padnou útokem hrubou silou nebo slovníkem.
- Vlastní konstrukce typu
sha256(salt + heslo)opakované tisíckrát. Pravděpodobně v tom bude chyba a stejně to bude slabší než hotové řešení.
Co dělá dobrý hash hesla
- Sůl – náhodná hodnota unikátní pro každé heslo. Dva uživatelé se stejným heslem mají jiný hash a předpočítané tabulky (rainbow tables) jsou k ničemu.
- Pomalost – nastavitelná cena výpočtu, aby jeden pokus trval desítky až stovky milisekund.
- Paměťová náročnost (u Argon2 a scrypt) – výrazně ztěžuje útok na GPU a specializovaném hardwaru.
Moderní knihovny sůl generují samy a ukládají ji přímo do výsledného řetězce spolu s parametry. Do databáze tedy stačí jeden sloupec.
Které algoritmy použít
| Algoritmus | Doporučení |
|---|---|
| Argon2id | první volba pro nové projekty |
| bcrypt | stále bezpečný, široce podporovaný; pozor na limit 72 bajtů vstupu |
| scrypt | dobrý, pokud Argon2 není dostupný |
| PBKDF2 | jen kde je vyžadována certifikace FIPS, s vysokým počtem iterací |
OWASP Password Storage Cheat Sheet doporučuje pro Argon2id minimálně 19 MiB paměti, 2 iterace a paralelismus 1. Výchozí hodnoty knihoven bývají podobné nebo vyšší.
PHP
PHP má vše vestavěné od verze 5.5 (bcrypt) a 7.3 (Argon2id, pokud je PHP zkompilované s libargon2):
<?php
$hash = password_hash($password, PASSWORD_ARGON2ID);
// nebo: password_hash($password, PASSWORD_DEFAULT); // aktuálně bcrypt
if (password_verify($inputPassword, $hash)) {
if (password_needs_rehash($hash, PASSWORD_ARGON2ID)) {
$newHash = password_hash($inputPassword, PASSWORD_ARGON2ID);
// uložit $newHash do databáze
}
// přihlášení OK
}
password_needs_rehash umožní plynule přejít na silnější parametry – hash se přepočítá při dalším přihlášení. Sloupec dimenzujte s rezervou, například VARCHAR(255).
Python
S knihovnou argon2-cffi (pip install argon2-cffi):
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher() # rozumné výchozí parametry
hash_ = ph.hash(password)
try:
ph.verify(hash_, input_password)
if ph.check_needs_rehash(hash_):
hash_ = ph.hash(input_password) # a uložit
except VerifyMismatchError:
print("Špatné heslo")
Django a FastAPI projekty často používají passlib nebo vestavěné hashery (Django má Argon2PasswordHasher, který stačí zapnout v PASSWORD_HASHERS).
Node.js
Balíček argon2 (npm install argon2):
import argon2 from "argon2";
const hash = await argon2.hash(password); // výchozí typ je argon2id
if (await argon2.verify(hash, inputPassword)) {
// přihlášení OK
}
Pro bcrypt existuje bcrypt (nativní) nebo bcryptjs (čistý JavaScript, pomalejší). Cost faktor nastavte alespoň na 10, raději 12.
Jak nastavit cenu
Změřte na produkčním hardwaru, kolik trvá jeden hash. Cíl je zhruba 100–500 ms pro přihlášení – pro uživatele nepostřehnutelné, pro útočníka zdrcující. Myslete ale i na zátěž: pokud se přihlašuje sto lidí za sekundu, server to musí utáhnout. Přihlašovací endpoint proto vždy chraňte i rate limitingem, jinak se z pomalého hashe stává nástroj pro DoS.
Další dobré praktiky
- Nevymýšlejte limity složitosti typu „velké písmeno a speciální znak“. Aktuální NIST SP 800-63B doporučuje spíš minimální délku (alespoň 8, lépe 12+ znaků), povolit dlouhá hesla a kontrolovat je proti seznamu uniklých hesel.
- Kontrola uniklých hesel přes Have I Been Pwned Pwned Passwords API funguje anonymně: posíláte jen prvních 5 znaků SHA-1 hashe (k-anonymity).
- Porovnání v konstantním čase zajišťují funkce
verifysamy; nehashujte a nesrovnávejte ručně přes==. - Pepper – tajný klíč mimo databázi (například HMAC hesla před hashováním) – přidává ochranu, pokud unikne jen databáze. Zkomplikuje ale rotaci klíče.
- Neomezujte délku hesla na 16 znaků. Rozumný horní limit (třeba 128 znaků) jen kvůli ochraně před DoS.
- Logy – nikdy nelogujte těla přihlašovacích požadavků.
Migrace ze starého MD5
Nemusíte čekat, až se všichni přihlásí. Zabalte starý hash do nového: argon2id(md5_hash), označte záznam příznakem a při přihlášení ověřujte argon2id(md5(heslo)). Po úspěšném přihlášení hash nahraďte čistým argon2id(heslo). Tím ochráníte okamžitě všechny účty, i ty neaktivní.