mcrypt已经被弃用,有什么替代品?

126

根据此处发布的评论,mcrypt扩展已经被弃用,并将在PHP 7.2中被删除。因此,我正在寻找替代方法来加密密码。

目前我正在使用类似于以下内容:

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

我需要你的意见,关于最佳/最强的密码加密方式,加密后的密码当然要支持PHP 7.xx,并且应该是可解密的,因为我的客户希望有一个"恢复"密码的选项,而不是生成新的密码。


11
为什么需要加密/解密密码呢?为什么不只是使用 password_hash 哈希它们,并用 password_verify 验证它们? - Don't Panic
5
“加密密码也应该是可解密的” - 为什么?听起来不太安全。有特殊原因吗? 这句话的意思是,即使密码被加密,也应该有一种方法可以将其还原回原始文本。这是因为在某些情况下(例如,当用户忘记他们的密码时),需要能够找回密码。尽管这可能会增加潜在的安全风险,但它也是确保用户拥有访问帐户的权利的必要步骤之一。 - Funk Forty Niner
35
“因为我的客户希望能够有‘找回’密码的选项,而不是生成一个新密码。”- 这并不安全,他们应该被给予重置密码的选项。 - Funk Forty Niner
9
不要加密密码,当攻击者获取数据库时,也会得到加密密钥。使用一个随机盐值对HMAC进行迭代,持续约100毫秒,并将盐值与散列保存在一起。使用password_hash、PBKDF2、Bcrypt等类似函数。目的是让攻击者花费很长时间通过暴力破解找到密码。 - zaph
2
从php手册->此函数已自PHP 7.1.0起被弃用。依赖此函数是极不鼓励的。替代方案是sodium->http://php.net/manual/en/book.sodium.php - MarcoZen
显示剩余6条评论
10个回答

54
最佳实践是对密码进行哈希处理,这样它们就无法被解密。这使得攻击者在获得您的数据库或文件访问权限时更加困难。
如果必须加密数据并且需要解密,可以查看https://paragonie.com/white-paper/2015-secure-php-data-encryption中提供的安全加密/解密指南. 总结如下:
  • 使用Libsodium - PHP扩展
  • 如果不能使用Libsodium,则使用defuse/php-encryption - 纯PHP代码
  • 如果不能使用Libsodium或defuse/php-encryption,则使用OpenSSL - 许多服务器已经安装了它。否则,可以使用--with-openssl[=DIR]编译它。

2
应该首先尝试使用OpenSSL,因为它非常常见,而Libsodium则不是。除非所有本地扩展都无法使用,否则不应使用原始的PHP。 - JSON
尽管openssl非常常见,但似乎PHP 7将使用libsodium作为其核心加密库。https://securityintelligence.com/news/php-will-incorporate-libsodium-for-crypto/ - Shadi
1
请注意,有一个名为Sodium-compat(https://github.com/paragonie/sodium_compat)的库可在PHP >= 5.2.4中使用。 - RaelB

36

根据@rqLizard的建议,您可以使用openssl_encrypt/openssl_decrypt PHP函数,这提供了一个更好的替代方案来实现AES(高级加密标准),也称为Rijndael加密。

根据php.net上的Scott评论

如果你在2015年编写加密/解密数据的代码,应该使用openssl_encrypt()openssl_decrypt()。底层库(libmcrypt)自2007年以来就已经被弃用,并且表现远不如OpenSSL(后者利用现代处理器上的AES-NI并具有缓存时间安全性)。
此外,MCRYPT_RIJNDAEL_256不是AES-256,它是Rijndael块密码的另一种变体。如果您想在mcrypt中使用AES-256,则必须使用32字节密钥的MCRYPT_RIJNDAEL_128。 OpenSSL更明显地显示您正在使用哪个模式(即aes-128-cbc vs aes-256-ctr)。
OpenSSL还使用CBC模式的PKCS7填充,而不是mcrypt的NULL字节填充。因此,使用mcrypt更容易使您的代码容易受到填充Oracle攻击。
最后,如果您没有对密文进行身份验证(然后加密),那么您就做错了。
进一步阅读:

代码示例

示例#1

PHP 7.1+中使用GCM模式的AES身份验证加密示例

<?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

PHP 5.6+的AES认证加密示例

<?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, "\0");
    // 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, '\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();
  }

}

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

以下是@zaph的评论中提到的其他注意事项:

  • Rijndael 128 (MCRYPT_RIJNDAEL_128) 等同于 AES,但是 Rijndael 256 (MCRYPT_RIJNDAEL_256) 不是 AES-256,因为256指定了块大小为256位,而 AES 只有一个块大小:128位。所以基本上具有256位块大小的 Rijndael (MCRYPT_RIJNDAEL_256) 由于 mcrypt 开发者的选择错误地被命名。 @zaph
  • 块大小为256的 Rijndael 可能比128位块大小的更不安全,因为后者经过了更多的审查和使用。其次,互操作性受到阻碍,因为虽然 AES 通常可用,但带有256位块大小的 Rijndael 不可用。
  • 使用不同块大小的 Rijndael 进行加密会产生不同的加密数据。

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


1
一般来说,使用块大小为256位的Rijndael算法是一个错误,因为mcrypt开发人员的选择。此外,块大小为256的Rijndael可能比128位的不安全,因为后者经过了更多的审查和使用。另外,互操作性受到阻碍,因为虽然AES通常可用,但是块大小为256位的Rijndael却不可用。 - zaph
为什么要 $session_id = rtrim($decryptedSessionId, "\0");openssl_decrypt是否可能在结尾返回一些不需要的字符?如果加密变量以0结尾(即encrypt("abc0")),会发生什么? - hlscalon
@hiscalon "\0" 不是 "0",而是空字符(NULL character),其 ASCII 码为 0x00(十六进制 0)。 - apaderno

24

如其他答案所述,我发现最好的解决方案是使用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);
}

参考链接:https://www.shift8web.ca/2017/04/how-to-encrypt-and-execute-your-php-code-with-mcrypt/


朋友,祝你好运!只有一件事:如果密码例如是用旧代码加密的,新的解密代码将无法验证它。必须使用这个新代码重新保存和加密。 - Lumis
一个简单的迁移脚本可以解决这个问题。使用旧的解密方式,然后使用新的加密方式进行存储。另一种选择是在用户表中添加一个标志,并在需要更改密码的所有用户帐户上编写强制密码重置的脚本。 - CecilMerrell aka bringrainfire

13

phpseclib是一个基于纯PHP实现的Rijndael算法库,可以作为Composer软件包使用,并且已在我测试的PHP 7.3上工作。

在phpseclib文档中有一个页面,输入基本变量(密码、模式、密钥大小、位数),然后生成示例代码。对于Rijndael,ECB,256,256,它将输出以下内容:

使用mcrypt的代码

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

* $term 经过 base64 解码


这个答案加上下面某人提到的phpseclib mcrypt_compat polyfill包就是解决方案。 - Max

10
你可以使用phpseclib polyfill包。你不能使用open ssl或libsodium来进行rijndael 256的加密/解密。 另一个问题是,你不需要替换任何代码。

2
这非常有帮助,谢谢。必须删除php-mcrypt扩展,然后它就可以完美运行了。 - DannyB
我通过运行composer require phpseclib/mcrypt_compat安装了mcrypt_compat,但我仍然收到以下错误:PHP致命错误:在/app/kohana/classes/Kohana/Encrypt.php:124中未捕获的错误:调用未定义的函数mcrypt_get_key_size() 我正在使用php 7.2.26和Kohana框架。 在使用composer安装后,是否还需要执行其他步骤? - M-Dahab
1
你需要在 bootstrap.php 的底部添加 require APPPATH . '/vendor/autoload.php'; - M-Dahab
1
这是最有帮助的答案,解除了我的阻塞。mcrypt compat 2.0 + phpseclib 3.0 对于 rijndael 256 mcrypt 迁移非常有效,因为由于几十年的加密数据,无法切换到 AES。 - Max

3
您应该使用 OpenSSL 而不是 mcrypt,因为它正在积极开发和维护。它提供更好的安全性、可维护性和可移植性。其次,它执行 AES 加密/解密速度更快。默认情况下,它使用 PKCS7 填充,但如果需要,您可以指定 OPENSSL_ZERO_PADDING。要使用 32 字节二进制密钥,您可以指定 aes-256-cbc,这比 MCRYPT_RIJNDAEL_128 更明显。
以下是使用 Mcrypt 的代码示例:
未经身份验证的 AES-256-CBC 加密库,使用 Mcrypt 编写,并带有 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');
    }
}

下面是使用 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
        );
    }
}

来源:如果你在PHP代码中输入单词MCRYPT,那么你做错了


3

我正在使用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  
    }

就这些。


1
正如指出的那样,您不应该以可解密的格式存储用户的密码。 可逆加密为黑客提供了轻松查找用户密码的方法,这可能会使用户在其他站点上使用相同密码时面临风险。
PHP提供了一对强大的函数,用于随机盐和单向哈希加密 - password_hash()password_verify()。由于哈希自动随机盐,因此黑客无法利用预编译的密码哈希表来破解密码。设置PASSWORD_DEFAULT选项,未来的PHP版本将自动使用更强大的算法生成密码哈希,而无需更新代码。

0

PHP 7中的openssl加密是否存在“心脏出血”漏洞? - TheCrazyProfessor
15
OP为什么应该使用openssl_encrypt?请提供一些详细信息和背景说明。 - Martin

0

我成功地翻译了我的加密对象

  • 获取带有mcrypt的php副本以解密旧数据。我去了http://php.net/get/php-7.1.12.tar.gz/from/a/mirror,编译了它,然后添加了ext/mcrypt扩展(configure;make;make install)。我认为我还必须在php.ini中添加extension=mcrypt.so行。一系列脚本用于构建所有数据未加密的中间版本。

  • 为openssl构建公钥和私钥

    openssl genrsa -des3 -out pkey.pem 2048
    (设置密码)
    openssl rsa -in pkey.pem -out pkey-pub.pem -outform PEM -pubout
    
  • 要加密(使用公钥),请使用openssl_seal。从我所读到的内容来看,使用RSA密钥的openssl_encrypt受限于比密钥长度少11个字节(请参见http://php.net/manual/en/function.openssl-public-encrypt.php 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]);
    

你可以将原始二进制数据存储。

解密(使用私钥)
$passphrase="这里是密码";
$privKey = openssl_get_privatekey(file_get_contents('./pkey.pem'), $passphrase);
// 我将数据库列中的数据使用 base64_decode()
openssl_open($encryptedPassword, $plain, $key, $privKey);
echo "<h3>密码=$plain</h3>";

附言:你不能加密空字符串("")

再附言:这是用于密码数据库而不是用户验证。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接