Antes de continuar, intente comprender la diferencia entre cifrado y autenticación , y por qué probablemente desee un cifrado autenticado en lugar de solo cifrado .
Para implementar el cifrado autenticado, desea cifrar y luego MAC. ¡El orden de encriptación y autenticación es muy importante! Una de las respuestas existentes a esta pregunta cometió este error; al igual que muchas bibliotecas de criptografía escritas en PHP.
Debe evitar implementar su propia criptografía y, en su lugar, usar una biblioteca segura escrita y revisada por expertos en criptografía.
Actualización: PHP 7.2 ahora proporciona libsodium ! Para una mejor seguridad, actualice sus sistemas para usar PHP 7.2 o superior y solo siga los consejos de libsodium en esta respuesta.
Use libsodium si tiene acceso PECL (o sodium_compat si quiere libsodium sin PECL); de lo contrario ...
Use defuse / php-encryption ; ¡no tires tu propia criptografía!
Ambas bibliotecas vinculadas anteriormente hacen que sea fácil y sencillo implementar el cifrado autenticado en sus propias bibliotecas.
Si aún desea escribir e implementar su propia biblioteca de criptografía, en contra de la sabiduría convencional de cada experto en criptografía en Internet, estos son los pasos que debe seguir.
Cifrado:
- Cifrar usando AES en modo CTR. También puede usar GCM (que elimina la necesidad de un MAC separado). Además, ChaCha20 y Salsa20 (proporcionados por libsodium ) son cifrados de flujo y no necesitan modos especiales.
- A menos que elija GCM arriba, debe autenticar el texto cifrado con HMAC-SHA-256 (o, para los cifrados de flujo, Poly1305, la mayoría de las API de libsodium lo hacen por usted). ¡El MAC debería cubrir tanto el IV como el texto cifrado!
Descifrado:
- A menos que se use Poly1305 o GCM, vuelva a calcular el MAC del texto cifrado y compárelo con el MAC que se envió usando
hash_equals()
. Si falla, aborta.
- Descifra el mensaje.
Otras consideraciones de diseño:
- No comprimir nada nunca. El texto cifrado no es compresible; La compresión de texto sin formato antes del cifrado puede generar filtraciones de información (por ejemplo, CRIMEN y BREACH en TLS).
- Asegúrese de usar
mb_strlen()
y mb_substr()
, usando el '8bit'
modo de juego de caracteres para evitar mbstring.func_overload
problemas.
- Los IV deben generarse utilizando un CSPRNG ; Si está usando
mcrypt_create_iv()
, ¡NO LO USEMCRYPT_RAND
!
- A menos que esté utilizando una construcción AEAD, ¡ SIEMPRE cifre y luego MAC!
bin2hex()
, base64_encode()
, Etc., puede filtrar información acerca de sus claves de cifrado a través de la sincronización de caché. Evítalos si es posible.
Incluso si sigue los consejos dados aquí, muchas cosas pueden salir mal con la criptografía. Siempre haga que un experto en criptografía revise su implementación. Si no tiene la suerte de ser amigo personal de un estudiante de criptografía en su universidad local, siempre puede intentar el foro de intercambio de pila de criptografía para obtener asesoramiento.
Si necesita un análisis profesional de su implementación, siempre puede contratar a un equipo de consultores de seguridad acreditados para revisar su código de criptografía PHP (divulgación: mi empleador).
Importante: Cuándo no usar cifrado
No encriptes las contraseñas . Ensu lugar,deseausar hash , utilizando uno de estos algoritmos de hash de contraseña:
Nunca use una función hash de propósito general (MD5, SHA256) para el almacenamiento de contraseñas.
No cifre los parámetros de URL . Es la herramienta incorrecta para el trabajo.
Ejemplo de cifrado de cadenas PHP con Libsodium
Si tiene PHP <7.2 o no tiene instalado libsodium, puede usar sodium_compat para lograr el mismo resultado (aunque más lento).
<?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;
}
Luego para probarlo:
<?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);
Halita - Libsodium hecho más fácil
Uno de los proyectos en los que he estado trabajando es una biblioteca de cifrado llamada Halite , cuyo objetivo es hacer que libsodium sea más fácil e intuitivo.
<?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);
Toda la criptografía subyacente es manejada por libsodium.
Ejemplo con 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);
Nota : Crypto::encrypt()
devuelve salida codificada en hexadecimal.
Gestión de claves de cifrado
Si está tentado a usar una "contraseña", deténgase ahora mismo. Necesita una clave de cifrado aleatoria de 128 bits, no una contraseña humana memorable.
Puede almacenar una clave de cifrado para uso a largo plazo de esta manera:
$storeMe = bin2hex($key);
Y, a pedido, puede recuperarlo así:
$key = hex2bin($storeMe);
Yo fuertemente recomiendo que acaba de almacenar una clave generada aleatoriamente para uso a largo plazo en lugar de cualquier tipo de contraseña que la tecla (o deducir la clave).
Si está utilizando la biblioteca de Defuse:
"Pero realmente quiero usar una contraseña".
Esa es una mala idea, pero está bien, aquí está cómo hacerlo de manera segura.
Primero, genere una clave aleatoria y almacénela en una constante.
/**
* 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");
¡Tenga en cuenta que está agregando trabajo adicional y podría usar esta constante como la clave y ahorrarse mucha angustia!
Luego use PBKDF2 (así) para derivar una clave de cifrado adecuada de su contraseña en lugar de cifrarla directamente.
/**
* 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
);
}
No solo use una contraseña de 16 caracteres. Su clave de cifrado se romperá cómicamente.