nedávno jsem se zúčastnil Laracon EU 2018, kde Marcus Bointona dal velký rozhovor o Šifrování v PHP 7.2. Nechal jsem mluvit s mnohem větší porozumění pro to, jak obrovsky složité kryptografii, ale také na to, jak PHP dělá šifrování dostupnější díky zavedení Sodíku. Šifrování dat v PHP je něco, na čem jsem pracoval v rámci své práce na SpinupWP, takže jsem si myslel, že je čas, abych sdílel několik poznatků. Připoutejte se, protože by to mohla být hrbolatá jízda!
typy šifrování
dnes se používá řada různých šifrovacích metod, z nichž nejčastější je hašování, šifrování tajného klíče a šifrování veřejného klíče. Každá metoda šifrování má navíc na výběr více algoritmů nebo šifer(každý s vlastními silnými a slabými stránkami). V tomto článku se zaměříme na implementaci hašování a šifrování tajných klíčů.
hashování
hashovací algoritmus má vstupní hodnotu a transformuje ji na Přehled zpráv. Stručně řečeno, hodnoty prostého textu jsou transformovány na hash s pevnou délkou a lze je ověřit pouze předáním původní hodnoty algoritmu hashování. Díky tomu je hashing ideální pro ukládání uživatelských hesel.
stojí za zmínku, že hashování není neprůstřelným řešením a ne všechny hashovací algoritmy jsou stejné. Zvažte MD5 a SHA1, které jsou rychlé a efektivní, což je ideální pro kontrolní součet a ověření souborů. Jejich rychlost je však činí nevhodnými pro hašování hesla uživatele. S dnešní výpočetní silou moderních GPU, heslo může být popraskané hrubou silou během několika minut, odhalující původní prostý text heslo. Místo toho by měly být použity záměrně pomalejší hashovací algoritmy, jako je bcrypt nebo Argon2.
Zatímco hash hesla generované algoritmus bude jistě zakrýt původní data a zpomalit by útočník, my jako vývojáři by se měli snažit používat nejsilnější algoritmus k dispozici. Naštěstí to PHP usnadňuje díky password_hash()
.
$hash = password_hash($password, PASSWORD_DEFAULT);
funkce password_hash()
používá nejen bezpečný jednosměrný hashovací algoritmus, ale automaticky zpracovává sůl a zabraňuje časovým útokům na bočních kanálech. Od PHP 5.5 bude bcrypt použit ke generování hash, ale to se v budoucnu změní, protože do PHP budou přidány novější a bezpečnější hashovací algoritmy. Argon2 je pravděpodobné, aby se stal příštím výchozí hashovací algoritmus a může být použit dnes (v PHP 7.2) předáním PASSWORD_ARGON2I
vlajky místo PASSWORD_DEFAULT
.
ověření hesla uživatele je také triviální proces díky funkci password_verify()
. Jednoduše předat prostý text heslo dodané uživatelem a porovnat jej s uloženým hash, jako tak:
if (password_verify($password, $hash)) { echo "Let me in, I'm genuine!";}
Všimněte si, jak se provádí ověření hesla v PHP. Pokud jste ukládání uživatelských pověření v databázi, může být sklon k hash hesla, zadané při přihlášení a pak provést databázový dotaz, jako tak:
SELECT * FROM usersWHERE username = 'Ashley'AND password = 'password_hash'LIMIT 1;
Tento přístup je citlivý na side-channel útoky a měla by být vyhnout. Místo toho vraťte uživatele a zkontrolujte heslo hash v PHP.
SELECT username, password FROM usersWHERE username = 'Ashley'LIMIT 1;
Při zatřiďování je skvělé pro ukládání hesla uživatele, to nebude pracovat pro libovolná data, která naše aplikace potřebuje přístup bez zásahu uživatele. Zvažme fakturační aplikaci, která šifruje informace o kreditní kartě uživatele. Každý měsíc musí naše aplikace účtovat uživateli za použití předchozího měsíce. Hash údaje o kreditní kartě nebude fungovat, protože to vyžaduje, aby naše aplikace zná původní data pro něj načíst v prostém textu.
šifrování tajného klíče k záchraně!
šifrování tajného klíče
šifrování tajného klíče (nebo symetrické šifrování, jak je také známo) používá jediný klíč k šifrování i dešifrování dat. Podívejme se, jak bychom implementovali takový mechanismus pomocí sodíku, který byl zaveden v PHP 7.2. Pokud používáte starší verzi PHP, můžete nainstalovat sodium přes PECL.
nejprve potřebujeme šifrovací klíč, který lze vygenerovat pomocí funkce random_bytes()
. Obvykle to uděláte pouze jednou a uložíte jej jako proměnnou prostředí. Nezapomeňte, že tento klíč musí být za každou cenu utajen. Jakmile je klíč ohrožen, jsou také šifrovaná data.
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
Chcete-li zašifrovat hodnotu, předáme ji sodium_crypto_secretbox()
pomocí našeho $key
a $nonce
. Nonce je generován pomocí random_bytes()
, protože stejné nonce by nikdy nemělo být znovu použito.
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);$ciphertext = sodium_crypto_secretbox('This is a secret!', $nonce, $key);
to představuje problém, protože potřebujeme nonce dešifrovat hodnotu později. Naštěstí tentokrát nemuseli být drženy v tajnosti, takže se můžeme předřadit to, aby naše $ciphertext
base64_encode()
hodnotu před uložením do databáze.
$encoded = base64_encode($nonce . $ciphertext);var_dump($encoded);// string 'v6KhzRACVfUCyJKCGQF4VNoPXYfeFY+/pyRZcixz4x/0jLJOo+RbeGBTiZudMLEO7aRvg44HRecC' (length=76)
pokud jde o dešifrování hodnoty, děláme opak.
$decoded = base64_decode($encoded);
protože známe délku nonce, můžeme ji extrahovat pomocí mb_substr()
před dešifrováním hodnoty.
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);var_dump($plaintext);// string 'This is a secret!' (length=17)
to je vše, co je tajné šifrování klíčů v PHP, díky sodíku!
šifrování obálky
zatímco výše uvedený přístup je jistě krokem správným směrem, stále ponechává naše data zranitelná, pokud je tajný klíč ohrožen. Uvažujme o škodlivém uživateli, který získá přístup k serveru, který je hostitelem naší aplikace. V tomto scénáři je pravděpodobné, že útočník bude schopen objevit náš tajný klíč, který jsme použili k šifrování dat. Tím jsou naše data zcela vystavena.
jednoduchým řešením je neukládat náš tajný klíč na stejném místě jako šifrovaná data, ale to představuje problém. Jak zašifrujeme a dešifrujeme na vyžádání? Zadejte službu správy klíčů Google Cloud (Cloud KMS).
Cloud KMS je služba poskytovaná společností Google pro bezpečné hostování kryptografických klíčů. Poskytuje řadu užitečných funkcí kolem úložiště klíčů, včetně automatického otáčení klíčů a zpožděného zničení klíčů. V tomto příkladu se však primárně zabýváme ukládáním tajného klíče mimo naše data.
aby se věci bezpečnější budeme používat techniku známou jako envelope encryption. Šifrování obálky v podstatě zahrnuje šifrování klíčů jiným klíčem. Děláme to ze dvou důvodů:
- Cloud KM má limit velikosti 64 KiB na data, která mohou být šifrována a dešifrována. Proto nemusí být možné odeslat všechna data najednou.
- ještě důležitější je, že nechceme zasílat naše citlivá data v prostém textu třetí straně, bez ohledu na to, jak důvěryhodná se mohou zdát.
Místo odeslání naše holého data na Cloud KM, se chystáme vytvořit jedinečný šifrovací klíč pokaždé, když jsme se napsat citlivá data do databáze. Tento klíč je známý jako klíč pro šifrování dat (DEK), který bude použit k šifrování našich dat. Dek je poté odeslán do Cloud KMS, aby byl šifrován, což vrací šifrovací klíč (známý jako KEK). Nakonec je KEK uložen vedle sebe v databázi vedle šifrovaných dat a DEK je zničen. Proces vypadá takto:
- Generovat jedinečný šifrovací klíč (DEK)
- Zašifrovat data pomocí tajného klíče šifrování
- Odeslat jedinečný šifrovací klíč (DEK) do Cloudu KMS pro šifrování, který se vrací KEK
- Ukládat šifrovaná data a šifrovací klíč (KEK) side-by-side
- Zničit generovaný klíč (DEK)
Při dešifrování dat proces je obrácen:
- Načíst šifrovaná data a šifrovací klíč (KEK) z databáze
- Odeslat KEK Cloud KMS pro dešifrování, který vrací DEK
- Použít DEK dešifrovat šifrovaných dat
- Zničit DEK
S tímto na mysli jsem vytvořil velmi jednoduchý helper class for performing obálky šifrování. Nehodlám jít přes kroky v rámci služby Google Cloud konzole, jako Quickstart a Ověřování vodítka nastínit vše, co potřebujete, abyste mohli začít. Kvůli stručnosti neexistuje žádná manipulace s chybami atd. v tomto příkladu.
<?phpuse Google_Service_CloudKMS as Kms;use Google_Service_CloudKMS_DecryptRequest as DecryptRequest;use Google_Service_CloudKMS_EncryptRequest as EncryptRequest;class KeyManager{ private $kms; private $encryptRequest; private $decryptRequest; private $projectId; private $locationId; private $keyRingId; private $cryptoKeyId; public function __construct(Kms $kms, EncryptRequest $encryptRequest, DecryptRequest $decryptRequest, $projectId, $locationId, $keyRingId, $cryptoKeyId) { $this->kms = $kms; $this->encryptRequest = $encryptRequest; $this->decryptRequest = $decryptRequest; $this->projectId = $projectId; $this->locationId = $locationId; $this->keyRingId = $keyRingId; $this->cryptoKeyId = $cryptoKeyId; } public function encrypt($data) { $key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $ciphertext = sodium_crypto_secretbox($data, $nonce, $key); return ; } public function decrypt($secret, $data) { $decoded = base64_decode($data); $key = $this->decryptSecret($secret); $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); return sodium_crypto_secretbox_open($ciphertext, $nonce, $key); } private function encryptKey($key) { $this->encryptRequest->setPlaintext(base64_encode($key)); $response = $this->kms->projects_locations_keyRings_cryptoKeys->encrypt( $this->getResourceName(), $this->encryptRequest ); return $response; } private function decryptSecret($secret) { $this->decryptRequest->setCiphertext($secret); $response = $this->kms->projects_locations_keyRings_cryptoKeys->decrypt( $this->getResourceName(), $this->decryptRequest ); return base64_decode($response); } private function getResourceName() { return sprintf( 'projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s', $this->projectId, $this->locationId, $this->keyRingId, $this->cryptoKeyId ); }}
všimněte Si, že skutečné šifrování a dešifrování metody jsou téměř totožné s tajný klíč, provádění představil výše. Rozdíl je však v tom, že nyní používáme více šifrovacích klíčů. Podívejme se na třídu pomocníků v akci. Budete muset poskytnout své $projectId
, $locationId
, $keyRingId
a $cryptoKeyId
, které jsou k dispozici v konzole Google Cloud.
<?phpuse Google_Service_CloudKMS as Kms;use Google_Service_CloudKMS_DecryptRequest as DecryptRequest;use Google_Service_CloudKMS_EncryptRequest as EncryptRequest;$client = new Google_Client();$client->setAuthConfig(getenv('GOOGLE_CREDENTIALS_FILE'));$client->addScope('https://www.googleapis.com/auth/cloud-platform');$keyManager = new KeyManager( new Kms($client), new EncryptRequest(), new DecryptRequest(), $projectId, $locationId, $keyRingId, $cryptoKeyId);$encrypted = $keyManager->encrypt('This is a secret!');var_dump($encrypted);// array (size=2)// 'data' => string 'uKjmEU7e1JEU+2vL3hBK2wBk6afCSgb+Y4GQtu/mmLuffgHlnqxnqOMPOI6WGkM18vAGGvFVDTvd' (length=76)// 'secret' => string 'CiQAdA0emUW2nhlU3RijX/5GnUsTnPPrQdLZNxdHWXWYugx49a4SSQBHyYr0T/PEbKwyFhIkaZl28oKkJRkXqNcqOL4Z+OTQFLpGvS6zCDt2mFn/nUQ/bi4znD4DORk9ZDTqiIBK3UNFUZcrXvoExds=' (length=152)$decrypted = $keyManager->decrypt($encrypted, $encrypted);var_dump($decrypted);// string 'This is a secret!' (length=17)
pokud by útočník ohrozil náš systém, mohl by také získat naše pověření API pro Cloud KMS? V závislosti na použité metodě ověřování pak ano, může to být možnost. Pokud tomu tak je, možná vás zajímá, jak je šifrování obálky bezpečnější než běžné šifrování tajných klíčů? Klíčový rozdíl (slovní hříčka) spočívá v tom, že přístup API může být zrušen, a tím zmařit útočníka, který je vyroben s vašimi citlivými daty. Je to ekvivalent výměny zámků, pokud vám někdo ukradne klíč od domu. S pravidelným šifrováním tajného klíče, kde je ohrožen jeden místní klíč, nemáte tento luxus. Útočník má po celou dobu na světě dešifrovat vaše citlivá data.
Balení
zabezpečení Dat a šifrování jsou obrovské předměty a já jsem se vztahuje jen několik způsobů, jak chránit citlivá data pomocí PHP. (Dříve jsme psali o ochraně tohoto druhu dat ve vašem místním prostředí)