【问题标题】:Two takes on PHP two way encryption - which one is preferable?两种采用 PHP 双向加密——哪一种更可取?
【发布时间】:2015-03-28 14:03:23
【问题描述】:

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

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:似乎有一些讨论是否在解密时对返回结果调用修剪函数(这也适用于下面的解决方案)。为什么有必要这样做?


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

【问题讨论】:

标签: php security encryption cryptography


【解决方案1】:

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

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


另一个区别是第一个代码示例将密钥直接传递给分组密码 (Rijndael),而第二个代码示例首先通过 md5() 运行它,显然是在尝试将其用作 key derivation function 的弱尝试。

如果密钥已经是一个随机位串(长度合适),就像您的示例密钥生成代码将生成的那样,则无需通过md5() 运行它。相反,如果它类似于用户提供的密码,可能对它进行散列处理会有一些优势——但在这种情况下,你真的应该使用像 PBKDF2 这样的适当的密钥派生函数,例如像这样:

$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 ) 旁边的密文让你验证密码/密钥是否正确。)


我看到的两个代码 sn-ps 的其他一些问题:

  • 两个 sn-ps 都使用 MCRYPT_RIJNDAEL_256,显然 不是 AES-256,而是非标准的 Rijndael-256/256 变体,具有 256 位块大小(和密钥大小)。它可能是安全的,但是 Rijndael 的 256 位块大小的变体比 128 位块大小的变体(标准化为 AES)受到的密码分析审查要少得多,所以你'使用它们会稍微冒更高的风险。

    因此,如果您想安全起见,需要使用标准 AES 与其他软件进行互操作,或者只需要能够告诉您的老板,是的,您使用的是标准 NIST 批准的密码,那么您应该使用 MCRYPT_RIJNDAEL_128(显然,这就是 mcrypt 所称的 AES-256)。

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

    另外,您生成的是密钥,而不是 IV,因此您应该使用 mcrypt_get_<b>key</b>_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,因为 accidental IV reuse is very bad for OFB。这对 CFB 或 CBC 也不好,但它们的故障模式要少得多。)

【讨论】:

  • 嗯,是的,我想我应该再次邮寄给 PHP 维护人员以修复 rtrim 功能。我不是一个经验丰富的 PHP 开发人员,但我在此建议使用 rtrim( $msg, "\0" ) :)(当然是 +1)
【解决方案2】:

Q1:选择这个!

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

我个人不建议使用MCRYPT_RIJNDAEL_256。您通过使用密钥大小为 32 字节(256 位)的密钥用于 MCRYPT_RIJNDAEL_128 算法,而不是通过选择块大小为 256 的 Rijndael 来使用 AES-256。我明确重写了示例以删除 MCRYPT_RIJNDAEL_256 –除其他错误外 - 并输入 cmets 为什么您应该使用 MCRYPT_RIJNDAEL_128

Q 1.1:base64 的填充字节

= 是 base 64 编码的填充字符。 Base64 将 3 个字节编码为 4 个字符。如果需要,他们使用这些填充字节来获得正好是 4 的倍数的字符数。

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

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

Q1.3:在 rtrim 上

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

Q2:取消资格

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

它也不使用AES,选择MCRYPT_RIJNDAEL_256

Q3:关于投票

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

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

Q4:混淆

您可以对密钥进行混淆处理,但如果您将 AES 密钥存储在软件或配置文件中,则不能进行其他操作。

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

【讨论】:

  • 我个人认为 6/7 题一次就够了。
【解决方案3】:

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

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

说明:

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

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

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

根据参数$raw_output是真还是假,加密方法默认返回小写十六进制或加密数据的原始二进制。

解密将反转加密过程并检查计算出的 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 ();
}

【讨论】:

    【解决方案4】:

    1.1 这只是填充。大多数对 base64 的输入都会发生这种情况,但不是全部。

    1.2 没有区别。保持 base64,它是加密的标准。

    1.3 我看不出有必要这样做的理由。人们有时会在错误的地方解决问题。他们不是固定输入,而是修改输出。这是在哪里讨论?

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

    2. 如果解密是在不同的机器或不同的用户上,则使用非对称加密。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-06
      • 2010-10-18
      • 1970-01-01
      • 2014-02-25
      相关资源
      最近更新 更多