Obousměrné šifrování v PHP (5.3+) rubrika: Programování: PHP

4 hutlik007
položil/-a 22.8.2015

Dobrý den,

pracuji teď na projektu, v rámci kterého bych potřeboval bezpěčně ukládat data. Proto se chci zeptat, znáte nebo máte zkušenosti s nějakou obousměrně šifrovací metodou/knihovnou/funkcí v PHP?

Díky za odpovědi.

Komentáře

  • v6ak : Chtělo by to přesněji definovat vaše možnosti (je například možné přidat do PHP nové extension nebo volat shell?) i možnosti útočníka (může například data modifikovat, získat postupně přístup k několika různě starým kopiím atd., částečně ovlivňovat šifrovaná data apod.). Jinak můžete dostat falešný pocit bezpečí. 22.8.2015
  • hutlik007 : Používám Openshift, takže v případě potřeby by neměl být problém rozšíření přidat - maximální dostupná verze je ale 5.3. Účelem je šifrovat informace o relaci, které se ukládají na počítač klienta (ať už v session nebo cookie), proto je potřeba zajistit, aby nebylo možné je bez klíče snadno dešifrovat. 22.8.2015
  • ales_novak : V Openshift je i PHP 5.4 Cartridge 22.8.2015
  • v6ak : Session se typicky ukládá na server a na klienta se pošle jen session id. Ale záleží na implementaci, jsou i implementace, které session podepíšou a pošlou na klienta. Má to nějaké výhody, ale u takovýchto implementací je potřeba session případně šifrovat a nelze zabránit replay attacku. Pokud ale budete session mít na serveru, pak je otázka, jestli je šifrování opravdu potřeba. 22.8.2015
odkaz Vyřešeno
8 v6ak
odpověděl/-a 23.8.2015
 
upravil/-a 29.8.2015

Možná by se hodilo pro tento účel (šifrované cookies) najít přímo knihovnu. Nějakou aspoň trochu renomovanou. Klidně one-man-show, ale od někoho, kdo rozumí kryptografii. Pokud si to budete implementovat sami, pak:

  1. K šifrování: AES je dobrá volba, asi bude bohatě stačit i varianta s 128b klíčem, ale volba šifrování není otázka jenom šifry. U blokových šifer jako je AES se volí tzv. mode of operation (aby šlo zašifrovat zprávu delší než jeden blok) a padding (aby šlo zašifrovat zprávu délky, která není dělitelná velikostí bloku). Dále je potřeba mít inicializační vektor (díky tomu se stejná zpráva zašifruje pokaždé jinak a v některých modes of operation je to celkově dost zásadní). Je potřeba mít kvalitní (tzn. náhodně zvolený) klíč a ověřit, že útočník se zprávou nijak nemanipuloval.

  2. Mode of operation: U krátkých dat, která čteme celá najednou, to nemá až tak velký význam řešit, pokud dodržíte ostatní věci, které tu píšu. Vyhněte se ECB. Režim CBC je dobrá volba. U režimů OFB nebo CTR ušetříte několik bytů na paddingu (tyto režimy z blokové šifry dělají proudovou šifru) a taky by měly dobře posloužit, ale případné chyby jako je špatná volba inicializačního vektoru nebo absence autentizace tu mají poněkud větší dopad na bezpečnost.

  3. Padding: Řešíme jen u blokových šifer, u proudových šifer a proudových režimů (OFB, CTR) není potřeba. Knihovna mcrypt to řeší poněkud neobratně – do zašifrované zprávy v případě potřeby připojí na konec několik znaků '\0', aby se délka zprávy zarovnala na bloky. Pokud se tento znak v šifrovaných datech nevyskytuje, dá se použít rtrim($retval, "\0"), ale pokud se tam '\0' vyskytne na konci řetězce, funkce rtrim ořeže i to.

Pokud rtrim nevyhovuje, je možné implementovat různé varianty paddingu. Volba paddingu a jeho implementace sice může mít v některých případech vliv na bezpečnost, ale pokud zajistíme, že nám útočník nebude manipulovat s ciphertextem, vliv by tu být neměl.

  1. Inicializační vektor. Při šifrování jej předáváme šifrovací funkci, při dešifrování jej předáváme dešifrovací funkci. Když budeme volit pokaždé jiný IV, bude stejná zpráva zašifrována pokaždé úplně jinak. Zajistí také, že u dvou podobných zpráv nepůjde poznat, že jsou podobné. U proudových šifer a proudových režimů (např. CTR nebo OFB) tím znemožní některé zajímavé útoky pomocí operace xor.

Důležité je ovšem nenechat útočníka zvolit inicializační vektor a nenechat jej inicializační vektor předem uhodnout.

V případě mcryptu získáte délku IV pomocí https://secure.php.net/manual/en/function.mcrypt-enc-get-iv-size.php . Nový IV vygenerujete pomocí funkce https://secure.php.net/manual/en/function.mcrypt-create-iv.php . U starších verzí PHP (< 5.6) doporučuji zvolit variantu MCRYPT_DEV_URANDOM, jinak to v ostrém provozu může být někdy pomalé…

EDIT: Abych to upřesnil, i v PHP ≥ 5.6 je vhodné použít MCRYPT_DEV_URANDOM. Rozdíl je ale v tom, že v PHP ≥ 5.6 to není potřeba uvádět, protože od 5.6 je tato volba výchozí.

IV není potřeba šifrovat. Lze připojit k ciphertextu – klidně jako $iv.$ciphertext, protože před dešifrováním známe délku IV a můžeme to rozdělit funkcí substr. Kdybychom jej nepřipojili k ciphertextu, neuměli bychom pak zprávu rozšifrovat.

  1. Kvalitní klíč. To není až taková věda, ale i tady to lze pokazit:

$key = substr(md5('very secret key'), 0, $ks); // TAKTO NE! (zkopírováno z <a href="https://secure.php.net/manual/en/function.mcrypt-module-open.php"; title="https://secure.php.net/manual/en/function.mcrypt-module-open.php">https://secure.php.net/manual/en/function.mcrypt-module-open.php</a>; )

Generuje se klíč o velikosti $ks z textu 'very very secret'. Jsou zde špatně minimálně dvě věci:

  • Klíč jsou binární data. Výstup z funkce md5 jsou hexadecimální data. Tím ale máme poloviční účinnou délku klíče. V případě AES-128 dodáváme sice 128b klíč, ale jeho hodnotu vybíráme jen z 2^64 hodnot. Je sice fakt, že projít 2^64 různých hodnot není levné ani rychlé, ale možné to je. (Experimentálně se to povedlo cca přes 13 rokama.) Když už, tak spíš substr(md5('very secret key', true), 0, $ks), čímž vypneme konverzi do hexadecimálního a budeme volit klíč ze (snad) všech 2^128 hodnot.
  • Druhá špatná věc je 'very very secret'. Je to samozřejmě potřeba nahradit něčím dostatečně náhodným.
  • Nakonec k MD5: Pokud vyřešíme předchozí věci, pak by nám slabiny MD5 vadit neměly. Ale pokud bychom použili šifru s klíčem delším než 128 bitů, dostaneme krátký (a tedy neplatný!) klíč. Lepší je použít funkci, která dává delší výstup. Například hash('sha256', $data, true).
  1. Pokud šifrujete, typicky chcete zajistit, aby útočník nemohl šifrovanou zprávu (ani IV) měnit. Zabrání se tím mnoha různým útokům. (Nebudu všechny rozebírat úplně do detailů.) Šifrování totiž samo o sobě nezajistí, že útočník zprávu nepozměnil. U různých modes of operations blokových šifer má útočník různé možnosti, jak zprávu pozměnit, tomu se tu nebudu věnovat detailně. U proudových šifer jde o Bit-flipping attack, ale taky ho tu nebudu rozebírat do detailů. Jednak by útočník mohl cookie nějak pozměnit ve svůj prospěch a jednak by mohl dělat různé kulišárny, že by poškodil data, poslal to serveru ke zpracování a čekal, kde/kdy dojde k chybě. Vypadá to možná krkolomně, ale existují prakticky proveditelné útoky postavené na tomto. Jak to tedy vyřešit:
    • K zašifrované zprávě spočítáme její MAC (například HMAC). Pomůže nám třeba funkce https://secure.php.net/manual/en/function.hash-hmac.php .
    • Ideální je vzít inicializační vektor (vizte níže, v části o šifrování) a zprávu po zašifrování (tzn. $iv.$cryptotext) a z toho teprve počítat MAC. Jiný postup (například nejdříve připojím MAC a potom teprve zašifruju) dává větší prostor k útokům. Na opačný postup mimochodem skončilo SSL3, když se objevil POODLE.
    • Když chci přečíst cookie, nejdříve ji ověřím nějak takto:
      I. Spočítám MAC.
      II. Porovnám spočítanou MAC s MAC obdrženou od klienta. Abychom předešli útokům založeným na časování, doporučuji použít
      funkci hash_compare zmíněnou v komentářích na https://secure.php.net/manual/en/function.hash-hmac.php .
      III. Pokud se MAC neshodují, rovnou cookie zamítám a nic nedešifruju. Pokud se MAC shodují, rozdělím ověřený text do $iv a $cryptotext a dešifruju. Dešifrovací funkce by po mě měla chtít jak $ciphertext, tak $iv. Pokud by mi náhodou knihovna povolila nezadat IV a já na to zapomněl, dostanu změť znaků…

Takto nějak by to šlo implementovat s pomocí mcrypt. Přemýšlel jsem nad použitím GPG – asi by to šlo, mělo by to část ze zmíněných věcí vyřešit, ale nejspíš by tu byl větší overhead na velikost cookie, což může být problém.

EDIT: Nahoře jsem to už psal, ale tady to pro jistotu ještě zopakuju a doplním: Je potřeba počítat s tím, že bez stavu na serveru je možné provést https://en.wikipedia.org/wiki/Replay_attack . Někdy to vadit nemusí, jindy ano. Dá se udělat i jistý kompromis, kdy se do databáze přistupuje jen občas a replay attack lze provést jen po určitou omezenou dobu. Popsal jsem to tady: https://groups.google.com/d/msg/securesocial/K_MR2iySvTw/LAKyeEvGKgAJ

EDIT2: Ještě dodám, že šifrování nezakryje délku plaintextu. Pokud použiju padding, pak se délka plaintextu trošku zamlží, ale stále útočník dostává celkem přesnou informaci. S tím je dobré případně počítat. Není ale dobré to řešit kompresí – tím se sice délka trošku zamlží, ale dost dvojsečným způsobem, takže to může nadělat více škody než užitku. Nebudu rozebírat, ale můžete si vygooglit útok CRIME, popř. BREACH.

EDIT2: Upraveny informace o IV, aby to bylo jasnější.

Komentáře

  • vaclav : Skvěle sepsáno. Je vidět, že teorii bezpečnosti rozumíš. 23.8.2015
  • v6ak : Ještě jsem doplnil info o replay attack a upřesnil MCRYPT_DEV_URANDOM pro PHP ≥ 5.6. 24.8.2015
  • hutlik007 : Mockrát díky, když to tak sleduju, musím si o šifrování ještě hodně pozjišťovat. 24.8.2015
  • klapuch : Ahoj, v kryptografii se nevyznám, a proto bych se chtěl zeptat, proč děláš $iv.$ciphertext, když na straně dešifrování se vektoru opět zbavíš, protože znáš jeho délku. Utočník si přece může zjistit velikost vektoru také (podle algoritmu) a vektoru se zbavit a není to tedy žádná překážka, nebo se mýlím? Díky za odpověď. 28.8.2015
  • v6ak : @InsaneFacedown Díky za zpětnou vazbu. Popravdě mě trochu vyděsilo, že by se můj text dal pochopit takto. Ale když jsem si ho po sobě přečetl… Některé věci mi přišly až moc jasné a neuvědomil jsem si, že až tak jasné být nemusí. Už jsem to upravil – když si bod 4 přečteš znovu (po úpravě), je to teď jasnější? (BTW, pokud by ses to pokusil implementovat, nejspíš bys to pochopil. Chápu ale, že to chceš nejdříve celé pochopit a potom teprve implementovat. Chvílím, že nad tím přemýšlíš.) 29.8.2015
  • v6ak : A doplnil jsem taky na konec informaci o tom, že šifrování neskryje před útočníkem délku textu. Plus ještě informaci o tom, že komprese nemusí být dobrý nápad. 29.8.2015
  • klapuch : Moc ti děkuji za doplňující informaci, pomohla mi, teď je mi už vše jasné. :) 29.8.2015
  • v6ak : Dobře mě doplnil spazef0rze – existuje tu knihovna pro PHP 5.4, která řeší velkou část z toho, co jsem tu psal. (Kdybych ji byl býval znal, tak bych ji taky byl doporučil.) Je ale stále potřeba pamatovat na replay attack a na to, že šifrování neskrývá délku a že komprimovat data před šifrováním může být problém. Taky je dobré vědět, jak zvolit klíč. Ale i tak ta knihovna nemalou část toho, co tu píšu, dělá za vás, což je určitě plus. A i já hodnotím kladně použití OpenSSL místo mcryptu. 31.8.2015

Pro zobrazení všech 3 odpovědí se prosím přihlaste:

Rychlé přihlášení přes sociální sítě:

Nebo se přihlaste jménem a heslem:

Zadejte prosím svou e-mailovou adresu.
Zadejte své heslo.