mcrypt está obsoleto, qual é a alternativa?

103

A extensão mcrypt está obsoleta e será removida no PHP 7.2 de acordo com o comentário postado aqui . Portanto, estou procurando uma maneira alternativa de criptografar senhas.

No momento estou usando algo como

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

Preciso da sua opinião sobre a maneira melhor / mais forte de criptografar senhas, a senha criptografada deve, é claro, ser compatível com o PHP 7.xx e também deve ser descriptografável porque meus clientes querem ter a opção de 'recuperar' suas senhas sem gerar um novo 1.

Piet
fonte
9
Por que você precisa criptografar / descriptografar senhas? Por que não apenas hash password_hashe verificá-los com password_verify?
Não entre em pânico
3
"a senha criptografada também deve ser descriptografável" - por quê? não parece muito seguro. Algum motivo especial?
Funk Forty Niner
24
"porque meus clientes querem ter a opção de 'recuperar' suas senhas sem gerar uma nova." - Isso não é seguro e eles devem ter a opção de redefinir suas senhas.
Funk Forty Niner
4
Não criptografe as senhas , pois quando o invasor obtiver o banco de dados, também obterá a chave de criptografia. Itere sobre um HMAC com um sal aleatório por cerca de 100 ms de duração e salve o sal com o hash. Use funções como password_hash, PBKDF2, Bcrypt e funções semelhantes. O objetivo é fazer com que o invasor gaste muito tempo encontrando senhas por meio de força bruta.
zaph
2
Do manual do php -> Esta função foi DESCONTINUADA a partir do PHP 7.1.0. Depender desta função é altamente desencorajado. A alternativa é sódio -> php.net/manual/en/book.sodium.php
MarcoZen

Respostas:

47

É uma prática recomendada fazer hash das senhas para que não sejam descriptografáveis. Isso torna as coisas um pouco mais difíceis para os invasores que podem ter obtido acesso ao seu banco de dados ou arquivos.

Se você precisar criptografar seus dados e torná-los descriptografáveis, um guia para criptografia / descriptografia segura está disponível em https://paragonie.com/white-paper/2015-secure-php-data-encryption . Para resumir esse link:

  • Use Libsodium - uma extensão do PHP
  • Se você não pode usar Libsodium, use defuse / php-encryption - código PHP direto
  • Se você não pode usar Libsodium ou defuse / php-encryption, use OpenSSL - muitos servidores já o terão instalado. Caso contrário, pode ser compilado com --with-openssl [= DIR]
Phil
fonte
1
Deve primeiro tentar o openssl porque é muito comum, mas o libsodium não. O php bruto não deve ser usado a menos que todas as extensões nativas estejam fora da questão
JSON
embora o openssl seja muito comum, parece que o php 7 estará usando libsodium para sua criptografia central securityintelligence.com/news/…
shadi
1
Observe que há uma biblioteca chamada Sodium-compat( github.com/paragonie/sodium_compat ) que funciona em PHP> = 5.2.4
RaelB
30

Conforme sugerido por @rqLizard , você pode usar funções openssl_encrypt/ openssl_decryptPHP em vez disso, o que fornece uma alternativa muito melhor para implementar AES (The Advanced Encryption Standard) também conhecido como criptografia Rijndael.

De acordo com o seguinte comentário de Scott em php.net :

Se você estiver escrevendo código para criptografar / criptografar dados em 2015, deve usar openssl_encrypt()e openssl_decrypt(). A biblioteca subjacente ( libmcrypt) foi abandonada desde 2007 e tem um desempenho muito pior do que o OpenSSL (que aproveitaAES-NI de processadores modernos e é seguro para temporização de cache).

Além disso, MCRYPT_RIJNDAEL_256não é AES-256, é uma variante diferente da cifra de bloco Rijndael. Se você quiser AES-256em mcrypt, você tem que usar MCRYPT_RIJNDAEL_128com uma chave de 32 bytes. O OpenSSL torna mais óbvio qual modo você está usando (ou seja, aes-128-cbcvs aes-256-ctr).

OpenSSL também usa preenchimento PKCS7 com modo CBC em vez do preenchimento de bytes NULL do mcrypt. Portanto, é mais provável que mcrypt torne seu código vulnerável a ataques oracle padding do que OpenSSL.

Finalmente, se você não está autenticando seus textos criptografados (Encrypt Then MAC), você está fazendo isso errado.

Leitura adicional:

Exemplos de código

Exemplo 1

AES Authenticated Encryption in GCM mode example for 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";
}
?>

Exemplo # 2

Exemplo de criptografia autenticada 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";
}
?>

Exemplo # 3

Com base nos exemplos acima, alterei o código a seguir, que visa criptografar a id de sessão do usuário:

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());
  }

}

para 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 esclarecer, a alteração acima não é uma conversão verdadeira, pois as duas criptografias usam um tamanho de bloco diferente e dados criptografados diferentes. Além disso, o preenchimento padrão é diferente, MCRYPT_RIJNDAELsuporta apenas preenchimento nulo não padrão. @zaph


Observações adicionais (dos comentários de @zaph):

  • Rijndael 128 ( MCRYPT_RIJNDAEL_128) é equivalente a AES , entretanto Rijndael 256 ( MCRYPT_RIJNDAEL_256) não é AES-256, pois 256 especifica um tamanho de bloco de 256 bits, enquanto AES tem apenas um tamanho de bloco: 128 bits. Então, basicamente, Rijndael com um tamanho de bloco de 256 bits ( MCRYPT_RIJNDAEL_256) foi nomeado por engano devido às escolhas dos desenvolvedores do mcrypt . @zaph
  • Rijndael com um tamanho de bloco de 256 pode ser menos seguro do que com um tamanho de bloco de 128 bits porque o último teve muito mais análises e utilizações. Em segundo lugar, a interoperabilidade é prejudicada porque, embora o AES esteja geralmente disponível, o Rijndael com um tamanho de bloco de 256 bits não está.
  • A criptografia com tamanhos de bloco diferentes para Rijndael produz dados criptografados diferentes.

    Por exemplo, MCRYPT_RIJNDAEL_256(não equivalente a AES-256) define uma variante diferente da cifra de bloco Rijndael com tamanho de 256 bits e um tamanho de chave baseado na chave passada, onde aes-256-cbcé Rijndael com um tamanho de bloco de 128 bits com um tamanho de chave de 256 bits. Portanto, eles estão usando tamanhos de bloco diferentes que produzem dados criptografados totalmente diferentes, já que mcrypt usa o número para especificar o tamanho do bloco, onde o OpenSSL usa o número para especificar o tamanho da chave (AES só tem um tamanho de bloco de 128 bits). Então, basicamente, AES é Rijndael com um tamanho de bloco de 128 bits e tamanhos de chave de 128, 192 e 256 bits. Portanto, é melhor usar AES, que é chamado de Rijndael 128 no OpenSSL.

Kenorb
fonte
1
Em geral, usar Rijndael com um tamanho de bloco de 256 bits é um erro devido às escolhas dos desenvolvedores do mcrypt. Além disso, o Rijndael com um tamanho de bloco de 256 pode ser menos seguro do que com um tamanho de bloco de 128 bits porque o último teve muito mais revisão e uso. Além disso, a interoperabilidade é prejudicada porque, embora o AES esteja geralmente disponível, o Rijndael com um tamanho de bloco de 256 bits não está.
zaph 05 de
Por que você quer $session_id = rtrim($decryptedSessionId, "\0");? É possível openssl_decryptretornar alguns personagens indesejados no final? E se a variável criptografada terminar com 0 (ou seja encrypt("abc0")?
hlscalon
@hiscalon "\0"não é "0"senão o caractere NULL, cujo código ASCII é 0x00 (hexadecimal 0).
kiamlaluno
11

Implementação Pure-PHP de Rijndael existe com phpseclib disponível como pacote composer e funciona no PHP 7.3 (testado por mim).

Há uma página na documentação do phpseclib, que gera um código de amostra depois que você insere as variáveis ​​básicas (cifra, modo, tamanho da chave, tamanho do bit). Ele produz o seguinte para Rijndael, ECB, 256, 256:

um código com mycrypt

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

funciona assim com a 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);

* $termerabase64_decoded

Pentium10
fonte
11

Conforme detalhado por outras respostas aqui, a melhor solução que encontrei é usar OpenSSL. Ele é integrado ao PHP e você não precisa de nenhuma biblioteca externa. Aqui estão alguns exemplos simples:

Para criptografar:

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 descriptografar:

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

Link de referência: https://www.shift8web.ca/2017/04/how-to-encrypt-and-execute-your-php-code-with-mcrypt/

Ariston Cordeiro
fonte
Muito carma bom para você, amigo! Só uma coisa: se a senha, por exemplo, foi criptografada com o código antigo, o novo código de descriptografia não conseguirá verificá-la. Ele deve ser salvo novamente e criptografado com este novo código.
Lumis
Um script de migração simples resolveria esse problema. Use a maneira antiga de descriptografar e a nova maneira de criptografar e armazenar. A alternativa é adicionar um sinalizador à tabela do usuário e criar um script em uma redefinição de senha forçada em todas as contas de usuário que precisam de alterações de senha.
cecil merrel também conhecido como bringrainfire
8

Você pode usar o pacote phpseclib pollyfill. Você não pode usar open ssl ou libsodium para criptografar / descriptografar com rijndael 256. Outro problema, você não precisa substituir nenhum código.

Ahmet Erkan ÇELİK
fonte
2
Isso foi super útil, obrigado. Tive que remover a extensão php-mcrypt, e então isso funcionou perfeitamente.
DannyB
Eu instalei mcrypt_compatexecutando, composer require phpseclib/mcrypt_compatmas continuo PHP Fatal error: Uncaught Error: Call to undefined function mcrypt_get_key_size() in /app/kohana/classes/Kohana/Encrypt.php:124usando php 7.2.26e Kohana framwork. Há mais etapas a serem executadas após instalá-lo com o composer?
M-Dahab
Entendi. Você tem que adicionar require APPPATH . '/vendor/autoload.php';ao final de bootstrap.php.
M-Dahab
3

Você deve usar o OpenSSL mcryptconforme ele é desenvolvido e mantido ativamente. Ele fornece melhor segurança, facilidade de manutenção e portabilidade. Em segundo lugar, ele executa a criptografia / descriptografia AES muito mais rápido. Ele usa preenchimento PKCS7 por padrão, mas você pode especificar OPENSSL_ZERO_PADDINGse for necessário. Para usar com uma chave binária de 32 bytes, você pode especificar o aes-256-cbcque é muito mais óbvio do queMCRYPT_RIJNDAEL_128 .

Aqui está o exemplo de código usando Mcrypt:

Biblioteca de criptografia AES-256-CBC não autenticada escrita em Mcrypt com preenchimento 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');
    }
}

E aqui está a versão 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
        );
    }
}

Fonte: Se você está digitando a palavra MCRYPT em seu código PHP, está fazendo errado .

Kenorb
fonte
2

Estou usando no PHP 7.2.x, está funcionando bem para mim:

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;
            }
        }

e, em seguida, autenticar o hash com a seguinte função:

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;
            }
        }

Exemplo:

  //create hash from user string

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

e para autenticar este hash use o seguinte código:

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

Isso é tudo.

Abdul Rahman
fonte
1

Conforme mencionado, você não deve armazenar as senhas de seus usuários em um formato que possa ser decifrado. A criptografia reversível fornece um caminho fácil para os hackers descobrirem as senhas dos seus usuários, o que se estende a colocar em risco as contas dos seus usuários em outros sites, caso eles usem a mesma senha lá.

O PHP fornece um par de funções poderosas para criptografia hash unilateral com sal aleatório - password_hash()e password_verify(). Como o hash é automaticamente salgado aleatoriamente, não há como os hackers utilizarem tabelas pré-compiladas de hashes de senha para fazer a engenharia reversa da senha. Defina a PASSWORD_DEFAULTopção e as versões futuras do PHP usarão automaticamente algoritmos mais fortes para gerar hashes de senha sem que você precise atualizar seu código.

Thoracius Appotite
fonte
1

Você deve usar a openssl_encrypt()função.

rqLizard
fonte
O openssl criptografado no php 7 tem o "heartbleed"?
TheCrazyProfessor de
13
por que o OP deve usar openssl_encrypt? Forneça alguns detalhes e histórico
Martin,
0

Consegui traduzir meu objeto Crypto

  • Obtenha uma cópia do php com mcrypt para descriptografar os dados antigos. Eu fui para http://php.net/get/php-7.1.12.tar.gz/from/a/mirror , compilei, então adicionei a extensão ext / mcrypt (configure; make; make install). Acho que tive que adicionar a linha extenstion = mcrypt.so ao php.ini também. Uma série de scripts para construir versões intermediárias dos dados com todos os dados não criptografados.

  • Construa uma chave pública e privada para o openssl

    openssl genrsa -des3 -out pkey.pem 2048
    (set a password)
    openssl rsa -in pkey.pem -out pkey-pub.pem -outform PEM -pubout
  • Para criptografar (usando a chave pública), use openssl_seal. Pelo que li, o openssl_encrypt usando uma chave RSA é limitado a 11 bytes a menos que o comprimento da chave (veja o comentário http://php.net/manual/en/function.openssl-public-encrypt.php 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]);

Você provavelmente poderia armazenar o binário bruto.

  • Para descriptografar (usando chave 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>";

PS Você não pode criptografar a string vazia ("")

PPS Isso é para um banco de dados de senha, não para validação do usuário.

Joshua Goldstein
fonte