Cifrado bidireccional más simple usando PHP

230

¿Cuál es la forma más sencilla de hacer cifrado bidireccional en instalaciones PHP comunes?

Necesito poder cifrar datos con una clave de cadena y usar la misma clave para descifrar en el otro extremo.

La seguridad no es tan importante 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 guardar un montón de código innecesario.

usuario1206970
fuente
44
Solo XOR tu cuerda.
fix_moeller
3
Para el cifrado de uso general, use defuse / php-encryption / en lugar de rodar el suyo.
Scott Arciszewski,
2
Manos lejos de github.com/defuse/php-encryption : es más lento en órdenes de magnitud que mcrypt.
Eugen Rieck
1
@Scott Pensar en la línea de "esto probablemente no será el cuello de botella" es lo que nos trajo una gran cantidad de software malo.
Eugen Rieck
3
Si realmente está encriptando / desencriptando una gran cantidad de datos hasta el punto de que los milisegundos que cuesta atascan su aplicación, muerda la bala y cambie a libsodium. Sodium::crypto_secretbox()y Sodium::crypto_secretbox_open()son seguros y efectivos.
Scott Arciszewski

Respuestas:

196

Editado:

Realmente deberías estar usando openssl_encrypt () & openssl_decrypt ()

Como dice Scott , Mcrypt no es una buena idea, ya que no se ha actualizado desde 2007.

Incluso hay un RFC para eliminar Mcrypt de PHP - https://wiki.php.net/rfc/mcrypt-viking-funeral

472084
fuente
66
@EugenRieck Sí, ese es el punto. Mcrypt no recibe parches. OpenSSL recibe parches tan pronto como se descubre cualquier vulnerabilidad, grande o pequeña.
Greg
55
sería mejor para una respuesta tan votada, que allí se proporcionaran ejemplos más simples también en respuesta. gracias de cualquier manera.
T.Todua
chicos, solo FYI => MCRYPT ES DEPRECADO. para que todos sepan que no deben usarlo, ya que nos dio una gran cantidad de problemas. En desuso desde PHP 7.1 si no me equivoco.
clusterBuddy
Desde PHP 7, la función mcrypt se elimina de la base de código php. Entonces, cuando se usa la última versión de php (que debería ser estándar), ya no puede usar esta función obsoleta.
Alexander Behling
234

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 mcryptporque 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 SaferCryptobiblioteca 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 .

Scott Arciszewski
fuente
3
Entonces, solo estoy tratando de hacer que UnsafeCrypto funcione primero. El cifrado ocurre bien, pero cada vez que ejecuto el descifrado, obtengo 'falso' como respuesta. Estoy usando la misma clave para descifrar, y pasando true en la codificación, así como la decodificación. Hay, lo que supongo que es un tipo en el ejemplo, me pregunto si de ahí viene mi problema. ¿Puedes explicar de dónde viene la variable $ mac, y debería ser simplemente $ iv?
David C
1
@EugenRieck Las implementaciones de cifrado OpenSSL son probablemente las únicas partes que no son malas, y es la única forma de aprovechar AES-NI en PHP vainilla. Si instala en OpenBSD, PHP se compilará contra LibreSSL sin que el código PHP note una diferencia. Libsodium> OpenSSL cualquier día. Además, no use libmcrypt . ¿Qué recomendarías que usen los desarrolladores de PHP en lugar de OpenSSL?
Scott Arciszewski
2
Ya no se admiten ni 5.2 ni 5.3 . En su lugar, debería considerar actualizar a una versión compatible de PHP , como 5.6.
Scott Arciszewski el
1
@BBeta paragonie.com/blog/2015/09/…
Scott Arciszewski
1
Lo acabo de hacer como una demostración de que quieres cadenas binarias, no cadenas legibles por humanos, para tus llaves .
Scott Arciszewski
22

Uso mcrypt_encrypt()y mcrypt_decrypt()con los parámetros correspondientes. Realmente fácil y directo, y utiliza un paquete de cifrado probado en batalla.

EDITAR

5 años y 4 meses después de esta respuesta, la mcryptextensión ahora está en proceso de desaprobación y eventual eliminación de PHP.

Eugen Rieck
fuente
34
¿Batalla probada y no actualizada durante más de 8 años?
Maarten Bodewes
2
Bueno, mcrypt está en PHP7 y no está en desuso, eso es lo suficientemente bueno para mí. No todo el código es de la horrible calidad de OpenSSL y necesita parches cada pocos días.
Eugen Rieck
3
mcrypt no solo es horrible con respecto al soporte. Tampoco implementa las mejores prácticas como el relleno compatible con PKCS # 7, cifrado autenticado. No admitirá SHA-3 ni ningún otro algoritmo nuevo, ya que nadie lo mantiene, lo que le roba una ruta de actualización. Además, solía aceptar cosas como claves parciales, realizar relleno cero, etc. Hay una buena razón por la cual está en proceso de ser eliminado gradualmente de PHP.
Maarten Bodewes
2
En PHP 7.1, todas las funciones mcrypt_ * generarán un aviso E_DEPRECATED. En PHP 7.1 + 1 (ya sea 7.2 u 8.0), la extensión mcrypt se moverá fuera del núcleo hacia PECL, donde las personas que realmente quieran instalarla aún pueden hacerlo si pueden instalar extensiones PHP desde PECL.
Mladen Janjetovic
4

PHP 7.2 se alejó completamente Mcrypty el cifrado ahora se basa en la Libsodiumbiblioteca mantenible .

Todas sus necesidades de cifrado se pueden resolver básicamente a través de la Libsodiumbiblioteca.

// On Alice's computer:
$msg = 'This comes from Alice.';
$signed_msg = sodium_crypto_sign($msg, $secret_sign_key);


// On Bob's computer:
$original_msg = sodium_crypto_sign_open($signed_msg, $alice_sign_publickey);
if ($original_msg === false) {
    throw new Exception('Invalid signature');
} else {
    echo $original_msg; // Displays "This comes from Alice."
}

Documentación de Libsodium: https://github.com/paragonie/pecl-libsodium-doc

Hemerson Varela
fuente
2
Si pega algún código, asegúrese de que todas las variables estén cubiertas. En su ejemplo, $ secret_sign_key y $ alice_sign_publickey son NULL
undefinedman
1
La crypto_signAPI no cifra los mensajes, eso requerirá una de las crypto_aead_*_encryptfunciones.
Roger Dueck
1

IMPORTANTE esta respuesta es válida solo para PHP 5, en PHP 7 use funciones criptográficas incorporadas.

Aquí hay una implementación simple pero lo suficientemente segura:

  • Encriptación AES-256 en modo CBC
  • PBKDF2 para crear una clave de cifrado a partir de una contraseña de texto sin formato
  • HMAC para autenticar el mensaje cifrado.

El código y los ejemplos están aquí: https://stackoverflow.com/a/19445173/1387163

Eugene Fidelin
fuente
1
No soy un experto en criptografía, pero tener una clave derivada directamente de una contraseña parece una idea terrible. Tablas de arcoíris + contraseña débil y desaparecida es su seguridad. También su enlace apunta a funciones mcrypt, que están en desuso desde PHP 7.1
Alph.Dev
@ Alph.Dev tienes razón, la respuesta anterior solo es válida para PHP 5
Eugene Fidelin