Zanim zrobisz cokolwiek dalej, spróbuj zrozumieć różnicę między szyfrowaniem a uwierzytelnianiem i dlaczego prawdopodobnie chcesz szyfrowania uwierzytelnionego, a nie tylko szyfrowania .
Aby zaimplementować uwierzytelnianie szyfrowane, chcesz zaszyfrować, a następnie MAC. Kolejność szyfrowania i uwierzytelniania jest bardzo ważna! Jedna z istniejących odpowiedzi na to pytanie popełniła ten błąd; podobnie jak wiele bibliotek kryptograficznych napisanych w PHP.
Należy unikać wdrażania własnej kryptografii , a zamiast tego korzystać z bezpiecznej biblioteki napisanej i sprawdzonej przez ekspertów w dziedzinie kryptografii.
Aktualizacja: PHP 7.2 zapewnia teraz libsodium ! Aby zapewnić najwyższy poziom bezpieczeństwa, zaktualizuj system, aby korzystał z PHP 7.2 lub nowszego i postępuj zgodnie z zaleceniami libsodium zawartymi w tej odpowiedzi.
Użyj libsodium, jeśli masz dostęp do PECL (lub sodu_compat, jeśli chcesz libsodium bez PECL); w przeciwnym razie ...
Użyj szyfrowania / defuse / php ; nie rzucaj własną kryptografią!
Obie wyżej wymienione biblioteki ułatwiają i bezbolesne wdrożenie uwierzytelnionego szyfrowania we własnych bibliotekach.
Jeśli nadal chcesz pisać i wdrażać własną bibliotekę kryptograficzną, wbrew konwencjonalnej wiedzy każdego eksperta w dziedzinie kryptografii w Internecie, musisz wykonać te kroki.
Szyfrowanie:
- Szyfruj za pomocą AES w trybie CTR. Możesz także użyć GCM (co eliminuje potrzebę oddzielnego MAC). Dodatkowo ChaCha20 i Salsa20 (dostarczone przez libsodium ) są szyframi strumieniowymi i nie wymagają specjalnych trybów.
- O ile nie wybrałeś GCM powyżej, powinieneś uwierzytelnić tekst zaszyfrowany za pomocą HMAC-SHA-256 (lub, w przypadku szyfrów strumieniowych, Poly1305 - większość interfejsów API libsodium robi to za Ciebie). MAC powinien obejmować IV, a także tekst zaszyfrowany!
Deszyfrowanie:
- O ile nie zostanie użyty Poly1305 lub GCM, ponownie oblicz MAC zaszyfrowanego tekstu i porównaj go z MAC, który został wysłany
hash_equals()
. Jeśli to się nie powiedzie, przerwij.
- Odszyfruj wiadomość.
Inne uwagi dotyczące projektu:
- Nigdy nie kompresuj niczego. Tekst zaszyfrowany nie podlega kompresji; kompresowanie tekstu jawnego przed szyfrowaniem może prowadzić do wycieku informacji (np. PRZESTĘPSTWA i NARUSZENIE w TLS).
- Upewnij się, że używasz
mb_strlen()
i mb_substr()
, używając '8bit'
trybu zestawu znaków, aby zapobiec mbstring.func_overload
problemom.
- IV powinny generować przy użyciu CSPRNG ; Jeśli używasz
mcrypt_create_iv()
, NIE UŻYWAJMCRYPT_RAND
!
- O ile nie używasz konstrukcji AEAD, ZAWSZE szyfruj, a następnie MAC!
bin2hex()
, base64_encode()
itp. mogą wyciekać informacje o twoich kluczach szyfrujących poprzez synchronizację pamięci podręcznej. Unikaj ich, jeśli to możliwe.
Nawet jeśli zastosujesz się do podanych tu wskazówek, wiele może pójść nie tak z kryptografią. Zawsze poproś eksperta od kryptografii o sprawdzenie Twojej implementacji. Jeśli nie masz szczęścia, aby być osobistym przyjacielem studenta kryptografii na lokalnym uniwersytecie, zawsze możesz skorzystać z forum Cryptography Stack Exchange w celu uzyskania porady.
Jeśli potrzebujesz profesjonalnej analizy swojego wdrożenia, zawsze możesz zatrudnić renomowany zespół konsultantów ds. Bezpieczeństwa, aby sprawdzili Twój kod kryptograficzny PHP (ujawnienie: mój pracodawca).
Ważne: kiedy nie używać szyfrowania
Nie szyfruj haseł . Chcesz hash nich zamiast, korzystając z jednej z tych algorytmów mieszania hasłem:
Nigdy nie używaj funkcji skrótu ogólnego przeznaczenia (MD5, SHA256) do przechowywania haseł.
Nie szyfruj parametrów adresów URL . To nieodpowiednie narzędzie do pracy.
Przykład szyfrowania łańcucha PHP za pomocą Libsodium
Jeśli korzystasz z PHP <7.2 lub w inny sposób nie masz zainstalowanego libsodium, możesz użyć sodium_compat, aby osiągnąć ten sam wynik (choć wolniej).
<?php
declare(strict_types=1);
/**
* Encrypt a message
*
* @param string $message - message to encrypt
* @param string $key - encryption key
* @return string
* @throws RangeException
*/
function safeEncrypt(string $message, string $key): string
{
if (mb_strlen($key, '8bit') !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new RangeException('Key is not the correct size (must be 32 bytes).');
}
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = base64_encode(
$nonce.
sodium_crypto_secretbox(
$message,
$nonce,
$key
)
);
sodium_memzero($message);
sodium_memzero($key);
return $cipher;
}
/**
* Decrypt a message
*
* @param string $encrypted - message encrypted with safeEncrypt()
* @param string $key - encryption key
* @return string
* @throws Exception
*/
function safeDecrypt(string $encrypted, string $key): string
{
$decoded = base64_decode($encrypted);
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plain = sodium_crypto_secretbox_open(
$ciphertext,
$nonce,
$key
);
if (!is_string($plain)) {
throw new Exception('Invalid MAC');
}
sodium_memzero($ciphertext);
sodium_memzero($key);
return $plain;
}
Następnie, aby to przetestować:
<?php
// This refers to the previous code block.
require "safeCrypto.php";
// Do this once then store it somehow:
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$message = 'We are all living in a yellow submarine';
$ciphertext = safeEncrypt($message, $key);
$plaintext = safeDecrypt($ciphertext, $key);
var_dump($ciphertext);
var_dump($plaintext);
Halite - Libsodium Made Easy
Jednym z projektów, nad którymi pracuję, jest biblioteka szyfrowania o nazwie Halite , która ma na celu uczynienie libsodium łatwiejszym i bardziej intuicyjnym.
<?php
use \ParagonIE\Halite\KeyFactory;
use \ParagonIE\Halite\Symmetric\Crypto as SymmetricCrypto;
// Generate a new random symmetric-key encryption key. You're going to want to store this:
$key = new KeyFactory::generateEncryptionKey();
// To save your encryption key:
KeyFactory::save($key, '/path/to/secret.key');
// To load it again:
$loadedkey = KeyFactory::loadEncryptionKey('/path/to/secret.key');
$message = 'We are all living in a yellow submarine';
$ciphertext = SymmetricCrypto::encrypt($message, $key);
$plaintext = SymmetricCrypto::decrypt($ciphertext, $key);
var_dump($ciphertext);
var_dump($plaintext);
Cała kryptografia jest obsługiwana przez libsodium.
Przykład z defuse / php-encryption
<?php
/**
* This requires https://github.com/defuse/php-encryption
* php composer.phar require defuse/php-encryption
*/
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
require "vendor/autoload.php";
// Do this once then store it somehow:
$key = Key::createNewRandomKey();
$message = 'We are all living in a yellow submarine';
$ciphertext = Crypto::encrypt($message, $key);
$plaintext = Crypto::decrypt($ciphertext, $key);
var_dump($ciphertext);
var_dump($plaintext);
Uwaga : Crypto::encrypt()
zwraca dane wyjściowe w formacie szesnastkowym.
Zarządzanie kluczami szyfrującymi
Jeśli masz ochotę użyć „hasła”, przestań już teraz. Potrzebujesz losowego 128-bitowego klucza szyfrowania, a nie hasła, które można zapamiętać.
Możesz przechowywać klucz szyfrujący do długoterminowego użytku, tak jak:
$storeMe = bin2hex($key);
I na żądanie możesz go pobrać w następujący sposób:
$key = hex2bin($storeMe);
I zdecydowanie zalecamy tylko przechowywanie losowo wygenerowanego klucza do długotrwałego stosowania zamiast jakichkolwiek hasła jako klucza (lub w celu uzyskania klucza).
Jeśli korzystasz z biblioteki Defuse:
„Ale naprawdę chcę użyć hasła”.
To zły pomysł, ale dobrze, oto jak to zrobić bezpiecznie.
Najpierw wygeneruj losowy klucz i zapisz go w stałej.
/**
* Replace this with your own salt!
* Use bin2hex() then add \x before every 2 hex characters, like so:
*/
define('MY_PBKDF2_SALT', "\x2d\xb7\x68\x1a\x28\x15\xbe\x06\x33\xa0\x7e\x0e\x8f\x79\xd5\xdf");
Pamiętaj, że dodajesz dodatkową pracę i możesz użyć tej stałej jako klucza i zaoszczędzić sobie dużo bólu serca!
Następnie użyj PBKDF2 (podobnie jak ten), aby uzyskać odpowiedni klucz szyfrowania na podstawie hasła zamiast szyfrować go bezpośrednio.
/**
* Get an AES key from a static password and a secret salt
*
* @param string $password Your weak password here
* @param int $keysize Number of bytes in encryption key
*/
function getKeyFromPassword($password, $keysize = 16)
{
return hash_pbkdf2(
'sha256',
$password,
MY_PBKDF2_SALT,
100000, // Number of iterations
$keysize,
true
);
}
Nie używaj tylko 16-znakowego hasła. Twój klucz szyfrujący zostanie komicznie zepsuty.