两个采用PHP双向加密 - 哪个更好?

时间:2014-10-09 08:32:59

标签: php security encryption cryptography

我需要对某些数据进行加密,并在稍后的时间点对其进行解密。数据与特定用户相关联。我收集了两种可能的解决方案......

1 :第一个来自官方文档(例如#1 @ http://php.net/manual/en/function.mcrypt-encrypt.php):

function encrypt($toEncrypt)
{
    global $key;
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
    return base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $toEncrypt, MCRYPT_MODE_CBC, $iv));
}

function decrypt($toDecrypt)
{
    global $key;
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
    $toDecrypt = base64_decode($toDecrypt);
    return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, substr($toDecrypt, $iv_size), MCRYPT_MODE_CBC, substr($toDecrypt, 0, $iv_size)));
}

使用以下代码生成密钥:

echo bin2hex(openssl_random_pseudo_bytes(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC)))

后来又称之为:

$key = pack('H*', [result of above]);

1.1 :我注意到加密结果总是以两个相等的符号结尾('==')。为什么? - 在encrypt()和decrypt()中使用bin2hex()和hex2bin()而不是base64_encode()/ base64_decode()分别不会产生这些结果。

1.2 :使用bin2hex()/ hex2bin()会对结果产生任何影响(长度除外)吗?

1.3 :在解密时,似乎有一些关于是否在返回结果上调用trim函数的讨论(这也适用于下面的解决方案)。为什么这是必要的?


2 :第二个解决方案来自此处,Stackoverflow(Simplest two-way encryption using PHP):

function encrypt($key, $toEncrypt)
{
    return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, md5($key), $toEncrypt, MCRYPT_MODE_CBC, md5(md5($key))));
}

function decrypt($key, $toDecrypt)
{
    return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5($key), base64_decode($toDecrypt), MCRYPT_MODE_CBC, md5(md5($key))), "\0");
}

我知道密钥处理的两种方法都是可以互换的,我故意在这方面使它们有所不同,以突出可能的解决方案,请随意混合搭配。

我个人觉得第一个提供更严格的安全性,因为密钥和初始化向量都是正确随机的。然而,第二种解决方案确实提供某种形式的不可预测性,因为密钥对于每条加密数据是唯一的(即使它在md5()的弱随机化下遭受损失)。 例如,密钥可以是用户的名字。

3 :那么哪一个更好?自从Stackoverflow的回答得到高达105票之后,我有点陷入黑暗中。其他想法,提示?

4 :奖金问题!:我在服务器安全方面并不是非常聪明,但显然获得对PHP文件的访问会暴露密钥,这直接导致加密没用,假设攻击者也可以访问数据库。有没有办法掩盖密钥?

感谢您阅读并度过美好的一天!

编辑:考虑到所有事情,这似乎是我最好的选择:

function encrypt($toEncrypt)
{
    global $key;
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
    $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_RAND);
    return base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $toEncrypt, MCRYPT_MODE_CBC, $iv));
}

function decrypt($toDecrypt)
{
    global $key;
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
    $toDecrypt = base64_decode($toDecrypt);
    return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, substr($toDecrypt, $iv_size), MCRYPT_MODE_CBC, substr($toDecrypt, 0, $iv_size)));
}

使用以下创建的密钥:

bin2hex(openssl_random_pseudo_bytes(32)));

4 个答案:

答案 0 :(得分:2)

Q1:选择此项!

披露:我(重新)编写了mcrypt_encrypt代码示例。所以我选择1。

我个人不建议使用MCRYPT_RIJNDAEL_256。您使用密钥大小为32字节(256位)的密钥为MCRYPT_RIJNDAEL_128算法使用AES-256,而不是选择块大小为256的Rijndael。我明确重写了示例以删除{{ 1}} - 以及其他错误 - 并在评论中提出您应该使用MCRYPT_RIJNDAEL_256的原因。

问题1.1:base64的填充字节

MCRYPT_RIJNDAEL_128是base 64编码的填充字符。 Base64将3个字节编码为4个字符。要获得4个字符的精确倍数,如果需要,可以使用这些填充字节。

Q1.2:使用bin2hex()/ hex2bin()会对结果产生任何影响(长度除外)吗?

不,因为hex和base64都是确定性且完全可逆的。

Q1.3:在rtrim

=同样如此。这是必需的,因为PHP的mcrypt使用非标准零填充,直到块大小(它在右边填充明文值为rtrim个值)。这适用于ASCII和& UTF-8字符串,00字节不在可打印字符范围内,但如果要加密二进制数据,可能需要进一步查看。在00的评论部分中有一些PKCS#7填充的示例。小注意:mcrypt_encrypt可能仅适用于某些语言(如PHP),其他实现可能会留下尾随rtrim个字符,因为00不被视为空格。

Q2:取消资格

另一个SO答案使用MD5进行密码派生,MD5使用密码进行IV计算。这完全取消了它作为一个好答案的资格。如果您有密码而不是密钥,请检查this Q/A

它也没有使用AES,选择选择00

问题3:投票

只要SO社区对似乎适用于某种语言/配置的答案进行投票,而不是对加密安全的答案进行投票,您就会发现绝对陷阱喜欢Q2中的答案。不幸的是,大多数来这里的人都不是密码学家;另一个答案是在crypto.stackexchange.com上绝对 smitten

请注意,就在昨天我不得不向某人解释为什么不能在iOS上使用CCCrypt解密MCRYPT_RIJNDAEL_256,因为只有AES可用。

问题4:混淆

如果您将AES密钥存储在软件或配置文件中,您可以对密钥进行模糊处理,但不能更多。

您需要使用公钥(例如RSA)和混合密码术,或者您需要将密钥存储在安全的地方,例如HSM或智能卡。密钥管理是加密的一个复杂部分,可能是最复杂的部分。

答案 1 :(得分:2)

两个代码示例之间的主要区别在于,第一个代码为每条消息生成一个随机initialization vector(IV),而第二个代码始终使用从密钥派生的固定IV。

如果您从未使用相同的密钥加密多条消息,则两种方法都可以。但是,encrypting multiple messages with the same key and IV is dangerous,所以您永远不应该使用第二个代码示例来加密具有相同密钥的多个邮件。


另一个不同之处在于第一个代码示例将密钥直接传递给块密码(Rijndael),而第二个代码示例首先将其传递给md5(),显然是在尝试将其用作{{3} }}

如果密钥已经是一个随机的比特串(长度合适),就像你的样本密钥生成代码那样,那么就不需要通过md5()来运行它。相反,如果它类似于用户提供的密码,那么可能在散列它时有一些优势 - 但在这种情况下,你真的应该使用正确的密钥派生函数,如{ {3}}相反,例如像这样:

$cipher = MCRYPT_RIJNDAEL_128;  // = AES-256
$mode   = MCRYPT_MODE_CBC;
$keylen = mcrypt_get_key_size( $cipher, $mode );

$salt   = mcrypt_create_iv( $keylen, MCRYPT_DEV_URANDOM );
$iterations = 10000;  // higher = slower; make this as high as you can tolerate

$key = hash_pbkdf2( 'sha256', $password, $salt, $iterations, $keylen, true );

请注意,从解密密码重建密钥需要正确的$salt$iterations值,因此请记住将它们存储在某处,例如通过将它们添加到密文。盐的长度并不重要,只要它不是很短;使其等于密钥长度是一个安全的选择。

(顺便提一下,这也是散列密码以验证其正确性的一种非常好的方法。显然,您不应该使用相同的$key值进行加密和密码验证,但您可以安全地与密文一起存储,例如hash( 'sha256', $key, true ),以便您验证密码/密钥是否正确。)


我在两个代码段中看到的一些其他问题:

  • 两个代码段都使用MCRYPT_RIJNDAEL_256,显然,不是 key derivation function,而是使用非标准的Rijndael-256/256变体位块大小(和密钥大小)。它的可能安全,但Rijndael的256位块大小的变种比128位块大小(标准化为AES)接受的密码分析审查要少得多,因此您使用它们会带来稍微更高的风险。

    因此,如果您想要安全地玩,需要使用标准AES与其他软件互操作,或者只需要能够告诉您的老板,是的,您使用的是标准的NIST认可的密码,你应该选择MCRYPT_RIJNDAEL_128(显然,mcrypt称之为AES-256)。

  • 在密钥生成代码中,pack( 'H*', bin2hex( ... ) )是无操作:bin2hex()将密钥从二进制转换为十六进制,然后pack( 'H*', ... )执行相反的操作。只是摆脱这两个功能。

    此外,您还要生成密钥,而不是IV,因此您应该使用mcrypt_get_key_size(),而不是mcrypt_get_iv_size()。实际上,对于MCRYPT_RIJNDAEL_256,没有区别(因为IV大小和密钥大小都是32字节= 256位),但对于MCRYPT_RIJNDAEL_128(和许多其他密码),

  • 正如owlstead所说,mcrypt实施的CBC模式显然使用了非标准的零填充方案。第二个代码示例使用rtrim( $msg, "\0" )正确删除了填充;第一个只调用rtrim( $msg ),它还将修剪消息末尾的任何空格。

    另外,显然,如果您的数据最终可以合法地包含零字节,那么这种零填充方案将无法正常工作。您可以转而使用其他密码模式,例如MCRYPT_MODE_CFBMCRYPT_MODE_OFB,它们不需要任何填充。 (在这两个中,我通常会推荐CFB,因为PBKDF2。它对CFB或CBC也不好,但是他们的失败模式更不是灾难性的。)

答案 2 :(得分:2)

首先,我为这个答案的长度道歉。

我刚刚遇到过这个帖子,我希望这个类可以帮助任何阅读此帖子的人寻找他们可以使用的答案和源代码。

说明

该类将首先使用提供的加密密钥,并使用SHA-512算法在1000次迭代中通过PBKDF2实现运行它。

加密数据时,此类将压缩数据并在加密前计算压缩数据的md5摘要。它还将计算压缩后数据的长度。然后使用压缩数据对这些计算值进行加密,并将IV预先添加到加密输出中。

在每次加密操作之前使用dev / urandom生成新的IV。如果脚本在Windows机器上运行且PHP版本小于5.3,则该类将使用MCRYPT_RAND生成IV。

根据参数$ raw_output是true还是false,加密方法将默认返回小写的hexit或加密数据的原始二进制。

解密将反转加密过程并检查计算的md5摘要是否等于使用数据加密的存储的md5摘要。如果散列不相同,则解密方法将返回false。它还将使用存储的压缩数据长度来确保在解压缩之前删除所有填充。

本课程在CBC模式下使用Rijndael 128。

此类将跨平台工作,并已在PHP 5.2,5.3,5.4,5.5和5.6

上进行了测试

文件:AesEncryption.php

<?php

/**
 * This file contains the class AesEncryption
 *
 * AesEncryption can safely encrypt and decrypt plain or binary data and
 * uses verification to ensure decryption was successful.
 *
 * PHP version 5
 *
 * LICENSE: This source file is subject to version 2.0 of the Apache license
 * that is available through the world-wide-web at the following URI:
 * https://www.apache.org/licenses/LICENSE-2.0.html.
 *
 * @author     Michael Bush <michael(.)bush(@)hotmail(.)co(.)uk>
 * @license    https://www.apache.org/licenses/LICENSE-2.0.html Apache 2.0
 * @copyright  2015 Michael Bush
 * @version    1.0.0
 */

/**
 * @version    1.0.0
 */
final class AesEncryption
{
    /**
     * @var string
     */
    private $key;

    /**
     * @var string
     */
    private $iv;

    /**
     * @var resource
     */
    private $mcrypt;

    /**
     * Construct the call optionally providing an encryption key
     *
     * @param string $key
     * @return Encryption
     * @throws RuntimeException if the PHP installation is missing critical requirements
     */
    public function __construct($key = null) {
        if (!extension_loaded ('mcrypt')) {
            throw new RuntimeException('MCrypt library is not availble');
        }
        if (!extension_loaded ('hash')) {
            throw new RuntimeException('Hash library is not availble');
        }
        if (!in_array('rijndael-128', mcrypt_list_algorithms(), true)) {
            throw new RuntimeException('MCrypt library does not contain an implementation of rijndael-128');
        }
        if (!in_array('cbc', mcrypt_list_modes(), true)) {
            throw new RuntimeException('MCrypt library does not support CBC encryption mode');
        }
        $this->mcrypt = mcrypt_module_open('rijndael-128', '', 'cbc', '');
        if(isset($key)) {
            $this->SetKey($key);
        }
    }

    /**
     * @return void
     */
    public function __destruct() {
        if (extension_loaded ('mcrypt')) {
            if (isset($this->mcrypt)) {
                mcrypt_module_close($this->mcrypt);
            }
        }
    }

    /**
     * Set the key to be used for encryption and decryption operations.
     *
     * @param string $key
     * @return void
     */
    public function SetKey($key){
        $this->key = $this->pbkdf2('sha512', $key, hash('sha512', $key, true), 1000, mcrypt_enc_get_key_size($this->mcrypt), true);
    }

    /**
     * Encrypts data
     *
     * @param string $data
     * @param bool $raw_output if false this method will return lowercase hexit, if true this method will return raw binary
     * @return string
     */
    public function Encrypt($data, $raw_output = false) {
        $data = gzcompress($data, 9);
        $hash = md5($data, true);
        $datalen = strlen($data);
        $datalen = pack('N', $datalen);
        $data = $datalen . $hash . $data;
        if (version_compare(PHP_VERSION, '5.3.0', '<=')) {
            if (strtolower (substr (PHP_OS, 0, 3)) == 'win') {
                $this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_RAND);
            } else {
                $this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_DEV_URANDOM);
            }
        } else {
            $this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_DEV_URANDOM);
        }
        $this->initialize();
        $data = mcrypt_generic($this->mcrypt, $data);
        $this->deinitialize();
        $data = $this->iv . $data;
        $this->iv = null;
        if ($raw_output) {
            return $data;
        }
        $data = unpack('H*',$data);
        $data = end($data);
        return $data;
    }

    /**
     * Decrypts data
     *
     * @param string $data
     * @return string This method will return false if an error occurs
     */
    public function Decrypt($data) {
        if (ctype_xdigit($data)) {
            $data = pack ('H*',$data);
        }
        $this->iv = substr ($data, 0, mcrypt_enc_get_iv_size($this->mcrypt));
        $data = substr ($data, mcrypt_enc_get_iv_size($this->mcrypt));
        $this->initialize();
        $data = mdecrypt_generic($this->mcrypt, $data);
        $this->deinitialize();
        $datalen = substr($data, 0, 4);
        $len = unpack('N', $datalen);
        $len = end($len);
        $hash = substr($data, 4, 16);
        $data = substr($data, 20, $len);
        $datahash = md5($data, true);
        if ($this->compare($hash,$datahash)) {
            $data = @gzuncompress($data);
            return $data;
        }
        return false;
    }

    /**
     * Initializes the mcrypt module
     *
     * @return void
     */
    private function initialize() {
        mcrypt_generic_init($this->mcrypt, $this->key, $this->iv);
    }

    /**
     * Deinitializes the mcrypt module and releases memory.
     *
     * @return void
     */
    private function deinitialize() {
        mcrypt_generic_deinit($this->mcrypt);
    }

    /**
     * Implementation of a timing-attack safe string comparison algorithm, it will use hash_equals if it is available
     *
     * @param string $safe
     * @param string $supplied
     * @return bool
     */
    private function compare($safe, $supplied) {
        if (function_exists('hash_equals')) {
            return hash_equals($safe, $supplied);
        }
        $safe .= chr(0x00);
        $supplied .= chr(0x00);
        $safeLen = strlen($safe);
        $suppliedLen = strlen($supplied);
        $result = $safeLen - $suppliedLen;
        for ($i = 0; $i < $suppliedLen; $i++) {
            $result |= (ord($safe[$i % $safeLen]) ^ ord($supplied[$i]));
        }
        return $result === 0;
    }

    /**
     * Implementation of the keyed-hash message authentication code algorithm, it will use hash_hmac if it is available
     *
     * @param string $algo
     * @param string $data
     * @param string $key
     * @param bool $raw_output
     * @return string
     *
     * @bug method returning wrong result for joaat algorithm
     * @id 101275
     * @affects PHP installations without the hash_hmac function but they do have the joaat algorithm
     * @action wont fix
     */
    private function hmac($algo, $data, $key, $raw_output = false) {
        $algo = strtolower ($algo);
        if (function_exists('hash_hmac')) {
            return hash_hmac($algo, $data, $key, $raw_output);
        }
        switch ( $algo ) {
            case 'joaat':
            case 'crc32':
            case 'crc32b':
            case 'adler32':
            case 'fnv132':
            case 'fnv164':
            case 'fnv1a32':
            case 'fnv1a64':
                $block_size = 4;
                break;
            case 'md2':
                $block_size = 16;
                break;
            case 'gost':
            case 'gost-crypto':
            case 'snefru':
            case 'snefru256':
                $block_size = 32;
                break;
            case 'sha384':
            case 'sha512':
            case 'haval256,5':
            case 'haval224,5':
            case 'haval192,5':
            case 'haval160,5':
            case 'haval128,5':
            case 'haval256,4':
            case 'haval224,4':
            case 'haval192,4':
            case 'haval160,4':
            case 'haval128,4':
            case 'haval256,3':
            case 'haval224,3':
            case 'haval192,3':
            case 'haval160,3':
            case 'haval128,3':
                $block_size = 128;
                break;
            default:
                $block_size = 64;
                break;
        }
        if (strlen($key) > $block_size) {
            $key=hash($algo, $key, true);
        } else {
            $key=str_pad($key, $block_size, chr(0x00));
        }
        $ipad=str_repeat(chr(0x36), $block_size);
        $opad=str_repeat(chr(0x5c), $block_size);
        $hmac = hash($algo, ($key^$opad) . hash($algo, ($key^$ipad) . $data, true), $raw_output);
        return $hmac;
    }

    /**
     * Implementation of the pbkdf2 algorithm, it will use hash_pbkdf2 if it is available
     *
     * @param string $algorithm
     * @param string $password
     * @param string $salt
     * @param int $count
     * @param int $key_length
     * @param bool $raw_output
     * @return string
     * @throws RuntimeException if the algorithm is not found
     */
    private function pbkdf2($algorithm, $password, $salt, $count = 1000, $key_length = 0, $raw_output = false) {
        $algorithm = strtolower ($algorithm);
        if (!in_array($algorithm, hash_algos(), true)) {
            throw new RuntimeException('Hash library does not contain an implementation of ' . $algorithm);
        }
        if (function_exists('hash_pbkdf2')) {
            return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
        }
        $hash_length = strlen(hash($algorithm, '', true));
        if ($count <= 0) {
            $count = 1000;
        }
        if($key_length <= 0) {
            $key_length = $hash_length * 2;
        }
        $block_count = ceil($key_length / $hash_length);
        $output = '';
        for($i = 1; $i <= $block_count; $i++) {
            $last = $salt . pack('N', $i);
            $last = $xorsum = $this->hmac($algorithm, $last, $password, true);
            for ($j = 1; $j < $count; $j++) {
                $xorsum ^= ($last = $this->hmac($algorithm, $last, $password, true));
            }
            $output .= $xorsum;
        }
        if ($raw_output) {
            return substr($output, 0, $key_length);
        }
        $output = unpack('H*',$output);
        $output = end ($output);
        return substr($output, 0, $key_length);
    }
}

用法示例:

<?php

include 'AesEncryption.php';

$key = 'my secret key';
$string = 'hello world';

try
{
    $aes = new AesEncryption($key); // exception can be thrown here if the class is not supported

    $data = $aes->Encrypt($string, true); // expecting return of a raw byte string
    $decr = $aes->Decrypt($data); // expecting the return of "hello world"
    var_dump ($decr);

    // encrypt something else with a different key
    $aes->SetKey('my other secret key'); // exception can be thrown here if the class is not supported

    $data2 = $aes->Encrypt($string); // return the return of a lowercase hexit string
    $decr = $aes->Decrypt($data2); // expecting the return of "hello world"
    var_dump ($decr);

    // proof that the key was changed
    $decr = $aes->Decrypt($data); // expecting return of Boolean False
    var_dump ($decr);

    // reset the key back
    $aes->SetKey($key); // exception can be thrown here if the class is not supported
    $decr = $aes->Decrypt($data); // expecting hello world
    var_dump ($decr);
}

catch (Exception $e)
{
    print 'Error running AesEncryption class; reason: ' . $e->getMessage ();
}

答案 3 :(得分:0)

1.1这只是填充。大多数输入都发生在base64上,但不是全部。

1.2没有区别。与base64保持一致,这是加密的标准。

1.3我不明白为什么有必要这样做。人们有时会在错误的地方解决问题。他们修改输出而不是修复输入。这个讨论在哪里?

  1. 绝对不要使用它。您将某些legnth的密钥更改为128位MD5。这不安全。

  2. 如果解密位于不同的计算机或不同的用户上,则使用非对称加密。