Importante : a menos que tenga un caso de uso muy particular, no cifre las contraseñas , utilice un algoritmo de hash de contraseña. Cuando alguien dice que cifran sus contraseñas en una aplicación del lado del servidor, no están informados o describen un diseño de sistema peligroso. El almacenamiento seguro de contraseñas es un problema totalmente separado del cifrado.
Ser informado. Diseñar sistemas seguros.
Cifrado de datos portátil en PHP
Si está utilizando PHP 5.4 o más reciente y no desea escribir un módulo de criptografía usted mismo, le recomiendo usar una biblioteca existente que proporcione cifrado autenticado . La biblioteca que vinculé se basa solo en lo que PHP proporciona y está bajo revisión periódica por un puñado de investigadores de seguridad. (Yo incluido)
Si sus objetivos de portabilidad no impiden requerir extensiones PECL, se recomienda encarecidamente libsodium sobre cualquier cosa que usted o yo podamos escribir en PHP.
Actualización (2016-06-12): ahora puede usar sodium_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.
Primero, debe tomarse el tiempo para aprender los peligros del cifrado no autenticado y el Principio de Doom criptográfico .
- Los datos cifrados pueden ser manipulados por un usuario malintencionado.
- 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 ninguna razón para creer que el tamaño de la clave AES es un tema importante por el 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 estamos utilizando mcrypt
porque es abandonware y tiene errores sin parches que pueden afectar la seguridad. Por estas razones, animo a otros desarrolladores de PHP a evitarlo también.
Contenedor 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 biblioteca criptográfica simple anterior aún no es segura de usar. Necesitamos autenticar los textos cifrados y verificarlos antes de descifrarlos .
Nota : Por defecto, UnsafeCrypto::encrypt()
devolverá una cadena binaria sin formato. Llámalo así si necesitas 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
Contenedor 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);
Demos : binario sin procesar , 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 contactar a sus criptógrafos residentes para obtener una segunda opinión antes de hacerlo. Podrán contarte sobre errores de los que quizás ni siquiera sea consciente.
Estará mucho mejor usando una biblioteca de criptografía de buena reputación .