Metody szyfrowania PHP dla haseł i innych poufnych danych

niedawno uczestniczyłem w LARACON EU 2018, Gdzie Marcus Bointon wygłosił świetną prelekcję na temat krypto w PHP 7.2. Zakończyłem wykład mając dużo większe uznanie dla tego, jak bardzo skomplikowana jest kryptografia, ale także dla tego, jak PHP czyni szyfrowanie bardziej dostępnym dzięki wprowadzeniu sodu. Szyfrowanie danych w PHP to coś, nad czym pracowałem w ramach mojej pracy nad SpinupWP, więc pomyślałem, że nadszedł czas, abym podzielił się kilkoma spostrzeżeniami. Zapnij pasy, bo to może być wyboista Jazda!

rodzaje szyfrowania

obecnie stosuje się wiele różnych metod szyfrowania, z których najczęstsze to hashing, szyfrowanie klucza tajnego i szyfrowanie klucza publicznego. Ponadto każda metoda szyfrowania ma wiele algorytmów lub szyfrów do wyboru (każdy z własnymi mocnymi i słabymi stronami). W tym artykule skupimy się na implementacji haszowania i szyfrowania tajnych kluczy.

haszowanie

algorytm haszujący pobiera wartość wejściową i przekształca ją w skrót wiadomości. W skrócie, wartości tekstowe są przekształcane do skrótu o stałej długości i mogą być walidowane tylko przez przekazanie oryginalnej wartości do algorytmu haszującego. Dzięki temu hashing jest idealny do przechowywania haseł użytkowników.

warto zauważyć, że hashowanie nie jest rozwiązaniem kuloodpornym i nie wszystkie algorytmy hashowania są sobie równe. Rozważ MD5 I SHA1, które są szybkie i wydajne, dzięki czemu idealnie nadają się do sumowania kontrolnego i weryfikacji plików. Jednak ich szybkość sprawia, że nie nadają się do haszowania hasła użytkownika. Dzięki dzisiejszej mocy obliczeniowej nowoczesnych procesorów graficznych hasło może zostać złamane brutalną siłą w ciągu kilku minut, ujawniając oryginalne hasło tekstowe. Zamiast tego należy użyć celowo wolniejszych algorytmów haszujących, takich jak bcrypt lub Argon2.

podczas gdy hashowane hasło wygenerowane przez dowolny algorytm z pewnością zasłoni oryginalne Dane i spowolni potencjalnego atakującego, my jako programiści powinniśmy starać się używać najsilniejszego dostępnego algorytmu. Na szczęście PHP ułatwia to dzięki password_hash().

$hash = password_hash($password, PASSWORD_DEFAULT);

funkcja password_hash() nie tylko używa bezpiecznego jednokierunkowego algorytmu haszującego, ale automatycznie obsługuje sól i zapobiega atakom bocznym opartym na czasie. Od wersji PHP 5.5, bcrypt będzie używany do generowania skrótu, ale zmieni się to w przyszłości, ponieważ do PHP dodawane są nowsze i bezpieczniejsze algorytmy haszujące. Argon2 prawdopodobnie stanie się następnym domyślnym algorytmem haszującym i może być używany dzisiaj (w PHP 7.2) poprzez przekazanie znacznika PASSWORD_ARGON2I zamiast PASSWORD_DEFAULT.

weryfikacja hasła użytkownika jest również trywialnym procesem dzięki funkcji password_verify(). Po prostu podaj hasło tekstowe dostarczone przez użytkownika i porównaj je z zapisanym Hashem, tak:

if (password_verify($password, $hash)) { echo "Let me in, I'm genuine!";}

zwróć uwagę na sposób weryfikacji hasła w PHP. Jeśli przechowujesz dane uwierzytelniające użytkownika w bazie danych, możesz być skłonny do skrócenia hasła wprowadzonego podczas logowania, a następnie wykonać zapytanie do bazy danych, w ten sposób:

SELECT * FROM usersWHERE username = 'Ashley'AND password = 'password_hash'LIMIT 1;

takie podejście jest podatne na ataki boczne i należy go unikać. Zamiast tego zwróć użytkownika, a następnie sprawdź hash hasła w PHP.

SELECT username, password FROM usersWHERE username = 'Ashley'LIMIT 1;

chociaż haszowanie jest Świetne do przechowywania hasła użytkownika, nie działa w przypadku dowolnych danych, do których nasza aplikacja musi uzyskać dostęp bez interwencji użytkownika. Rozważmy aplikację rozliczeniową, która szyfruje dane karty kredytowej użytkownika. Co miesiąc nasza aplikacja musi rozliczać użytkownika za korzystanie z poprzedniego miesiąca. Hashowanie danych karty kredytowej nie będzie działać, ponieważ wymaga, aby nasza aplikacja znała oryginalne dane, aby pobrać je w zwykłym tekście.

tajne szyfrowanie klucza na ratunek!

szyfrowanie klucza tajnego

szyfrowanie klucza tajnego (lub szyfrowanie symetryczne, jak jest również znane) wykorzystuje jeden klucz do szyfrowania i deszyfrowania danych. Zobaczmy, jak wdrożylibyśmy taki mechanizm za pomocą Sodium, który został wprowadzony w PHP 7.2. Jeśli używasz starszej wersji PHP możesz zainstalować sodium poprzez PECL.

najpierw potrzebujemy klucza szyfrowania, który można wygenerować za pomocą funkcji random_bytes(). Zazwyczaj zrobisz to tylko raz i zapiszesz ją jako zmienną środowiskową. Pamiętaj, że ten klucz musi być utrzymywany w tajemnicy za wszelką cenę. Gdy klucz zostanie naruszony, tak samo jak wszelkie zaszyfrowane dane.

$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);

aby zaszyfrować wartość przekazujemy ją do sodium_crypto_secretbox() z naszym $keyi $nonce. Nonce jest generowane przy użyciu random_bytes(), ponieważ ten sam nonce nigdy nie powinien być ponownie użyty.

$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);$ciphertext = sodium_crypto_secretbox('This is a secret!', $nonce, $key);

to stwarza problem, ponieważ potrzebujemy nonce, aby później odszyfrować wartość. Na szczęście, nonces nie muszą być trzymane w tajemnicy, więc możemy przed zapisaniem do bazy danych przedłożyć ją do wartości $ciphertext, a następnie base64_encode().

$encoded = base64_encode($nonce . $ciphertext);var_dump($encoded);// string 'v6KhzRACVfUCyJKCGQF4VNoPXYfeFY+/pyRZcixz4x/0jLJOo+RbeGBTiZudMLEO7aRvg44HRecC' (length=76)

jeśli chodzi o odszyfrowanie wartości, robimy odwrotnie.

$decoded = base64_decode($encoded);

ponieważ znamy długość nonce, możemy wyodrębnić ją za pomocą mb_substr() przed odszyfrowaniem wartości.

$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 wszystko, co jest do szyfrowania tajnych kluczy w PHP, dzięki Sodium!

Szyfrowanie koperty

chociaż opisane powyżej podejście jest z pewnością krokiem we właściwym kierunku, nadal pozostawia nasze dane podatne na ataki, jeśli tajny klucz zostanie naruszony. Rozważmy złośliwego użytkownika, który uzyskuje dostęp do serwera, na którym znajduje się nasza aplikacja. W tym scenariuszu możliwe, że atakujący będzie w stanie odkryć nasz tajny klucz, którego użyliśmy do zaszyfrowania danych. To całkowicie ujawnia nasze dane.

prostym rozwiązaniem jest nie przechowywanie naszego tajnego klucza w tej samej lokalizacji, co zaszyfrowane dane, ale stanowi to problem. Jak szyfrować i odszyfrowywać na żądanie? Wprowadź usługę Google Cloud Key Management Service (Cloud KMS).

Cloud KMS to usługa świadczona przez Google do bezpiecznego hostowania kluczy kryptograficznych. Zapewnia wiele przydatnych funkcji związanych z przechowywaniem kluczy, w tym automatyczną rotację kluczy i opóźnione niszczenie kluczy. Jednak w tym przykładzie zajmujemy się przede wszystkim przechowywaniem naszego tajnego klucza z dala od naszych danych.

aby uczynić rzeczy bardziej bezpiecznymi, użyjemy techniki znanej jako szyfrowanie koperty. Zasadniczo szyfrowanie kopert polega na szyfrowaniu kluczy za pomocą innego klucza. Robimy to z dwóch powodów:

  1. Cloud KMS ma limit rozmiaru 64 KiB danych, które mogą być szyfrowane i odszyfrowane. Dlatego może nie być możliwe wysłanie wszystkich danych za jednym zamachem.
  2. co ważniejsze, nie chcemy wysyłać naszych poufnych danych tekstowych osobom trzecim, niezależnie od tego, jak wiarygodne mogą się wydawać.

zamiast wysyłać nasze dane tekstowe do Cloud KMS, będziemy generować unikalny klucz szyfrowania za każdym razem, gdy zapisujemy poufne dane do bazy danych. Ten klucz jest znany jako klucz szyfrowania danych (dek), który będzie używany do szyfrowania naszych danych. DEK jest następnie wysyłany do Cloud KMS w celu zaszyfrowania, który zwraca klucz szyfrujący (znany jako KEK). Wreszcie, KEK jest przechowywany obok siebie w bazie danych obok zaszyfrowanych danych, a dek jest niszczony. Proces wygląda tak:

  1. Wygeneruj unikalny klucz szyfrowania (DEK)
  2. Zaszyfruj dane przy użyciu tajnego klucza szyfrowania
  3. Wyślij unikalny klucz szyfrowania (dek) do Cloud KMS w celu szyfrowania, który zwraca KEK
  4. przechowuj zaszyfrowane dane i zaszyfrowany klucz (KEK) obok siebie
  5. Zniszcz wygenerowany klucz (DEK)

podczas deszyfrowania danych Proces jest odwrócony:

  1. Pobierz zaszyfrowane dane i zaszyfrowany klucz (KEK) z bazy danych
  2. Wyślij KEK do Cloud KMS w celu odszyfrowania, który zwraca DEK
  3. użyj DEK do odszyfrowania naszych zaszyfrowanych danych
  4. Zniszcz DEK

Mając to na uwadze, stworzyłem bardzo prosta klasa pomocnicza do wykonywania szyfrowania obwiedni. Nie zamierzam przeglądać kroków wymaganych w konsoli Google Cloud, ponieważ Przewodniki Quickstart i uwierzytelnianie opisują wszystko, czego potrzebujesz, aby rozpocząć. Ze względu na zwięzłość nie ma obsługi błędów itp. w tym przykładzie.

<?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 ); }}

zauważysz, że rzeczywiste metody szyfrowania i deszyfrowania są prawie identyczne z implementacją klucza tajnego wprowadzoną powyżej. Różnica polega jednak na tym, że teraz używamy wielu kluczy szyfrujących. Zobaczmy klasę helper w akcji. Musisz podać swój $projectId, $locationId, $keyRingId i $cryptoKeyId, które są dostępne z konsoli 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)

Jeśli atakujący skompromitował nasz system, czy byłby w stanie uzyskać nasze poświadczenia API dla Cloud KMS? W zależności od zastosowanej metody uwierzytelniania, to tak może być możliwe. Jeśli tak jest, możesz się zastanawiać, w jaki sposób szyfrowanie kopert jest bezpieczniejsze niż zwykłe szyfrowanie tajnych kluczy? Kluczową różnicą (gra słów przeznaczona) jest to, że dostęp do API może zostać odwołany, co uniemożliwia atakującemu, który uciekł z Twoimi wrażliwymi danymi. To odpowiednik wymiany zamków, jeśli ktoś ukradnie Twój klucz do domu. Dzięki standardowemu szyfrowaniu klucza tajnego, w którym naruszony jest pojedynczy klucz lokalny, nie masz takiego luksusu. Atakujący ma cały czas na świecie, aby odszyfrować twoje poufne dane.

bezpieczeństwo danych i szyfrowanie to rozległe tematy i omówiłem tylko kilka sposobów ochrony poufnych danych za pomocą PHP. (Wcześniej pisaliśmy o ochronie tego rodzaju danych w środowisku lokalnym)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.

Previous post Aktualizacja: policja aresztuje domniemanego strzelca w Shady Oaks-Albert Lea Tribune / Albert Lea Tribune
Next post jak usunąć plamy oleju i tłuszczu na ubraniach