不推荐使用mcrypt,替代方法是什么?


103

根据此处发布的评论,不建议使用 mcrypt-extension 并将在PHP 7.2中删除。因此,我正在寻找一种加密密码的替代方法。

现在我正在使用类似

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

我需要您的意见以最好/最有效的方式对密码进行加密,当然,加密的密码应受PHP 7.xx支持,并且也应可解密,因为我的客户确实希望选择“恢复”密码而不生成新密码之一。


9
为什么需要加密/解密密码?为什么不仅仅使用它们进行哈希并用它们进行password_hash验证password_verify
不要惊慌

3
“加密的密码也应该是可解密的” -为什么?听起来不太安全。有什么特殊原因吗?
Funk 40 Niner

24
“因为我的客户确实希望可以选择'恢复'他们的密码而不生成新的密码。” -这不安全,应该给他们选择重设密码的选项。
Funk 40 Niner

4
不要加密密码,当攻击者获得数据库时,他还将获得加密密钥。使用随机盐在HMAC上迭代大约100毫秒的时间,然后将盐与哈希一起保存。使用诸如password_hash,PBKDF2,Bcrypt之类的功能以及类似功能。关键是使攻击者花费大量时间通过蛮力查找密码。
zaph

2
从php手册-> PHP 7.1.0开始,此功能已被弃用。强烈建议不要使用此功能。备选方案是钠-> php.net/manual/en/book.sodium.php
MarcoZen

Answers:


47

最佳做法是对密码进行哈希处理,以使密码不可解密。这对于可能已经获得对数据库或文件的访问权限的攻击者来说,事情变得有些困难。

如果必须加密数据并使其可解密,请访问https://paragonie.com/white-paper/2015-secure-php-data-encryption以获得安全加密/解密指南。总结该链接:

  • 使用Libsodium -PHP扩展
  • 如果您不能使用Libsodium,请使用defuse / php-encryption-直接使用PHP代码
  • 如果您无法使用Libsodium和化解/ PHP加密,使用OpenSSL的 -很多服务器都已经安装了这个。如果没有,则可以使用--with-openssl [= DIR]进行编译

1
应该首先尝试使用openssl,因为它很常见,而libsodium并不常见。除非所有问题都没有本机扩展,否则不应该使用原始php
JSON

尽管openssl很常见,但php 7似乎将libsodium用于其核心加密 securityintelligence.com/news/…–
shadi

1
请注意,有一个名为Sodium-compatgithub.com/paragonie/sodium_compat)的库可在PHP> = 5.2.4中工作
RaelB

30

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

根据以下Scott在php.net上的评论

如果您要在2015年编写用于加密/加密数据的代码,则应使用openssl_encrypt()openssl_decrypt()。基础库(libmcrypt)自2007年以来已被废弃,其性能远不如OpenSSL(后者利用AES-NI现代处理器并且缓存定时安全)。

此外,MCRYPT_RIJNDAEL_256不是AES-256,这是Rijndael分组密码的另一种变体。如果要AES-256使用mcrypt,则必须使用MCRYPT_RIJNDAEL_12832字节的密钥。OpenSSL使您更清楚地知道正在使用哪种模式(即aes-128-cbcvs aes-256-ctr)。

OpenSSL还使用具有CBC模式的PKCS7填充,而不是mcrypt的NULL字节填充。因此,与OpenSSL相比,mcrypt更可能使您的代码容易遭受填充oracle攻击。

最后,如果您不对密文进行身份验证(先加密然后加密MAC),那么您做错了。

进一步阅读:

代码示例

例子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算法128MCRYPT_RIJNDAEL_128等同于AES,但是Rijndael算法256MCRYPT_RIJNDAEL_256不是 AES-256作为256个指定的256位的块尺寸,而AES仅具有一个块大小:128比特。因此,MCRYPT_RIJNDAEL_256由于mcrypt开发人员的选择,基本上具有256位()块大小的Rijndael 被错误地命名。@zaph
  • 块大小为256的Rijndael可能不如块大小为128位的Rijndael,因为后者有更多的评论和用途。其次,互操作性受到阻碍,因为虽然AES通常可用,但Rijndael的块大小为256位则不可用。
  • Rijndael使用不同的块大小进行加密会生成不同的加密数据。

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


1
通常,由于mcrypt开发人员的选择,使用具有256位块大小的Rijndael是一个错误。块大小为256的其他Rijndael可能不如块大小为128位的Rijndael,因为后者的审查和使用情况要多得多。此外,互操作性受到阻碍,因为虽然AES通常可用,但Rijndael的块大小为256位的设备却没有。
zaph

你为什么$session_id = rtrim($decryptedSessionId, "\0");?最后是否可以openssl_decrypt返回一些不需要的字符?如果加密后的变量以0结尾(即,encrypt("abc0")
hlscalon,

@hiscalon "\0"不是"0"NULL字符,其ASCII码为0x00(十六进制0)。
kiamlaluno

11

Rijndael的纯PHP实现与phpseclib一起存在,可以作为作曲家软件包使用,并且可以在PHP 7.3上运行(由我测试)。

在phpseclib文档上有一个页面,在您输入基本变量(密码,模式,密钥大小,位大小)之后,该页面会生成示例代码。它为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


11

如此处其他答案所详述,我发现的最佳解决方案是使用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

一个简单的迁移脚本可以解决该问题。使用旧方法解密,然后使用新方法加密和存储。另一种方法是在用户表中添加一个标志,并在所有需要更改密码的用户帐户上强制重置密码来编写脚本。
塞西尔·梅雷尔(aka cecil merrel),19年

8

您可以使用phpseclib pollyfill软件包。您不能使用open ssl或libsodium通过rijndael 256进行加密/解密。另一个问题是,您不需要替换任何代码。


2
这非常有帮助,谢谢。不得不删除php-mcrypt扩展名,然后它像一个魅力一样起作用。
DannyB

mcrypt_compat通过运行进行安装,composer require phpseclib/mcrypt_compat但仍然可以PHP Fatal error: Uncaught Error: Call to undefined function mcrypt_get_key_size() in /app/kohana/classes/Kohana/Encrypt.php:124使用php 7.2.26和Kohanaframwork。使用composer安装后是否还有其他步骤要执行?
M-Dahab

得到它了。您必须添加require APPPATH . '/vendor/autoload.php';到的底部bootstrap.php
M-Dahab

3

您应该使用OpenSSL,mcrypt因为它是积极开发和维护的。它提供了更好的安全性,可维护性和可移植性。其次,它执行AES加密/解密的速度更快。默认情况下,它使用PKCS7填充,但是您可以指定OPENSSL_ZERO_PADDING是否需要它。要与32字节的二进制密钥一起使用,您可以指定aes-256-cbc哪个明显比MCRYPT_RIJNDAEL_128

这是使用Mcrypt的代码示例:

用Mcrypt编写的未经身份验证的AES-256-CBC加密库,带有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,那说明您做错了


2

我发布此消息的时间很晚,但我希望它能对其他在PHP 7.2x版本中面临相同问题的人有所帮助。

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

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

我能够翻译我的加密对象

  • 使用mcrypt获取php副本以解密旧数据。我去了http://php.net/get/php-7.1.12.tar.gz/from/a/mirror,对其进行了编译,然后添加了ext / mcrypt扩展名(configure; make; make install)。我想我也必须在php.ini中添加extenstion = mcrypt.so行。一系列脚本可构建数据的中间版本,而所有数据均未加密。

  • 为openssl构建公用密钥和专用密钥

    openssl genrsa -des3 -out pkey.pem 2048
    (set a password)
    openssl rsa -in pkey.pem -out pkey-pub.pem -outform PEM -pubout
  • 要加密(使用公共密钥),请使用openssl_seal。根据我的阅读,使用RSA密钥的openssl_encrypt限制为小于密钥长度的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>";

PS您不能加密空字符串(“”)

PPS这是用于密码数据库而不用于用户验证。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.