El cifrado bidireccional más simple usando PHP
¿Cuál es la forma más sencilla de realizar cifrado bidireccional en instalaciones comunes de PHP?
Necesito poder cifrar datos con una clave de cadena y usar la misma clave para descifrarlos en el otro extremo.
La seguridad no es una preocupación tan grande como la portabilidad del código, por lo que me gustaría poder mantener las cosas lo más simples posible. Actualmente, estoy usando una implementación RC4, pero si puedo encontrar algo compatible de forma nativa, creo que puedo ahorrar una gran cantidad de código innecesario.
Importante : a menos que tenga un caso de uso muy particular, no cifre las contraseñas ; en su lugar, utilice un algoritmo de hash de contraseñas. Cuando alguien dice que cifra sus contraseñas en una aplicación del lado del servidor, o no está informado o está describiendo un diseño de sistema peligroso. Almacenar contraseñas de forma segura es un problema totalmente independiente del cifrado.
Ser informado. Diseñar sistemas seguros.
Cifrado de datos portátil en PHP
Si está utilizando PHP 5.4 o posterior y no desea escribir un módulo de criptografía usted mismo, le recomiendo utilizar una biblioteca existente que proporcione cifrado autenticado . La biblioteca que vinculé se basa únicamente en lo que proporciona PHP y está bajo revisión periódica por parte de un puñado de investigadores de seguridad. (Yo incluido).
Si sus objetivos de portabilidad no le impiden requerir extensiones PECL, se recomienda encarecidamente libsodium sobre cualquier cosa que usted o yo podamos escribir en PHP.
Actualización (12 de junio de 2016): ahora puede usar sodio_compat y usar las mismas ofertas de crypto libsodium sin instalar extensiones PECL.
Si quieres probar suerte en la ingeniería de criptografía, sigue leyendo.
En primer lugar, debe tomarse el tiempo para conocer los peligros del cifrado no autenticado y el principio de fatalidad criptográfica .
- Un usuario malintencionado aún puede alterar los datos cifrados.
- La autenticación de los datos cifrados evita la manipulación.
- La autenticación de los datos no cifrados no evita la manipulación.
Cifrado y descifrado
El cifrado en PHP es realmente simple (lo usaremos openssl_encrypt()
y openssl_decrypt()
una vez que haya tomado algunas decisiones sobre cómo cifrar su información. Consulte openssl_get_cipher_methods()
para obtener una lista de los métodos admitidos en su sistema. La mejor opción es AES en modo CTR :
aes-128-ctr
aes-192-ctr
aes-256-ctr
Actualmente no hay razón para creer que el tamaño de la clave AES sea un problema importante del que preocuparse (más grande probablemente no sea mejor, debido a una mala programación de claves en el modo de 256 bits).
Nota: No lo utilizamos mcrypt
porque es software abandonado y tiene errores sin parchear que podrían afectar la seguridad. Por estas razones, animo a otros desarrolladores de PHP a que también lo eviten.
Envoltura de cifrado/descifrado simple usando OpenSSL
class UnsafeCrypto
{
const METHOD = 'aes-256-ctr';
/**
* Encrypts (but does not authenticate) a message
*
* @param string $message - plaintext message
* @param string $key - encryption key (raw binary expected)
* @param boolean $encode - set to TRUE to return a base64-encoded
* @return string (raw binary)
*/
public static function encrypt($message, $key, $encode = false)
{
$nonceSize = openssl_cipher_iv_length(self::METHOD);
$nonce = openssl_random_pseudo_bytes($nonceSize);
$ciphertext = openssl_encrypt(
$message,
self::METHOD,
$key,
OPENSSL_RAW_DATA,
$nonce
);
// Now let's pack the IV and the ciphertext together
// Naively, we can just concatenate
if ($encode) {
return base64_encode($nonce.$ciphertext);
}
return $nonce.$ciphertext;
}
/**
* Decrypts (but does not verify) a message
*
* @param string $message - ciphertext message
* @param string $key - encryption key (raw binary expected)
* @param boolean $encoded - are we expecting an encoded string?
* @return string
*/
public static function decrypt($message, $key, $encoded = false)
{
if ($encoded) {
$message = base64_decode($message, true);
if ($message === false) {
throw new Exception('Encryption failure');
}
}
$nonceSize = openssl_cipher_iv_length(self::METHOD);
$nonce = mb_substr($message, 0, $nonceSize, '8bit');
$ciphertext = mb_substr($message, $nonceSize, null, '8bit');
$plaintext = openssl_decrypt(
$ciphertext,
self::METHOD,
$key,
OPENSSL_RAW_DATA,
$nonce
);
return $plaintext;
}
}
Ejemplo de uso
$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');
$encrypted = UnsafeCrypto::encrypt($message, $key);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key);
var_dump($encrypted, $decrypted);
Demostración : https://3v4l.org/jl7qR
La simple biblioteca criptográfica anterior todavía no es segura de usar. Necesitamos autenticar los textos cifrados y verificarlos antes de descifrarlos .
Nota : De forma predeterminada, UnsafeCrypto::encrypt()
devolverá una cadena binaria sin formato. Llámelo así si necesita almacenarlo en un formato binario seguro (codificado en base64):
$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');
$encrypted = UnsafeCrypto::encrypt($message, $key, true);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key, true);
var_dump($encrypted, $decrypted);
Demostración : http://3v4l.org/f5K93
Envoltorio de autenticación simple
class SaferCrypto extends UnsafeCrypto
{
const HASH_ALGO = 'sha256';
/**
* Encrypts then MACs a message
*
* @param string $message - plaintext message
* @param string $key - encryption key (raw binary expected)
* @param boolean $encode - set to TRUE to return a base64-encoded string
* @return string (raw binary)
*/
public static function encrypt($message, $key, $encode = false)
{
list($encKey, $authKey) = self::splitKeys($key);
// Pass to UnsafeCrypto::encrypt
$ciphertext = parent::encrypt($message, $encKey);
// Calculate a MAC of the IV and ciphertext
$mac = hash_hmac(self::HASH_ALGO, $ciphertext, $authKey, true);
if ($encode) {
return base64_encode($mac.$ciphertext);
}
// Prepend MAC to the ciphertext and return to caller
return $mac.$ciphertext;
}
/**
* Decrypts a message (after verifying integrity)
*
* @param string $message - ciphertext message
* @param string $key - encryption key (raw binary expected)
* @param boolean $encoded - are we expecting an encoded string?
* @return string (raw binary)
*/
public static function decrypt($message, $key, $encoded = false)
{
list($encKey, $authKey) = self::splitKeys($key);
if ($encoded) {
$message = base64_decode($message, true);
if ($message === false) {
throw new Exception('Encryption failure');
}
}
// Hash Size -- in case HASH_ALGO is changed
$hs = mb_strlen(hash(self::HASH_ALGO, '', true), '8bit');
$mac = mb_substr($message, 0, $hs, '8bit');
$ciphertext = mb_substr($message, $hs, null, '8bit');
$calculated = hash_hmac(
self::HASH_ALGO,
$ciphertext,
$authKey,
true
);
if (!self::hashEquals($mac, $calculated)) {
throw new Exception('Encryption failure');
}
// Pass to UnsafeCrypto::decrypt
$plaintext = parent::decrypt($ciphertext, $encKey);
return $plaintext;
}
/**
* Splits a key into two separate keys; one for encryption
* and the other for authenticaiton
*
* @param string $masterKey (raw binary)
* @return array (two raw binary strings)
*/
protected static function splitKeys($masterKey)
{
// You really want to implement HKDF here instead!
return [
hash_hmac(self::HASH_ALGO, 'ENCRYPTION', $masterKey, true),
hash_hmac(self::HASH_ALGO, 'AUTHENTICATION', $masterKey, true)
];
}
/**
* Compare two strings without leaking timing information
*
* @param string $a
* @param string $b
* @ref https://paragonie.com/b/WS1DLx6BnpsdaVQW
* @return boolean
*/
protected static function hashEquals($a, $b)
{
if (function_exists('hash_equals')) {
return hash_equals($a, $b);
}
$nonce = openssl_random_pseudo_bytes(32);
return hash_hmac(self::HASH_ALGO, $a, $nonce) === hash_hmac(self::HASH_ALGO, $b, $nonce);
}
}
Ejemplo de uso
$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');
$encrypted = SaferCrypto::encrypt($message, $key);
$decrypted = SaferCrypto::decrypt($encrypted, $key);
var_dump($encrypted, $decrypted);
Demostraciones : binario sin formato , codificado en base64
Si alguien desea utilizar esta SaferCrypto
biblioteca en un entorno de producción, o su propia implementación de los mismos conceptos, le recomiendo encarecidamente que se comunique con sus criptógrafos residentes para obtener una segunda opinión antes de hacerlo. Podrán informarle sobre errores de los que quizás yo ni siquiera sea consciente.
Será mucho mejor que utilice una biblioteca de criptografía acreditada .
Editado:
Realmente deberías usar openssl_encrypt() y openssl_decrypt()
Como dice Scott , Mcrypt no es una buena idea ya que no se ha actualizado desde 2007.
Incluso existe un RFC para eliminar Mcrypt de PHP: https://wiki.php.net/rfc/mcrypt-viking-funeral
Utilice mcrypt_encrypt()
y mcrypt_decrypt()
con los parámetros correspondientes. Realmente fácil y sencillo, y utiliza un paquete de cifrado probado en batalla.
EDITAR
5 años y 4 meses después de esta respuesta, la mcrypt
extensión ahora está en proceso de desaprobación y eventual eliminación de PHP.