mcrypt 已被弃用,还有什么选择?

mcrypt is deprecated, what is the alternative?

mcrypt 扩展名是 deprecated will be removed in PHP 7.2 according to the comment posted here。所以我正在寻找另一种加密密码的方法。

现在我正在使用类似

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

我需要你对 best/strongest 密码加密方式的意见,加密后的密码当然应该得到 PHP 7.xx 的支持,并且还应该是可解密的,因为我的客户确实想要可以选择 'recover' 他们的密码而不生成新密码。

最佳做法是散列密码,使它们不可解密。这让可能获得对您的数据库或文件的访问权限的攻击者的攻击变得更加困难。

如果您必须加密数据并使其可解密,https://paragonie.com/white-paper/2015-secure-php-data-encryption 上提供了 encryption/decryption 安全指南。总结一下 link:

  • 使用 Libsodium - PHP 扩展名
  • 如果您不能使用 Libsodium,请使用 defuse/php-encryption - 直接 PHP 代码
  • 如果您不能使用 Libsodium 或 defuse/php-encryption,请使用 OpenSSL - 很多服务器已经安装了这个。如果没有,可以用 --with-openssl[=DIR]
  • 编译

如前所述,您不应以可解密的格式存储用户密码。可逆加密为黑客找到您用户的密码提供了一条简单的途径,如果他们在其他站点使用相同的密码,这会扩展到使您的用户在其他站点的帐户面临风险。

PHP 为随机加盐、单向散列加密提供了一对强大的函数——password_hash()password_verify()。因为散列是自动随机加盐的,所以黑客无法利用预编译的密码散列表对密码进行逆向工程。设置 PASSWORD_DEFAULT 选项,PHP 的未来版本将自动使用更强大的算法来生成密码哈希,而无需更新代码。

您可以使用 phpseclib pollyfill 包。您不能使用 open ssl 或 libsodium for encrypt/decrypt with rijndael 256。 另一个问题,你不需要替换任何代码。

你应该使用 openssl_encrypt() 函数。

我能够翻译我的 Crypto 对象

  • 使用 mcrypt 获取 php 的副本以解密旧数据。我转到 http://php.net/get/php-7.1.12.tar.gz/from/a/mirror,编译它,然后添加 ext/mcrypt 扩展(配置;制作;制作安装)。我想我还必须将 extension=mcrypt.so 行添加到 php.ini 中。一系列脚本,用于构建数据的中间版本,所有数据均未加密。

  • 为 openssl

    构建 public 和私钥
    openssl genrsa -des3 -out pkey.pem 2048
    (set a password)
    openssl rsa -in pkey.pem -out pkey-pub.pem -outform PEM -pubout
    
  • 加密(使用 public 密钥)使用 openssl_seal。根据我的阅读,openssl_encrypt 使用 RSA 密钥被限制为比密钥长度少 11 个字节(参见 Thomas Horsten 的 http://php.net/manual/en/function.openssl-public-encrypt.php 评论)

    $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]);
    

您或许可以存储原始二进制文件。

  • 解密(使用私钥)

    $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>";
    

P.S。不能加密空串("")

P.P.S。这是用于密码数据库而不是用于用户验证。

正如 , you can use openssl_encrypt/openssl_decrypt PHP 所建议的那样,函数提供了很多 实施 AES(高级加密标准)也称为 Rijndael 加密的更好替代方案。

根据以下Scott's comment at php.net

If you're writing code to encrypt/encrypt data in 2015, you should use openssl_encrypt() and openssl_decrypt(). The underlying library (libmcrypt) has been abandoned since 2007, and performs far worse than OpenSSL (which leverages AES-NI on modern processors and is cache-timing safe).

Also, MCRYPT_RIJNDAEL_256 is not AES-256, it's a different variant of the Rijndael block cipher. If you want AES-256 in mcrypt, you have to use MCRYPT_RIJNDAEL_128 with a 32-byte key. OpenSSL makes it more obvious which mode you are using (i.e. aes-128-cbc vs aes-256-ctr).

OpenSSL also uses PKCS7 padding with CBC mode rather than mcrypt's NULL byte padding. Thus, mcrypt is more likely to make your code vulnerable to padding oracle attacks than OpenSSL.

Finally, if you are not authenticating your ciphertexts (Encrypt Then MAC), you're doing it wrong.

进一步阅读:

代码示例

示例 #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";
}
?>

示例 #2

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

示例 #3

基于以上示例,我更改了以下旨在加密用户会话 ID 的代码:

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, "[=12=]");
    // Return it.
    return $session_id;
  }

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

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

}

进入:

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, '[=13=]');
    // 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();
  }

}

需要澄清的是,上述更改不是真正的转换,因为两种加密使用不同的块大小和不同的加密数据。此外,默认填充不同,MCRYPT_RIJNDAEL 仅支持非标准空填充。


补充说明(来自@zaph 的评论):

  • Rijndael 128 (MCRYPT_RIJNDAEL_128) 等同于AES,但是Rijndael 256 (MCRYPT_RIJNDAEL_256) 不是 AES-256 因为256指定了一个块大小为 256 位,而 AES 只有一种块大小:128 位。因此,由于 mcrypt 开发人员的选择,基本上块大小为 256 位 (MCRYPT_RIJNDAEL_256) 的 Rijndael 被错误命名。 @zaph
  • 块大小为 256 位的 Rijndael 可能不如块大小为 128 位的块安全,因为后者有更多的评论和使用。其次,互操作性受到阻碍,因为虽然 AES 普遍可用,但块大小为 256 位的 Rijndael 却不可用。
  • Rijndael 不同块大小的加密产生不同的加密数据。

    例如,MCRYPT_RIJNDAEL_256(不等同于 AES-256)定义了 Rijndael 块密码的不同变体,其大小为 256 位,密钥大小基于传入的密钥,其中aes-256-cbc 是 Rijndael,块大小为 128 位,密钥大小为 256 位。因此,他们使用不同的块大小,这会产生完全不同的加密数据,因为 mcrypt 使用数字来指定块大小,而 OpenSSL 使用数字来指定密钥大小(AES 只有一个 128 位的块大小)。所以基本上 AES 是 Rijndael,块大小为 128 位,密钥大小为 128、192 和 256 位。因此最好使用AES,在OpenSSL中称为Rijndael 128。

您应该使用 OpenSSL 而不是 mcrypt,因为它正在积极开发和维护。它提供了更好的安全性、可维护性和可移植性。其次,它执行 AES encryption/decryption 的速度要快得多。它默认使用 PKCS7 填充,但如果需要,可以指定 OPENSSL_ZERO_PADDING。要使用 32 字节的二进制密钥,您可以指定 aes-256-cbc,这比 MCRYPT_RIJNDAEL_128.

更明显

这是使用 Mcrypt 的代码示例:

Unauthenticated AES-256-CBC encryption library written in Mcrypt with PKCS7 padding.

/**
 * 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');
    }
}

这里是使用 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
        );
    }
}

来源:If You're Typing the Word MCRYPT Into Your PHP Code, You're Doing It Wrong.

Pure-PHP Rijndael 的实现与 phpseclib 一起作为 composer 包提供,并在 PHP 7.3 上运行(由我测试)。

phpseclib 文档上有一个页面,generates sample code 在您输入基本变量(密码、模式、密钥大小、位大小)后。它为 Rijndael、ECB、256、256 输出以下内容:

带有 mycrypt 的代码

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

像这样与图书馆一起工作

$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);

* $termbase64_decoded

正如此处其他答案所详述的,我找到的最佳解决方案是使用 OpenSSL。它内置于 PHP 中,您不需要任何外部库。以下是简单的示例:

要加密:

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

解密:

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:https://www.shift8web.ca/2017/04/how-to-encrypt-and-execute-your-php-code-with-mcrypt/

我在 PHP 7.2.x 上使用它,它对我来说工作正常:

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

然后使用以下函数验证散列:

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

示例:

  //create hash from user string

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

并使用以下代码验证此哈希:

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

就这些了。