Trvalé přihlášení bez ukládání dat

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

Nedávno jsem psal o tom, jak zajistit trvalé přihlášení do webové aplikace. Stačí si na serveru uložit náhodný řetězec a ten poslat klientovi v trvalé cookie. Jak však postupovat, pokud si na serveru data ukládat nemůžeme?

Určitě už jste uhodli, proč mě to zajímá. Adminer je sice nástroj pro správu databáze, do této databáze si ale nemůže nic ukládat. Stejně tak si nemůže nic zapisovat ani do souboru na disk, protože nemusí mít nikam právo zápisu.

Heslo si v tomto případě musíme uložit přímo do cookie, rozhodně ho tam ale nebudeme ukládat v otevřeném textu, že ne? Musíme ho zašifrovat nějakým klíčem, kde ale tento klíč vzít? Pokud by byl uložen přímo v aplikaci, tak by to bylo skoro stejné, jako kdyby bylo heslo uložené v otevřeném textu, protože by si ho mohl kdokoliv rozšifrovat. Každá instalace Admineru tedy musí mít vlastní heslo, jak to ale zařídit? Modifikovat stahovaný soubor tak, aby měl každý jiný klíč, by nebylo nejšťastnější už jenom proto, že někdo svoji verzi občas vezme a nabídne ji dál ke stažení (třeba autoři pluginů). Klíč tedy bude vhodné definovat až v přizpůsobení. Jak ale lidem vysvětlit, že na dané místo mají zadat svůj vlastní unikátní klíč? Mohl by na to být nějaký generátor, většina lidí ale prostě zkopíruje ukázku z webu a dále si ji upraví. Vyřešil jsem to tak, že je tato ukázka dynamická a každému uživateli vygeneruje jeho vlastní klíč.

Technická implementace

Když už máme klíč, můžeme ho zkombinovat s heslem. To můžeme udělat třeba operátorem ^ (bitový XOR). Pokud je klíč alespoň tak dlouhý jako heslo, jde o aplikaci nerozluštitelné Vernamovy šifry. Tak jednoduché to ale není.

V první řadě – pokud zná uživatel heslo i jeho zašifrovanou podobu, tak si může vypočítat klíč a pomocí něj rozluštit i ostatní uložená hesla. Pro každého uživatele tedy budeme používat jeho vlastní klíč získaný třeba operací sha1($username . $key, true). Alternativou by bylo používat asymetrickou kryptografii.

Za druhé – zašifrovaný text je stejně dlouhý jako heslo, takže by útočník zjistil délku hesla, což by ho mohlo nalákat k útoku hrubou silou, pokud by heslo bylo krátké. Místo hesla tedy budeme ukládat např. strlen($password) . ":" . str_pad($password, 17), což ochrání krátká hesla.

<?php
/** Zašifrování hesla
* @param string heslo v čitelném textu
* @param string binární klíč, měl by být delší než $password
* @return string binární šifra
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function cipher_password($password, $key) {
    $password2 = strlen($password) . ":" . str_pad($password, 17);
    $repeat = ceil(strlen($password2) / strlen($key));
    return $password2 ^ str_repeat($key, $repeat);
}

/** Rozšifrování hesla
* @param string binární šifra
* @param string binární klíč
* @return string heslo v čitelném textu
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function decipher_password($cipher, $key) {
    $repeat = ceil(strlen($cipher) / strlen($key));
    $password2 = $cipher ^ str_repeat($key, $repeat);
    list($length, $password) = explode(":", $password2, 2);
    return substr($password, 0, $length);
}

$key = md5(uniqid(mt_rand(), true));
$username = "root";
$password = "heslo";
$username_key = sha1($username . $key, true);
$cipher = cipher_password($password, $username_key);
echo decipher_password($cipher, $username_key) . "\n";
?>
Jakub Vrána, Řešení problému, 21.12.2009, diskuse: 16 (nové: 0)

Diskuse

ikona pepak:

1) Neškodilo by do článku explicitně napsat, které údaje je tedy třeba poslat uživateli (při přihlášení), resp. je od něj načíst (při autentizaci) - že jde o proměnné $username_key a $cipher.

2) Pořád bych se přimlouval za použití nějaké fixní hodnoty uložené ve skriptu - v současném stavu má uživatel všechny informace pro to, aby mohl zašifrovanou informaci pozměnit. U jména a hesla to ještě je celkem jedno, ale co když budu potřebovat uložit ještě něco, například oprávnění? Majitel webu by měl mít možnost tomu zabránit (když jí nevyužije, jeho problém).

3) Kromě toho - není mi úplně jasné, co vlastně má to šifrování zajišťovat - bez té tajné informace je to v podstatě stejně bezpečné, jako kdyby se to jméno ukládalo v plaintextu (každý, kdo získá autentizační cookie, ho dokáže dešifrovat).

4) Prodloužení hesel pomocí str_pad je z bezpečnostního hlediska úplně k ničemu, pokud nebudou parametry str_padu tajné a pokud možno pro každého uživatele jiné - brute force je jen otázkou modifikování algoritmu (nepoužívat tupé zkoušení všech znaků, ale po vygenerování stingu použít stejnou str_pad operaci pro jeho prodloužení).

5) Technická - Vernamova šifra má víc předpokladů než jen to, že její klíč je stejně dlouhý jako otevřený text.

6) Přímo mě děsí použití tohoto šifrovacího algoritmu. Je nějaké zásadní důvod, proč nepoužít třeba Blowfish (malý, rychlý a bezpečný)?

ikona Jakub Vrána OpenID:

1. Nikoliv, poslat a přijmout je potřeba $username a $cipher.

2. Datům od uživatele nepotřebuji věřit, zachází se s nimi stejně jako kdyby je vyplnil do formuláře. Pokud by jim bylo potřeba věřit, bylo by možno je doplnit o zašifrovaný otisk. Vzhledem k tomu, že se uživateli neposílá $username_key, ale jen $username, tak je to nasnadě.

3. Mýlíš se proto, že jsi usoudil, že se k uživateli posílá $username_key.

4. Ale tady nejde o nějaké dodatečné zabezpečení hesla. Tady jde o to, aby útočník nepoznal, že je heslo krátké a tudíž snadno prolomitelné. Ať už bude padding náhodný nebo mezerová, bezpečnost slabého hesla to nezvýší.

5. Ano a všechny předopklady jsou splněny. Kromě dost dlouhého klíče to je požadavek na náhodnost klíče (zajištěn náhodným vygenerováním hlavního klíče) a jeho neopakováním (zajištěn připojením $username).

6. Na dostupnost Blowfish v PHP se nedá spolehnout a uživatelská implementace by byla podstatně delší. Souhlasím ale, že bez nějakých omezení by to bylo lepší řešení.

ikona pepak:

1) Tak to by mě moc zajímalo, jak z $username a $cipher dostanu $password. Já k tomu žádnou cestu nevidím, ale třeba jen blbě koukám. Mohl bys podrobněji popsat, jakým postupem dostaneš ze znalosti $username a $cipher ostatní údaje?

3) Myslím, že se nemýlím. Tvrdíš, že z $username a $cipher dokážeš dešifrovat ostatní. Já si to sice nemyslím (viz bod 1), ale dejme tomu - ale v tom případě mi vysvětli, proč by stejný postup nemohl použít útočník.

4) Čili se bavíme o "security by obscurity" u open-source implementace?

5) No, já si myslím, že když je "náhodnost zajištěna vygenerováním", tak jsou předpoklady Vernamovy šifry porušeny. Ale to není pro tento případ podstatné, takže to toho nebudu dál šťourat.

ikona Jakub Vrána OpenID:

1. Na serveru je uložený $key. Z něho si pomocí $username vypočteme $username_key a z toho si pomocí $cipher odvodíme $password.

3. Útočník nezná $key.

4. Ne. Pokud má uživatel slabé heslo, tak mu ho trvalým přihlášením nemáme jak zesílit. Opatření slouží jenom k tomu, abychom slabost hesla zbytečně nedávali na odiv.

5. Generování klíče může využívat skutečný zdroj náhody, viz http://php.vrana.cz/nahodna-cisla.php, předpoklad tedy porušen není. Ukázka pro jednoduchost využívá pseudonáhodnost.

ikona pepak:

1) Aha. Tak to mě zmátlo to tvoje "... do této databáze si ale nemůže nic ukládat. Stejně tak si nemůže nic zapisovat ani do souboru na disk, protože nemusí mít nikam právo zápisu." - vycházel jsem z toho, že nikde nic uloženého nemáš, tedy ani $key.

ikona Jakub Vrána OpenID:

Aplikace ale $key skutečně nikam nezapisuje. Ten je přímo součástí zdrojových kódů. Ty ovšem musí být pro každou instalaci jiné, což je v článku také poměrně podrobně rozebráno.

Prdlořeznictví Krkovička, n. p.:

Jen pro orientaci - ten řádek:

$key = md5(uniqid(mt_rand(), true));

* není uvnitř Admineru ani Editoru ani rozšíření
* slouží jenom pro uvedený příklad
* v rozšíření Adminu bude výraz md5(uniqid(mt_rand(), true)) nahrazeno nějakým literálem (explicitně napsanou hodnotou)

Říkám to správně?

ikona Jakub Vrána OpenID:

Přesně tak.

ikona Jakub Vrána OpenID:

4. Abych to ještě k něčemu připodobnil: Při zadávání hesla ve Windows se zobrazují puntíky, takže když někdo vidí na monitor, tak může zjistit, kolik znaků heslo má, což ho může nalákat k útoku. Na Linuxu se nezobrazuje nic. Tak zhruba stejný význam má použití str_pad.

Martin:

Já to věděl! Každý neumětel se jednoho dne pustí do tajemných vod bezpečnosti. Jak to dopadá, vidíte v článku!

rsvanda:

Martine, nemám důvod Vám nevěřit, ale můžete ukázat KDE konkrétně to "dopadlo"?

ikona David Grudl:

Emkei?

PHX:

A co vyuzit nejake informace o stroji? Od URL, domeny, velikost HDD, RAM, verze PHP, atd. + uzivatelsky definovany klic. To byzajistilo unikatnost ruznych instanci s identickym (defaultnim) klicem ve zdrojaku.

Jakub Šulák:

To neni dobry zpusob, pokud budu mit aplikaci na stejnem hostingu, budeme mi stejny klic.

dro:

Mě se aplikace moc líbí.

Prosím,

"Zásadním rozdílem je ale to, že Adminer před provedením dlouhotrvající operace odemkne session, čímž uživateli dovolí s aplikací pracovat v jiném panelu prohlížeče."

jak to udělám, v PHP? Mě se to nikdy nedařilo, díky.

ikona Jakub Vrána OpenID:

To asi spíš patří k článku http://php.vrana.cz/phpmyadmin-vs-adminer.php.

Session se odemyká funkcí session_write_close. Pravý oříšek je ale ji pak zase zamknout.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 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.