am participat recent Laracon EU 2018 unde Marcus Bointon a ținut o discuție grozavă despre cripto în PHP 7.2. Am părăsit discuția având o apreciere mult mai mare pentru cât de complicată este criptografia, dar și pentru modul în care PHP face criptarea mai accesibilă datorită introducerii sodiului. Criptarea datelor în PHP este ceva la care am lucrat ca parte a muncii mele pe SpinupWP, așa că am crezut că este timpul să împărtășesc câteva informații. Cataramă, pentru că acest lucru ar putea fi o plimbare accidentat!
tipuri de criptare
există o serie de metode de criptare diferite utilizate astăzi, cele mai frecvente fiind hashing, criptarea cheilor secrete și criptarea cheilor publice. În plus, fiecare metodă de criptare are mai mulți algoritmi sau cifre din care să aleagă (fiecare cu propriile puncte forte și puncte slabe). În acest articol ne vom concentra pe implementarea criptării hash și a cheilor secrete.
Hashing
un algoritm de hashing ia o valoare de intrare și o transformă într-un rezumat de mesaje. Pe scurt, valorile plaintext sunt transformate într-un hash cu lungime fixă și pot fi validate numai prin trecerea valorii originale la algoritmul de hashing. Acest lucru face ca hashing-ul să fie perfect pentru stocarea parolelor utilizatorilor.
este demn de remarcat faptul că hashing-ul nu este o soluție antiglonț și nu toți algoritmii de hashing sunt egali. Luați în considerare MD5 și SHA1, care sunt rapide și eficiente, ceea ce le face ideale pentru verificarea și verificarea fișierelor. Cu toate acestea, viteza lor le face improprii pentru hashing parola unui utilizator. Cu puterea computațională de astăzi a GPU-urilor moderne, o parolă poate fi spartă prin forță brută în câteva minute, dezvăluind parola originală plaintext. În schimb, ar trebui utilizați algoritmi de hashing intenționat mai lenți, cum ar fi Bcrypt sau Argon2.
în timp ce o parolă hash generată de orice algoritm va ascunde cu siguranță datele originale și va încetini orice atacator potențial, noi, ca dezvoltatori, ar trebui să ne străduim să folosim cel mai puternic algoritm disponibil. Din fericire, PHP face acest lucru ușor datorită password_hash()
.
$hash = password_hash($password, PASSWORD_DEFAULT);
funcția password_hash()
nu numai că folosește un algoritm sigur de hashing unidirecțional, dar gestionează automat sare și previne atacurile pe canale laterale bazate pe timp. Începând cu PHP 5.5, bcrypt va fi folosit pentru a genera hash-ul, dar acest lucru se va schimba în viitor, pe măsură ce algoritmi de hash mai noi și mai siguri sunt adăugați la PHP. Argon2 este probabil să devină următorul algoritm implicit de hashing și poate fi folosit astăzi (pe PHP 7.2) prin trecerea steagului PASSWORD_ARGON2I
în loc de PASSWORD_DEFAULT
.
verificarea parolei unui utilizator este, de asemenea, un proces banal datorită funcției password_verify()
. Pur și simplu treceți parola plaintext furnizată de utilizator și comparați-o cu hash-ul stocat, așa:
if (password_verify($password, $hash)) { echo "Let me in, I'm genuine!";}
observați cum se efectuează verificarea parolei în PHP. Dacă stocați acreditările unui utilizator într-o bază de date, este posibil să fiți înclinat să hash parola introdusă la conectare și apoi să efectuați o interogare a bazei de date, astfel:
SELECT * FROM usersWHERE username = 'Ashley'AND password = 'password_hash'LIMIT 1;
această abordare este susceptibilă la atacuri cu canale laterale și ar trebui evitată. În schimb, returnați utilizatorul și apoi verificați hash-ul parolei în PHP.
SELECT username, password FROM usersWHERE username = 'Ashley'LIMIT 1;
în timp ce hashing-ul este excelent pentru stocarea parolei unui utilizator, nu funcționează pentru date arbitrare pe care aplicația noastră trebuie să le acceseze fără intervenția utilizatorului. Să luăm în considerare o aplicație de facturare, care criptează informațiile cardului de credit al unui utilizator. În fiecare lună, aplicația noastră trebuie să factureze utilizatorul pentru utilizarea lunii anterioare. Hashing datele cardului de credit nu va funcționa, deoarece necesită ca aplicația noastră cunoaște datele originale pentru a prelua în text clar.
criptare cheie secretă pentru salvare!
criptarea cheii secrete
criptarea cheii secrete (sau criptarea simetrică așa cum este cunoscută și) folosește o singură cheie atât pentru criptarea, cât și pentru decriptarea datelor. Să vedem cum vom implementa un astfel de mecanism folosind sodiu, care a fost introdus în PHP 7.2. Dacă rulați o versiune mai veche de PHP, puteți instala sodium prin PECL.
mai întâi avem nevoie de o cheie de criptare, care poate fi generată folosind funcția random_bytes()
. De obicei, veți face acest lucru o singură dată și îl veți stoca ca variabilă de mediu. Amintiți-vă că această cheie trebuie păstrată secretă cu orice preț. Odată ce cheia este compromisă, la fel și orice date criptate.
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
pentru a cripta valoarea, o transmitem la sodium_crypto_secretbox()
cu $key
și $nonce
. Nonce este generat folosind random_bytes()
, deoarece același nonce nu ar trebui niciodată reutilizat.
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);$ciphertext = sodium_crypto_secretbox('This is a secret!', $nonce, $key);
aceasta prezintă o problemă, deoarece avem nevoie de nonce pentru a decripta valoarea mai târziu. Din fericire, nonces nu trebuie să fie păstrate secrete, astfel încât să putem prepend-o la $ciphertext
apoi base64_encode()
valoarea noastră înainte de ao salva în baza de date.
$encoded = base64_encode($nonce . $ciphertext);var_dump($encoded);// string 'v6KhzRACVfUCyJKCGQF4VNoPXYfeFY+/pyRZcixz4x/0jLJOo+RbeGBTiZudMLEO7aRvg44HRecC' (length=76)
când vine vorba de decriptarea valorii, facem contrariul.
$decoded = base64_decode($encoded);
pentru că știm lungimea nonce putem extrage folosind mb_substr()
înainte de a decripta valoarea.
$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)
asta e tot ce este de criptare cheie secretă în PHP, datorită sodiu!
encryption Envelope
în timp ce abordarea prezentată mai sus este cu siguranță un pas în direcția cea bună, ea lasă în continuare datele noastre vulnerabile dacă cheia secretă este compromisă. Să luăm în considerare un utilizator rău intenționat care obține acces la serverul care găzduiește aplicația noastră. În acest scenariu, este posibil ca atacatorul să poată descoperi cheia noastră secretă pe care am folosit-o pentru a cripta datele. Acest lucru lasă datele noastre complet expuse.
soluția simplă este să nu stocăm cheia noastră secretă în aceeași locație cu datele criptate, dar aceasta prezintă o problemă. Cum criptăm și decriptăm la cerere? Introduceți serviciul de gestionare a cheilor Google Cloud (Cloud KMS).
Cloud KMS este un serviciu furnizat de Google pentru găzduirea în siguranță a cheilor criptografice. Oferă o varietate de caracteristici utile în jurul stocării cheilor, inclusiv rotirea automată a cheilor și distrugerea cheilor întârziate. Cu toate acestea, în acest exemplu, suntem preocupați în primul rând de stocarea cheii noastre secrete departe de datele noastre.
pentru a face lucrurile mai sigure, vom folosi o tehnică cunoscută sub numele de criptare plic. În esență, criptarea plicului implică criptarea cheilor cu o altă cheie. Facem acest lucru din două motive:
- cloud KMS are o limită de dimensiune de 64 KiB pe datele care pot fi criptate și decriptate. Prin urmare, este posibil să nu fie posibil să trimiteți toate datele dintr-o singură lovitură.
- mai important, nu dorim să trimitem datele noastre sensibile în text către o terță parte, indiferent de cât de demne de încredere pot părea.
în loc să trimitem datele noastre text în Cloud KMS, vom genera o cheie de criptare unică de fiecare dată când scriem date sensibile în baza de date. Această cheie este cunoscută sub numele de cheie de criptare a datelor (dek), care va fi utilizată pentru criptarea datelor noastre. DEK este apoi trimis la cloud KMS pentru a fi criptat, care returnează o cheie de criptare a cheii (cunoscută sub numele de KEK). În cele din urmă, KEK este stocat side-by-side în baza de date de lângă datele criptate și DEK este distrus. Procesul arată așa:
- generați o cheie de criptare unică (DEK)
- criptați datele folosind criptarea cheii secrete
- trimiteți cheia de criptare unică (Dek) către cloud KMS pentru criptare, care returnează KEK
- stocați datele criptate și cheia criptată (KEK) una lângă alta
- distrugeți cheia generată (DEK)
când decriptarea datelor procesul este inversat:
- preluați datele criptate și cheia criptată (KEK) din Baza de date
- trimiteți KEK în cloud KMS pentru decriptare, care returnează Dek
- utilizați Dek pentru a decripta datele noastre criptate
- distrugeți DEK
având în vedere acest lucru, am creat o idee foarte bună clasa helper simplu pentru efectuarea de criptare plic. Nu voi trece peste pașii necesari în Consola Google Cloud, deoarece ghidurile de pornire rapidă și autentificare conturează tot ce aveți nevoie pentru a începe. Din motive de concizie nu există nici o eroare de manipulare etc. în acest exemplu.
<?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 ); }}
veți observa că metodele reale de criptare și decriptare sunt aproape identice cu implementarea cheii secrete introdusă mai sus. Diferența este însă că acum folosim mai multe chei de criptare. Să vedem clasa de ajutor în acțiune. Va trebui să vă furnizați $projectId
, $locationId
, $keyRingId
și $cryptoKeyId
care sunt disponibile din Consola 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)
dacă un atacator ne-ar compromite sistemul, ar putea să obțină și acreditările noastre API pentru cloud KMS? În funcție de metoda de autentificare utilizată, atunci da poate fi o posibilitate. Dacă acesta este cazul, s-ar putea să vă întrebați cum criptarea plicului este mai sigură decât criptarea obișnuită a cheilor secrete? Diferența cheie (joc de cuvinte destinate) este că accesul API poate fi revocat, zădărnicind astfel un atacator care a făcut off cu datele sensibile. Este echivalentul schimbării încuietorilor dacă cineva îți fură cheia casei. Cu criptare regulat cheie secretă în cazul în care o singură cheie locală este compromisă nu aveți acest lux. Atacatorul are tot timpul din lume pentru a decripta datele dvs. sensibile.
împachetarea
securitatea și criptarea datelor sunt subiecte vaste și am acoperit doar o mână de moduri de a proteja datele sensibile folosind PHP. (Am scris anterior despre protejarea acestui tip de date în mediul dvs. local)