Ich habe kürzlich an der Laracon EU 2018 teilgenommen, wo Marcus Bointon einen großartigen Vortrag über Krypto in PHP 7.2 gehalten hat. Ich verließ den Vortrag mit einer viel größeren Wertschätzung dafür, wie sehr kompliziert Kryptographie ist, aber auch dafür, wie PHP die Verschlüsselung dank der Einführung von Sodium zugänglicher macht. Datenverschlüsselung in PHP ist etwas, woran ich im Rahmen meiner Arbeit an SpinupWP gearbeitet habe, also dachte ich, es wäre an der Zeit, ein paar Einblicke zu geben. Schnallen Sie sich an, denn dies könnte eine holprige Fahrt sein!
Verschlüsselungstypen
Es gibt heute eine Reihe verschiedener Verschlüsselungsmethoden, von denen die häufigsten Hashing, Secret Key-Verschlüsselung und Public Key-Verschlüsselung sind. Darüber hinaus stehen für jede Verschlüsselungsmethode mehrere Algorithmen oder Chiffren zur Auswahl (jede mit ihren eigenen Stärken und Schwächen). In diesem Artikel konzentrieren wir uns auf die Implementierung von Hashing und Secret Key-Verschlüsselung.
Hashing
Ein Hashing-Algorithmus nimmt einen Eingabewert und wandelt ihn in einen Message Digest um. Kurz gesagt, Klartextwerte werden in einen Hash mit fester Länge umgewandelt und können nur validiert werden, indem der ursprüngliche Wert an den Hashalgorithmus übergeben wird. Dies macht Hashing perfekt zum Speichern von Benutzerkennwörtern.
Es ist erwähnenswert, dass Hashing keine kugelsichere Lösung ist und nicht alle Hashing-Algorithmen gleich sind. Betrachten Sie MD5 und SHA1, die schnell und effizient sind und sich ideal für die Prüfsummierung und Dateiüberprüfung eignen. Ihre Geschwindigkeit macht sie jedoch ungeeignet für das Hashing des Passworts eines Benutzers. Mit der heutigen Rechenleistung moderner GPUs kann ein Passwort in wenigen Minuten mit Brute-Force geknackt werden, wobei das ursprüngliche Klartext-Passwort preisgegeben wird. Stattdessen sollten absichtlich langsamere Hashing-Algorithmen wie bcrypt oder Argon2 verwendet werden.
Während ein Hash-Passwort, das von einem beliebigen Algorithmus generiert wird, sicherlich die Originaldaten verdeckt und jeden angreifenden Angreifer verlangsamt, sollten wir als Entwickler uns bemühen, den stärksten verfügbaren Algorithmus zu verwenden. Glücklicherweise macht PHP dies dank password_hash()
einfach.
$hash = password_hash($password, PASSWORD_DEFAULT);
Die password_hash()
-Funktion verwendet nicht nur einen sicheren Einweg-Hashing-Algorithmus, sondern verarbeitet auch automatisch Salt und verhindert zeitbasierte Seitenkanalangriffe. Ab PHP 5.5 wird bcrypt verwendet, um den Hash zu generieren, aber dies wird sich in Zukunft ändern, wenn neuere und sicherere Hashing-Algorithmen zu PHP hinzugefügt werden. Argon2 wird wahrscheinlich der nächste Standard-Hashalgorithmus und kann heute (unter PHP 7.2) verwendet werden, indem das Flag PASSWORD_ARGON2I
anstelle von PASSWORD_DEFAULT
übergeben wird.
Das Überprüfen des Passworts eines Benutzers ist dank der Funktion password_verify()
ebenfalls ein trivialer Vorgang. Übergeben Sie einfach das vom Benutzer angegebene Klartext-Passwort und vergleichen Sie es mit dem gespeicherten Hash:
if (password_verify($password, $hash)) { echo "Let me in, I'm genuine!";}
Beachten Sie, wie die Passwortüberprüfung in PHP durchgeführt wird. Wenn Sie die Anmeldeinformationen eines Benutzers in einer Datenbank speichern, neigen Sie möglicherweise dazu, das bei der Anmeldung eingegebene Kennwort zu hashen und dann eine Datenbankabfrage wie folgt auszuführen:
SELECT * FROM usersWHERE username = 'Ashley'AND password = 'password_hash'LIMIT 1;
Dieser Ansatz ist anfällig für Seitenkanalangriffe und sollte vermieden werden. Geben Sie stattdessen den Benutzer zurück und überprüfen Sie dann den Passwort-Hash in PHP.
SELECT username, password FROM usersWHERE username = 'Ashley'LIMIT 1;
Hashing eignet sich zwar hervorragend zum Speichern des Kennworts eines Benutzers, funktioniert jedoch nicht für beliebige Daten, auf die unsere Anwendung ohne Benutzereingriff zugreifen muss. Betrachten wir eine Abrechnungsanwendung, die die Kreditkarteninformationen eines Benutzers verschlüsselt. Jeden Monat muss unsere Anwendung dem Benutzer die Nutzung des Vormonats in Rechnung stellen. Das Hashing der Kreditkartendaten funktioniert nicht, da unsere Anwendung die Originaldaten kennen muss, um sie im Klartext abzurufen.
Geheime Schlüsselverschlüsselung zur Rettung!
Geheime Schlüsselverschlüsselung
Die geheime Schlüsselverschlüsselung (oder symmetrische Verschlüsselung, wie sie auch genannt wird) verwendet einen einzigen Schlüssel zum Verschlüsseln und Entschlüsseln von Daten. Mal sehen, wie wir einen solchen Mechanismus mit Sodium implementieren würden, der in PHP 7.2 eingeführt wurde. Wenn Sie eine ältere PHP-Version verwenden, können Sie sodium über PECL installieren.
Zuerst benötigen wir einen Verschlüsselungsschlüssel, der mit der Funktion random_bytes()
generiert werden kann. Normalerweise tun Sie dies nur einmal und speichern es als Umgebungsvariable. Denken Sie daran, dass dieser Schlüssel um jeden Preis geheim gehalten werden muss. Sobald der Schlüssel kompromittiert ist, sind auch alle verschlüsselten Daten kompromittiert.
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
Um den Wert zu verschlüsseln, übergeben wir ihn mit unserem $key
und einem $nonce
an sodium_crypto_secretbox()
. Die Nonce wird mit random_bytes()
generiert, da dieselbe Nonce niemals wiederverwendet werden sollte.
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);$ciphertext = sodium_crypto_secretbox('This is a secret!', $nonce, $key);
Dies stellt ein Problem dar, da wir die Nonce benötigen, um den Wert später zu entschlüsseln. Glücklicherweise müssen Nonces nicht geheim gehalten werden, damit wir sie unserem $ciphertext
und dann unserem base64_encode()
Wert voranstellen können, bevor wir sie in der Datenbank speichern.
$encoded = base64_encode($nonce . $ciphertext);var_dump($encoded);// string 'v6KhzRACVfUCyJKCGQF4VNoPXYfeFY+/pyRZcixz4x/0jLJOo+RbeGBTiZudMLEO7aRvg44HRecC' (length=76)
Wenn es darum geht, den Wert zu entschlüsseln, machen wir das Gegenteil.
$decoded = base64_decode($encoded);
Da wir die Länge von nonce kennen, können wir sie mit mb_substr()
extrahieren, bevor wir den Wert entschlüsseln.
$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)
Das ist alles, was es zur geheimen Schlüsselverschlüsselung in PHP gibt, dank Sodium!
Envelope Encryption
Obwohl der oben beschriebene Ansatz sicherlich ein Schritt in die richtige Richtung ist, bleiben unsere Daten dennoch anfällig, wenn der geheime Schlüssel kompromittiert wird. Betrachten wir einen böswilligen Benutzer, der Zugriff auf den Server erhält, auf dem unsere Anwendung gehostet wird. In diesem Szenario besteht die Möglichkeit, dass der Angreifer unseren geheimen Schlüssel entdecken kann, mit dem wir die Daten verschlüsselt haben. Dadurch bleiben unsere Daten vollständig exponiert.
Die einfache Lösung besteht darin, unseren geheimen Schlüssel nicht am selben Ort wie die verschlüsselten Daten zu speichern. Wie ver- und entschlüsseln wir on Demand? Geben Sie den Google Cloud-Schlüsselverwaltungsdienst (Cloud KMS) ein.
Cloud KMS ist ein von Google bereitgestellter Dienst zum sicheren Hosten kryptografischer Schlüssel. Es bietet eine Vielzahl nützlicher Funktionen rund um die Schlüsselspeicherung, einschließlich automatischer Schlüsselrotation und verzögerter Schlüsselzerstörung. In diesem Beispiel geht es jedoch hauptsächlich darum, unseren geheimen Schlüssel außerhalb unserer Daten zu speichern.
Um die Dinge sicherer zu machen, werden wir eine Technik verwenden, die als Envelope Encryption bekannt ist. Bei der Umschlagverschlüsselung werden Schlüssel im Wesentlichen mit einem anderen Schlüssel verschlüsselt. Wir tun dies aus zwei Gründen:
- Cloud KMS hat eine Größenbeschränkung von 64 KB für die Daten, die verschlüsselt und entschlüsselt werden können. Daher ist es möglicherweise nicht möglich, alle Daten auf einen Schlag zu senden.
- Noch wichtiger ist, dass wir unsere sensiblen Klartextdaten nicht an Dritte senden möchten, unabhängig davon, wie vertrauenswürdig sie erscheinen mögen.
Anstatt unsere Klartextdaten an Cloud KMS zu senden, generieren wir jedes Mal, wenn wir vertrauliche Daten in die Datenbank schreiben, einen eindeutigen Verschlüsselungsschlüssel. Dieser Schlüssel wird als Datenverschlüsselungsschlüssel (DEK) bezeichnet, der zur Verschlüsselung unserer Daten verwendet wird. Die DEK wird dann zur Verschlüsselung an Cloud KMS gesendet, wodurch ein Schlüsselverschlüsselungsschlüssel (bekannt als KEK) zurückgegeben wird. Schließlich wird der KEK nebeneinander in der Datenbank neben den verschlüsselten Daten gespeichert und der DEK wird zerstört. Der Prozess sieht so aus:
- Generieren Sie einen eindeutigen Verschlüsselungsschlüssel (DEK)
- Verschlüsseln Sie die Daten mit der geheimen Schlüsselverschlüsselung
- Senden Sie den eindeutigen Verschlüsselungsschlüssel (DEK) zur Verschlüsselung an Cloud KMS, wodurch der KEK zurückgegeben wird
- Speichern Sie die verschlüsselten Daten und den verschlüsselten Schlüssel (KEK) nebeneinander
- Zerstören Sie den)
Beim Entschlüsseln von Daten wird der Vorgang umgekehrt:
- Rufen Sie die verschlüsselten Daten und den verschlüsselten Schlüssel (KEK) aus der Datenbank ab
- Senden Sie den KEK zur Entschlüsselung an Cloud KMS, wodurch der DEK zurückgegeben wird
- Verwenden Sie den DEK, um unsere verschlüsselten Daten zu entschlüsseln
- Zerstöre den DEK
In diesem Sinne habe ich einfache Hilfsklasse zur Durchführung der Envelope-Verschlüsselung. Ich werde nicht auf die in der Google Cloud-Konsole erforderlichen Schritte eingehen, da die Schnellstart- und Authentifizierungshandbücher alles enthalten, was Sie für den Einstieg benötigen. Der Kürze halber gibt es keine Fehlerbehandlung usw. in diesem Beispiel.
<?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 ); }}
Sie werden feststellen, dass die tatsächlichen Verschlüsselungs- und Entschlüsselungsmethoden fast identisch mit der oben eingeführten Implementierung des geheimen Schlüssels sind. Der Unterschied besteht jedoch darin, dass wir jetzt mehrere Verschlüsselungsschlüssel verwenden. Sehen wir uns die Helper-Klasse in Aktion an. Sie müssen Ihre $projectId
, $locationId
, $keyRingId
und $cryptoKeyId
, die über die Google Cloud Console verfügbar sind.
<?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)
Wenn ein Angreifer unser System kompromittiert, würden sie in der Lage sein, auch unsere API-Anmeldeinformationen für Cloud KMS zu gewinnen? Abhängig von der verwendeten Authentifizierungsmethode kann dies eine Möglichkeit sein. Wenn dies der Fall ist, fragen Sie sich vielleicht, wie die Umschlagverschlüsselung sicherer ist als die normale Verschlüsselung mit geheimen Schlüsseln? Der Hauptunterschied (Wortspiel beabsichtigt) besteht darin, dass der API-Zugriff widerrufen werden kann, um einen Angreifer zu vereiteln, der mit Ihren sensiblen Daten fertig wird. Es ist das Äquivalent zum Wechseln Ihrer Schlösser, wenn jemand Ihren Hausschlüssel stiehlt. Mit einer regulären geheimen Schlüsselverschlüsselung, bei der ein einzelner lokaler Schlüssel kompromittiert wird, haben Sie diesen Luxus nicht. Der Angreifer hat alle Zeit der Welt, um Ihre sensiblen Daten zu entschlüsseln.
Einwickeln
Datensicherheit und Verschlüsselung sind große Themen, und ich habe nur eine Handvoll Möglichkeiten zum Schutz sensibler Daten mit PHP behandelt. (Wir haben zuvor über den Schutz dieser Art von Daten in Ihrer lokalen Umgebung geschrieben.)