mcrypt está en desuso, ¿cuál es la alternativa?

103

La extensión mcrypt está en desuso y se eliminará en PHP 7.2 de acuerdo con el comentario publicado aquí . Así que estoy buscando una forma alternativa de cifrar las contraseñas.

Ahora mismo estoy usando algo como

mcrypt_encrypt(MCRYPT_RIJNDAEL_128, md5($key, true), $string, MCRYPT_MODE_CBC, $iv)

Necesito su opinión sobre la forma mejor / más segura de cifrar contraseñas, la contraseña cifrada debería, por supuesto, ser compatible con PHP 7.xx y también debería ser descifrable porque mis clientes quieren tener una opción para 'recuperar' sus contraseñas sin generar una nueva uno.

Piet
fuente
9
¿Por qué necesita cifrar / descifrar contraseñas? ¿Por qué no simplemente codificarlos password_hashy verificarlos password_verify?
Don't Panic
3
"la contraseña cifrada también debería ser descifrable" , ¿por qué? no suena demasiado seguro. ¿Alguna razón especial?
Funk Forty Niner
24
"porque mis clientes quieren tener la opción de 'recuperar' sus contraseñas sin generar una nueva". - Eso no es seguro y se les debería dar la opción de restablecer sus contraseñas.
Funk Forty Niner
4
No cifre las contraseñas , cuando el atacante obtenga la base de datos, también obtendrá la clave de cifrado. Repetir sobre un HMAC con una sal aleatoria durante aproximadamente 100 ms y guardar la sal con el hachís. Utilice funciones como password_hash, PBKDF2, Bcrypt y funciones similares. El punto es hacer que el atacante pase mucho tiempo buscando contraseñas por fuerza bruta.
zaph
2
Desde el manual de php -> Esta función ha sido DEPRECADA a partir de PHP 7.1.0. No se recomienda confiar en esta función. La alternativa es el sodio -> php.net/manual/en/book.sodium.php
MarcoZen

Respuestas:

47

Es una buena práctica aplicar hash a las contraseñas para que no se puedan descifrar. Esto hace que las cosas sean un poco más difíciles para los atacantes que pueden haber obtenido acceso a su base de datos o archivos.

Si debe cifrar sus datos y hacer que se puedan descifrar, hay disponible una guía para el cifrado / descifrado seguro en https://paragonie.com/white-paper/2015-secure-php-data-encryption . Para resumir ese enlace:

  • Utilice Libsodium : una extensión de PHP
  • Si no puede usar Libsodium, use defuse / php-encryption - Código PHP directo
  • Si no puede usar Libsodium o desactivar / php-encryption, use OpenSSL : muchos servidores ya lo tendrán instalado. De lo contrario, se puede compilar con --with-openssl [= DIR]
Phil
fuente
1
Primero debería probar openssl porque es muy común, donde libsodium no lo es. Php prima no debe utilizarse a menos que todos extensión nativa están fuera si la pregunta
JSON
aunque openssl es muy común, parece que php 7 usará libsodium para su criptografía principal securityintelligence.com/news/…
shadi
1
Tenga en cuenta que hay una biblioteca llamada Sodium-compat( github.com/paragonie/sodium_compat ) que funciona en PHP> = 5.2.4
RaelB
30

Como lo sugiere @rqLizard , puede usar funciones openssl_encrypt/ openssl_decryptPHP en su lugar, lo que proporciona una alternativa mucho mejor para implementar AES (The Advanced Encryption Standard), también conocido como cifrado Rijndael.

Según el siguiente comentario de Scott en php.net :

Si está escribiendo código para cifrar / cifrar datos en 2015, debe usar openssl_encrypt()y openssl_decrypt(). La biblioteca subyacente ( libmcrypt) ha sido abandonada desde 2007 y funciona mucho peor que OpenSSL (que aprovechaAES-NI los procesadores modernos y es seguro para el tiempo de caché).

Además, MCRYPT_RIJNDAEL_256no lo es AES-256, es una variante diferente del cifrado de bloques de Rijndael. Si desea que AES-256en mcrypt, usted tiene que utilizar MCRYPT_RIJNDAEL_128con una clave de 32 bytes. OpenSSL hace que sea más obvio qué modo está utilizando (es decir, aes-128-cbcvs aes-256-ctr).

OpenSSL también usa el relleno PKCS7 con el modo CBC en lugar del relleno de bytes NULL de mcrypt. Por lo tanto, es más probable que mcrypt haga que su código sea más vulnerable a los ataques de relleno de Oracle que OpenSSL.

Finalmente, si no está autenticando sus textos cifrados (Encriptar luego MAC), lo está haciendo mal.

Otras lecturas:

Ejemplos de código

Ejemplo 1

Ejemplo de cifrado autenticado AES en modo GCM para PHP 7.1+

<?php
//$key should have been previously generated in a cryptographically safe way, like openssl_random_pseudo_bytes
$plaintext = "message to be encrypted";
$cipher = "aes-128-gcm";
if (in_array($cipher, openssl_get_cipher_methods()))
{
    $ivlen = openssl_cipher_iv_length($cipher);
    $iv = openssl_random_pseudo_bytes($ivlen);
    $ciphertext = openssl_encrypt($plaintext, $cipher, $key, $options=0, $iv, $tag);
    //store $cipher, $iv, and $tag for decryption later
    $original_plaintext = openssl_decrypt($ciphertext, $cipher, $key, $options=0, $iv, $tag);
    echo $original_plaintext."\n";
}
?>

Ejemplo # 2

Ejemplo de cifrado autenticado AES para PHP 5.6+

<?php
//$key previously generated safely, ie: openssl_random_pseudo_bytes
$plaintext = "message to be encrypted";
$ivlen = openssl_cipher_iv_length($cipher="AES-128-CBC");
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext_raw = openssl_encrypt($plaintext, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv);
$hmac = hash_hmac('sha256', $ciphertext_raw, $key, $as_binary=true);
$ciphertext = base64_encode( $iv.$hmac.$ciphertext_raw );

//decrypt later....
$c = base64_decode($ciphertext);
$ivlen = openssl_cipher_iv_length($cipher="AES-128-CBC");
$iv = substr($c, 0, $ivlen);
$hmac = substr($c, $ivlen, $sha2len=32);
$ciphertext_raw = substr($c, $ivlen+$sha2len);
$original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv);
$calcmac = hash_hmac('sha256', $ciphertext_raw, $key, $as_binary=true);
if (hash_equals($hmac, $calcmac))//PHP 5.6+ timing attack safe comparison
{
    echo $original_plaintext."\n";
}
?>

Ejemplo # 3

Según los ejemplos anteriores, he cambiado el siguiente código que tiene como objetivo encriptar la identificación de sesión del usuario:

class Session {

  /**
   * Encrypts the session ID and returns it as a base 64 encoded string.
   *
   * @param $session_id
   * @return string
   */
  public function encrypt($session_id) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Encrypt the session ID.
    $encrypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $session_id, MCRYPT_MODE_CBC, $iv);
    // Base 64 encode the encrypted session ID.
    $encryptedSessionId = base64_encode($encrypt);
    // Return it.
    return $encryptedSessionId;
  }

  /**
   * Decrypts a base 64 encoded encrypted session ID back to its original form.
   *
   * @param $encryptedSessionId
   * @return string
   */
  public function decrypt($encryptedSessionId) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Decode the encrypted session ID from base 64.
    $decoded = base64_decode($encryptedSessionId);
    // Decrypt the string.
    $decryptedSessionId = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $decoded, MCRYPT_MODE_CBC, $iv);
    // Trim the whitespace from the end.
    $session_id = rtrim($decryptedSessionId, "\0");
    // Return it.
    return $session_id;
  }

  public function _getIv() {
    return md5($this->_getSalt());
  }

  public function _getSalt() {
    return md5($this->drupal->drupalGetHashSalt());
  }

}

dentro:

class Session {

  const SESS_CIPHER = 'aes-128-cbc';

  /**
   * Encrypts the session ID and returns it as a base 64 encoded string.
   *
   * @param $session_id
   * @return string
   */
  public function encrypt($session_id) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Encrypt the session ID.
    $ciphertext = openssl_encrypt($session_id, self::SESS_CIPHER, $key, $options=OPENSSL_RAW_DATA, $iv);
    // Base 64 encode the encrypted session ID.
    $encryptedSessionId = base64_encode($ciphertext);
    // Return it.
    return $encryptedSessionId;
  }

  /**
   * Decrypts a base 64 encoded encrypted session ID back to its original form.
   *
   * @param $encryptedSessionId
   * @return string
   */
  public function decrypt($encryptedSessionId) {
    // Get the Drupal hash salt as a key.
    $key = $this->_getSalt();
    // Get the iv.
    $iv = $this->_getIv();
    // Decode the encrypted session ID from base 64.
    $decoded = base64_decode($encryptedSessionId, TRUE);
    // Decrypt the string.
    $decryptedSessionId = openssl_decrypt($decoded, self::SESS_CIPHER, $key, $options=OPENSSL_RAW_DATA, $iv);
    // Trim the whitespace from the end.
    $session_id = rtrim($decryptedSessionId, '\0');
    // Return it.
    return $session_id;
  }

  public function _getIv() {
    $ivlen = openssl_cipher_iv_length(self::SESS_CIPHER);
    return substr(md5($this->_getSalt()), 0, $ivlen);
  }

  public function _getSalt() {
    return $this->drupal->drupalGetHashSalt();
  }

}

Para aclarar, el cambio anterior no es una verdadera conversión, ya que los dos cifrados usan un tamaño de bloque diferente y datos cifrados diferentes. Además, el relleno predeterminado es diferente, MCRYPT_RIJNDAELsolo admite relleno nulo no estándar. @zaph


Notas adicionales (de los comentarios de @ zaph):

  • Rijndael 128 ( MCRYPT_RIJNDAEL_128) es equivalente a AES , sin embargo, Rijndael 256 ( MCRYPT_RIJNDAEL_256) no es AES-256 ya que 256 especifica un tamaño de bloque de 256 bits, mientras que AES tiene solo un tamaño de bloque: 128 bits. Entonces, básicamente, Rijndael con un tamaño de bloque de 256 bits ( MCRYPT_RIJNDAEL_256) ha sido nombrado erróneamente debido a las elecciones de los desarrolladores de mcrypt . @zaph
  • Rijndael con un tamaño de bloque de 256 puede ser menos seguro que con un tamaño de bloque de 128 bits porque este último ha tenido muchas más revisiones y usos. En segundo lugar, la interoperabilidad se ve obstaculizada porque, si bien AES está generalmente disponible, Rijndael con un tamaño de bloque de 256 bits no lo está.
  • El cifrado con diferentes tamaños de bloque para Rijndael produce diferentes datos cifrados.

    Por ejemplo, MCRYPT_RIJNDAEL_256(no equivalente a AES-256) define una variante diferente del cifrado de bloque de Rijndael con un tamaño de 256 bits y un tamaño de clave basado en la clave pasada, donde aes-256-cbces Rijndael con un tamaño de bloque de 128 bits con un tamaño de clave de 256 bits. Por lo tanto, están usando diferentes tamaños de bloque que producen datos encriptados completamente diferentes ya que mcrypt usa el número para especificar el tamaño del bloque, donde OpenSSL usó el número para especificar el tamaño de la clave (AES solo tiene un tamaño de bloque de 128 bits). Básicamente, AES es Rijndael con un tamaño de bloque de 128 bits y tamaños de clave de 128, 192 y 256 bits. Por lo tanto, es mejor usar AES, que se llama Rijndael 128 en OpenSSL.

Kenorb
fuente
1
En general, usar Rijndael con un tamaño de bloque de 256 bits es un error debido a las elecciones de los desarrolladores de mcrypt. Además, Rijndael con un tamaño de bloque de 256 puede ser menos seguro que con un tamaño de bloque de 128 bits porque este último ha tenido mucha más revisión y uso. Además, la interoperabilidad se ve obstaculizada porque, si bien AES está disponible generalmente, Rijndael con un tamaño de bloque de 256 bits no lo está.
zaph
¿Por qué $session_id = rtrim($decryptedSessionId, "\0");? ¿Es posible openssl_decryptdevolver algunos personajes no deseados al final? ¿Qué pasa si los extremos variables cifradas con 0 (es decir encrypt("abc0")?
hlscalon
@hiscalon "\0"no es "0"más que el carácter NULL, cuyo código ASCII es 0x00 (hexadecimal 0).
kiamlaluno
11

La implementación de PHP puro de Rijndael existe con phpseclib disponible como paquete composer y funciona en PHP 7.3 (probado por mí).

Hay una página en los documentos de phpseclib, que genera código de muestra después de ingresar las variables básicas (cifrado, modo, tamaño de clave, tamaño de bit). Produce lo siguiente para Rijndael, ECB, 256, 256:

un código con mycrypt

$decoded = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, ENCRYPT_KEY, $term, MCRYPT_MODE_ECB);

funciona así con la biblioteca

$rijndael = new \phpseclib\Crypt\Rijndael(\phpseclib\Crypt\Rijndael::MODE_ECB);
$rijndael->setKey(ENCRYPT_KEY);
$rijndael->setKeyLength(256);
$rijndael->disablePadding();
$rijndael->setBlockLength(256);

$decoded = $rijndael->decrypt($term);

* $termfuebase64_decoded

Pentium10
fuente
11

Como se detalla en otras respuestas aquí, la mejor solución que encontré es usar OpenSSL. Está integrado en PHP y no necesita ninguna biblioteca externa. A continuación, se muestran ejemplos sencillos:

Para cifrar:

function encrypt($key, $payload) {
  $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
  $encrypted = openssl_encrypt($payload, 'aes-256-cbc', $key, 0, $iv);
  return base64_encode($encrypted . '::' . $iv);
}

Para descifrar:

function decrypt($key, $garble) {
    list($encrypted_data, $iv) = explode('::', base64_decode($garble), 2);
    return openssl_decrypt($encrypted_data, 'aes-256-cbc', $key, 0, $iv);
}

Enlace de referencia: https://www.shift8web.ca/2017/04/how-to-encrypt-and-execute-your-php-code-with-mcrypt/

Ariston Cordeiro
fuente
¡Mucho buen karma para ti amigo! Solo una cosa: si la contraseña, por ejemplo, se cifró con el código anterior, el nuevo código de descifrado no podrá verificarlo. Tiene que volver a guardarse y cifrarse con este nuevo código.
Lumis
Un simple script de migración resolvería ese problema. Utilice la forma antigua de descifrar y luego la nueva forma de cifrar y almacenar. La alternativa es agregar una bandera a la tabla del usuario y programar un restablecimiento de contraseña forzado en todas las cuentas de usuario que necesitan cambios de contraseña.
cecil merrel también conocido como bringrainfire
8

Puede usar el paquete phpseclib pollyfill. No puede usar open ssl o libsodium para cifrar / descifrar con rijndael 256. Otro problema, no necesita reemplazar ningún código.

Ahmet Erkan ÇELİK
fuente
2
Esto fue muy útil gracias. Tuve que eliminar la extensión php-mcrypt, y luego esto funciona como un encanto.
DannyB
Lo instalé mcrypt_compatejecutando, composer require phpseclib/mcrypt_compatpero PHP Fatal error: Uncaught Error: Call to undefined function mcrypt_get_key_size() in /app/kohana/classes/Kohana/Encrypt.php:124sigo usando php 7.2.26y el marco de Kohana. ¿Hay más pasos que realizar después de instalarlo con Composer?
M-Dahab
Entendido. Tienes que agregar require APPPATH . '/vendor/autoload.php';al final de bootstrap.php.
M-Dahab
3

Debería utilizar OpenSSL, mcryptya que se desarrolla y mantiene de forma activa. Proporciona mayor seguridad, facilidad de mantenimiento y portabilidad. En segundo lugar, realiza el cifrado / descifrado AES mucho más rápido. Utiliza relleno PKCS7 de forma predeterminada, pero puede especificar OPENSSL_ZERO_PADDINGsi lo necesita. Para usar con una clave binaria de 32 bytes, puede especificar aes-256-cbccuál es mucho más obvia queMCRYPT_RIJNDAEL_128 .

Aquí está el ejemplo de código usando Mcrypt:

Biblioteca de cifrado AES-256-CBC no autenticada escrita en Mcrypt con relleno PKCS7.

/**
 * This library is unsafe because it does not MAC after encrypting
 */
class UnsafeMcryptAES
{
    const CIPHER = MCRYPT_RIJNDAEL_128;

    public static function encrypt($message, $key)
    {
        if (mb_strlen($key, '8bit') !== 32) {
            throw new Exception("Needs a 256-bit key!");
        }
        $ivsize = mcrypt_get_iv_size(self::CIPHER);
        $iv = mcrypt_create_iv($ivsize, MCRYPT_DEV_URANDOM);

        // Add PKCS7 Padding
        $block = mcrypt_get_block_size(self::CIPHER);
        $pad = $block - (mb_strlen($message, '8bit') % $block, '8bit');
        $message .= str_repeat(chr($pad), $pad);

        $ciphertext = mcrypt_encrypt(
            MCRYPT_RIJNDAEL_128,
            $key,
            $message,
            MCRYPT_MODE_CBC,
            $iv
        );

        return $iv . $ciphertext;
    }

    public static function decrypt($message, $key)
    {
        if (mb_strlen($key, '8bit') !== 32) {
            throw new Exception("Needs a 256-bit key!");
        }
        $ivsize = mcrypt_get_iv_size(self::CIPHER);
        $iv = mb_substr($message, 0, $ivsize, '8bit');
        $ciphertext = mb_substr($message, $ivsize, null, '8bit');

        $plaintext = mcrypt_decrypt(
            MCRYPT_RIJNDAEL_128,
            $key,
            $ciphertext,
            MCRYPT_MODE_CBC,
            $iv
        );

        $len = mb_strlen($plaintext, '8bit');
        $pad = ord($plaintext[$len - 1]);
        if ($pad <= 0 || $pad > $block) {
            // Padding error!
            return false;
        }
        return mb_substr($plaintext, 0, $len - $pad, '8bit');
    }
}

Y aquí está la versión escrita usando OpenSSL:

/**
 * This library is unsafe because it does not MAC after encrypting
 */
class UnsafeOpensslAES
{
    const METHOD = 'aes-256-cbc';

    public static function encrypt($message, $key)
    {
        if (mb_strlen($key, '8bit') !== 32) {
            throw new Exception("Needs a 256-bit key!");
        }
        $ivsize = openssl_cipher_iv_length(self::METHOD);
        $iv = openssl_random_pseudo_bytes($ivsize);

        $ciphertext = openssl_encrypt(
            $message,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $iv
        );

        return $iv . $ciphertext;
    }

    public static function decrypt($message, $key)
    {
        if (mb_strlen($key, '8bit') !== 32) {
            throw new Exception("Needs a 256-bit key!");
        }
        $ivsize = openssl_cipher_iv_length(self::METHOD);
        $iv = mb_substr($message, 0, $ivsize, '8bit');
        $ciphertext = mb_substr($message, $ivsize, null, '8bit');

        return openssl_decrypt(
            $ciphertext,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $iv
        );
    }
}

Fuente: Si está escribiendo la palabra MCRYPT en su código PHP, lo está haciendo mal .

Kenorb
fuente
2

Estoy usando esto en PHP 7.2.x, funciona bien para mí:

public function make_hash($userStr){
        try{
            /** 
             * Used and tested on PHP 7.2x, Salt has been removed manually, it is now added by PHP 
             */
             return password_hash($userStr, PASSWORD_BCRYPT);
            }catch(Exception $exc){
                $this->tempVar = $exc->getMessage();
                return false;
            }
        }

y luego autenticar el hash con la siguiente función:

public function varify_user($userStr,$hash){
        try{
            if (password_verify($userStr, $hash)) {
                 return true;
                }
            else {
                return false;
                }
            }catch(Exception $exc){
                $this->tempVar = $exc->getMessage();
                return false;
            }
        }

Ejemplo:

  //create hash from user string

 $user_password = $obj->make_hash2($user_key);

y para autenticar este hash use el siguiente código:

if($obj->varify_user($key, $user_key)){
      //this is correct, you can proceed with  
    }

Eso es todo.

Abdul Rahman
fuente
1

Como se señaló, no debería almacenar las contraseñas de sus usuarios en un formato que se pueda descifrar. El cifrado reversible proporciona una ruta fácil para que los piratas informáticos encuentren las contraseñas de sus usuarios, lo que se extiende a poner en riesgo las cuentas de sus usuarios en otros sitios en caso de que usen la misma contraseña allí.

PHP proporciona un par de funciones poderosas para el cifrado de hash unidireccional con sal aleatorio, password_hash()y password_verify(). Debido a que el hash es automáticamente salado al azar, no hay forma de que los piratas informáticos utilicen tablas precompiladas de hashes de contraseñas para aplicar ingeniería inversa a la contraseña. Establezca la PASSWORD_DEFAULTopción y las versiones futuras de PHP usarán automáticamente algoritmos más fuertes para generar hashes de contraseña sin que tenga que actualizar su código.

Thoracius Appotite
fuente
1

Deberías usar openssl_encrypt()function.

rqLizard
fuente
¿El cifrado de openssl en php 7 tiene el "heartbleed"?
TheCrazyProfessor
13
¿Por qué el OP debería usar openssl_encrypt? Dar algunos detalles y antecedentes
Martin
0

Pude traducir mi objeto Crypto

  • Obtenga una copia de php con mcrypt para descifrar los datos antiguos. Fui a http://php.net/get/php-7.1.12.tar.gz/from/a/mirror , lo compilé y luego agregué la extensión ext / mcrypt (configure; make; make install). Creo que también tuve que agregar la línea extenstion = mcrypt.so al php.ini. Una serie de scripts para crear versiones intermedias de los datos con todos los datos sin cifrar.

  • Construya una clave pública y privada para openssl

    openssl genrsa -des3 -out pkey.pem 2048
    (set a password)
    openssl rsa -in pkey.pem -out pkey-pub.pem -outform PEM -pubout
  • Para cifrar (usando la clave pública) use openssl_seal. Por lo que he leído, openssl_encrypt usando una clave RSA está limitado a 11 bytes menos que la longitud de la clave (Ver http://php.net/manual/en/function.openssl-public-encrypt.php comentario de Thomas Horsten)

    $pubKey = openssl_get_publickey(file_get_contents('./pkey-pub.pem'));
    openssl_seal($pwd, $sealed, $ekeys, [ $pubKey ]);
    $encryptedPassword = base64_encode($sealed);
    $key = base64_encode($ekeys[0]);

Probablemente podría almacenar el binario en bruto.

  • Para descifrar (usando clave privada)

    $passphrase="passphrase here";
    $privKey = openssl_get_privatekey(file_get_contents('./pkey.pem'), $passphrase);
    // I base64_decode() from my db columns
    openssl_open($encryptedPassword, $plain, $key, $privKey);
    echo "<h3>Password=$plain</h3>";

PD: no puedes cifrar la cadena vacía ("")

PPS Esto es para una base de datos de contraseñas, no para la validación del usuario.

Joshua Goldstein
fuente