Ukládání hesel bezpečně

Školení, která pořádám

Článek vyšel v Crypto-Worldu 11-12/2013.

V Crypto-Worldu 9-10/2013 vyšel článek „Ochrání hashování uživatelská hesla?“, který na otázku z titulku bohužel neodpověděl. V tomto článku bych tedy rád popsal, jak uživatelská hesla správně ukládat.

V první řadě musíme vyloučit nesmyslná řešení jako ukládání hesel v čistém textu nebo jejich šifrování. Jsou špatná proto, že heslo se nesmí dozvědět ani provozovatel aplikace, který má přístup ke všem algoritmům, datům a klíčům. Pro ukládání hesel je potřeba použít hašování a k heslu přiložit náhodnou sůl. Tu přidáváme především proto, aby nebylo poznat, že dvě hesla jsou stejná, ať už hesla dvou uživatelů u stejné služby nebo hesla stejného uživatele u dvou různých služeb. Druhým důvodem je zamezení použití rainbow tables – předpočítaných tabulek hašů.

Pomalé hašovací funkce

Víme už tedy, že hesla musíme hašovat a že k nim musíme přiložit náhodnou sůl. Zbývá výběr hašovací funkce. Běžné hašovací funkce jako MD5, SHA-1 nebo SHA-2 jsou pro ukládání hesel nevhodné proto, že jsou příliš rychlé. MD5 hesla složeného z osmi malých písmen prolomí oclHashcat na celkem běžné grafické kartě průměrně za 10 sekund (26^8 / 10.742e9 / 2)! Pokud požadavek na heslo zpřísníme třeba tím, že budeme vyžadovat použití i velkých písmen a čísel, tak řada uživatelů dá velké písmeno na začátek a číslo přidá na konec. Takto zkonstruované heslo útočník prolomí dokonce za 4 sekundy. A to vůbec nemluvíme o použití slovníků nebo hesel získaných z dříve uniknutých databází.

Týrat uživatele požadavky na dlouhá a složitá hesla zkrátka není řešení. Geekům to možná vadit nebude, protože si heslo uloží do klíčenky, ale pro běžné uživatele to bude pohroma končící zapsáním hesla na žlutý papírek přilepený na monitor. Řešením je použít hašovací funkci, která je záměrně pomalá.

Pomalé hašovací funkce vám nabídnu tři: Bcrypt, PBKDF2 a Scrypt. Všechny tři se pro ukládání hesel dají použít, všechny mají parametr umožňující nastavit, kolik iterací hašování se má provést. Ten je vhodné nastavit tak, aby se jedno zadané heslo ověřilo na vašich serverech v rozumném čase, např. do půl sekundy. Vaše uživatele to nijak výrazně neomezí a útočníka to podstatně zpomalí.

Kterou funkci vybrat? Bcrypt je asi nejdostupnější, např. v PHP ji používá funkce password_hash a již dlouhou dobu i funkce crypt. PBKDF2 je nejstandardnější, používá se třeba ve WPA. Scrypt se od ostatních liší tím, že kromě počtu iterací používá i konfigurovatelné množství paměti, což bude představovat značný problém pro útočníka snažícího se paralelně louskat více hesel najednou. Osobně bych tedy doporučil použít Scrypt.

Mohlo by vás napadnout i prosté opakované použití rychlé hašovací funkce, třeba miliardkrát. Problém s tímto přístupem je v tom, že můžou vznikat cykly, které velkou množinu vstupů namapují na mnohem menší množinu hašů. Přestože jde spíše o teoretický problém, tak se tomuto řešení raději vyhněte.

Co se starými hesly?

Co dělat v případě, že nevyvíjíte novou aplikaci, ale chcete zabezpečit nějakou stávající, kde máte hesla uložená třeba pomocí MD5? Mohou vás napadnout tato řešení:

  1. Nový algoritmus použít pro nově registrované uživatele a těm stávajícím ho změnit při přihlašování (kdy máte k dispozici heslo v čistém textu).
  2. Uživatelům poslat zprávu a požádat je o změnu hesla.
  3. Pokusit se prolomit všechna hesla a přeuložit je.

Všechno to jsou hlouposti. Správné řešení je nový algoritmus aplikovat na původní haš, uložené heslo tedy bude např. výsledkem funkce scrypt(cost, salt, md5(password)). Všechny stávající uživatele převedete jednorázově a při ověřování hesla aplikujete danou posloupnost funkcí. Pokud se v budoucnu rozhodnete použít jiný algoritmus, jednoduše ho do posloupnosti přidáte.

Pokud máte důvodné podezření, že databáze s nebezpečně uloženými hesly už unikla, tak všechna stará hesla zneplatněte a uživatelům pošlete jednorázový odkaz pro jejich nové vytvoření.

Nejde jen o hesla

Hesla nejsou jediné tajemství, které musíme chránit. U webových aplikací je skoro stejně hodnotný i session identifikátor, který se používá pro přihlášené uživatele. Ten se na serveru často válí v čistém textu, aniž by se o něj někdo staral. Přitom když ho útočník získá, tak může jménem uživatele provádět většinu akcí. Věnujte mu proto stejnou pozornost jako heslu samotnému. Jen rychlost jeho výpočtu může být mnohem vyšší, protože se ověřuje mnohem častěji a při jeho úniku to přeci jen není tak velká katastrofa jako při úniku hesel – přinejhorším můžeme všechny uživatele odhlásit.

Přijďte si o tomto tématu popovídat na školení Bezpečnost PHP aplikací.

Jakub Vrána, Dobře míněné rady, 16.12.2013, diskuse: 9 (nové: 0)

Diskuse

myšnekočklokanutrie:

Jakube, co si myslíš o praktice pozdržet záměrně skript (např. zavolat sleep(5000)) v případě zadání chybných přihlašovacích údajů? Je to řešení proti automatizovanému prolamování hesel?

ikona Jakub Vrána OpenID:

Rozhodně to není alternativa k používání pomalé hašovací funkce, protože když se někdo dostane přímo k datům, tak ho to nijak nezpomalí. A ani jako doplněk se mi to moc nelíbí, protože to zdržuje i legitimní uživatele. Spíš se kloním k upravení chování po vyšším počtu pokusů – např. po deseti neplatných pokusech začít vyžadovat vyplnění CAPTCHy.

jakub:

mam taku teoreticku otazku - PRECO ukladat hesla hashovane. prave riesim s kolegom, ktory spravuje docela velky projekt a uklada hesla v povodnom stave a nejak sa neviem argumentovat

PJ:

Lebo ak sa niekto dostane k DB, tak nehashovane hesla priamo vidi. Hashovanie a solenie hesiel ich ciastocne uchrani proti kompromitacii adminami a utocnikmi. Robit bruteforce na hashovane heslo je ovela narocnejsie, ako skusit, ze user@example.com ma naozaj prihlasovacie heslo aj do emailu P4ssW0rd. Vdaka soleniu je treba "lamat" kazde heslo zvlast a to sa pri velkej narocnosti popisovanej v tomto clanku dost pretiahne.

myšlekočklokanutrie:

Jakube, je podle tebe dostatečně bezpečné mít jen jednu „sůl“ pro všechny uživatele? nebo by měla být extra pro každý záznam?

ikona Jakub Vrána OpenID:

Extra pro každý záznam.

Radek:

Mozna by stalo za to vysvetlit proc. Smyslem soli je zajsitit, aby dve stejna hesla mela odlisny hash. Pokud bychom to nedelali, pak by prolomeni jednoho hesla jednoho uzivatele vedla k prozrazeni hesla jineho uzivatele (majiciho stejne heslo a tedy stejnou hash hesla). Pokud ale ke kazdemu heslu pridame jedinecny retezec (sul), pak i dve stejna hesla (s ruznou soli) vygeneruji ruzne hashe. Pokud je prolomeno jedno heslo, druhe zustane uchraneno (diky soli ma jiny hash). Sul tak musi byt skutecne jedinecna pro kazde heslo, ale nemusi byt tajna - znalost soli vyrazne nepomuze k prołomeni hashe.

myšlekočklokanutrie:

Ke tvému odstavci "Nejde jen o hesla" - jak teda chránit session id? Mít nějaký mechanismus, který by kontroloval i další přenášené informace jako IP, user agent, ...? Regenerovat session id při každém požadavku?

ikona Jakub Vrána OpenID:

Ne. Měl jsem tím na mysli, že session ID se na serveru vůbec nemusí ukládat v čitelné podobě, stačí jeho haš.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.