双向加密:我需要存储可以检索的密码


174

我正在创建一个应用程序,该应用程序将存储用户可以检索和查看的密码。密码用于硬件设备,因此无法检查哈希。

我需要知道的是:

  1. 如何在PHP中加密和解密密码?

  2. 什么是最安全的密码加密算法?

  3. 我在哪里存储私钥?

  4. 除了存储私钥之外,要求用户在需要解密密码的任何时间都输入私钥是一个好主意吗?(可以信任此应用程序的用户)

  5. 密码可以通过什么方式被盗和解密?我需要注意什么?


1
注意:Libsodium现在已编译到PHP内核中,用于> = 7.2。这是现在的“执行”解决方案,因为它充满了与mcrypt不同的现代方法,mcrypt被认为已弃用并已被删除。
参展商

Answers:


212

就个人而言,我会mcrypt像其他人一样使用。但是还有更多需要注意的地方...

  1. 如何在PHP中加密和解密密码?

    请参阅下面的强大课程,该课程可以为您解决所有问题:

  2. 什么是最安全的密码加密算法?

    最安全?任何一位。如果要加密,最安全的方法是防止信息泄露漏洞(XSS,远程包含等)。如果失败,攻击者最终可以破解加密(没有密钥,没有任何加密是100%不可逆的-正如@NullUserException指出的那样,这并不完全正确。有一些无法破解的加密方案,例如OneTimePad) 。

  3. 我在哪里存储私钥?

    我要做的是使用3个键。一个是用户提供的,一个是特定于应用程序的,另一个是特定于用户的(例如盐)。特定于应用程序的密钥可以存储在任何地方(在Web根目录之外的配置文件中,环境变量中等等)。用户专用密码将存储在数据库中加密密码旁边的一列中。用户提供的将不会被存储。然后,您将执行以下操作:

    $key = $userKey . $serverKey . $userSuppliedKey;

    这样做的好处是,其中任何两个密钥都可以被破坏,而不会破坏数据。如果发生SQL注入攻击,则可以获取$userKey,但不能获取其他2。如果存在本地服务器利用,则可以获取$userKey$serverKey,但不能获取$userSuppliedKey。如果他们用扳手殴打用户,他们可以拿到$userSuppliedKey,但不能拿到其他2(但是,如果用扳手殴打用户,无论如何你都来不及了)。

  4. 除了存储私钥之外,要求用户在需要解密密码的任何时间都输入私钥是一个好主意吗?(可以信任此应用程序的用户)

    绝对。实际上,那是我要做的唯一方法。否则,您需要以持久存储格式(共享内存(例如APC或内存缓存)或会话文件)存储未加密的版本。这会使您面临更多折衷。除本地变量外,切勿将未加密版本的密码存储在任何其他内容中。

  5. 密码可以通过什么方式被盗和解密?我需要注意什么?

    系统的任何形式的破坏都会让他们查看加密的数据。如果他们可以注入代码或访问您的文件系统,则他们可以查看已解密的数据(因为他们可以编辑用于解密数据的文件)。任何形式的重播或MITM攻击也将使他们能够完全访问所涉及的密钥。嗅探原始HTTP流量也将为他们提供密钥。

    对所有流量使用SSL。并确保服务器上没有任何漏洞(CSRF,XSS,SQL注入,权限提升,远程执行代码等)。

编辑:这是一个强加密方法的PHP类实现:

/**
 * A class to handle secure encryption and decryption of arbitrary data
 *
 * Note that this is not just straight encryption.  It also has a few other
 *  features in it to make the encrypted data far more secure.  Note that any
 *  other implementations used to decrypt data will have to do the same exact
 *  operations.  
 *
 * Security Benefits:
 *
 * - Uses Key stretching
 * - Hides the Initialization Vector
 * - Does HMAC verification of source data
 *
 */
class Encryption {

    /**
     * @var string $cipher The mcrypt cipher to use for this instance
     */
    protected $cipher = '';

    /**
     * @var int $mode The mcrypt cipher mode to use
     */
    protected $mode = '';

    /**
     * @var int $rounds The number of rounds to feed into PBKDF2 for key generation
     */
    protected $rounds = 100;

    /**
     * Constructor!
     *
     * @param string $cipher The MCRYPT_* cypher to use for this instance
     * @param int    $mode   The MCRYPT_MODE_* mode to use for this instance
     * @param int    $rounds The number of PBKDF2 rounds to do on the key
     */
    public function __construct($cipher, $mode, $rounds = 100) {
        $this->cipher = $cipher;
        $this->mode = $mode;
        $this->rounds = (int) $rounds;
    }

    /**
     * Decrypt the data with the provided key
     *
     * @param string $data The encrypted datat to decrypt
     * @param string $key  The key to use for decryption
     * 
     * @returns string|false The returned string if decryption is successful
     *                           false if it is not
     */
    public function decrypt($data, $key) {
        $salt = substr($data, 0, 128);
        $enc = substr($data, 128, -64);
        $mac = substr($data, -64);

        list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);

        if (!hash_equals(hash_hmac('sha512', $enc, $macKey, true), $mac)) {
             return false;
        }

        $dec = mcrypt_decrypt($this->cipher, $cipherKey, $enc, $this->mode, $iv);

        $data = $this->unpad($dec);

        return $data;
    }

    /**
     * Encrypt the supplied data using the supplied key
     * 
     * @param string $data The data to encrypt
     * @param string $key  The key to encrypt with
     *
     * @returns string The encrypted data
     */
    public function encrypt($data, $key) {
        $salt = mcrypt_create_iv(128, MCRYPT_DEV_URANDOM);
        list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);

        $data = $this->pad($data);

        $enc = mcrypt_encrypt($this->cipher, $cipherKey, $data, $this->mode, $iv);

        $mac = hash_hmac('sha512', $enc, $macKey, true);
        return $salt . $enc . $mac;
    }

    /**
     * Generates a set of keys given a random salt and a master key
     *
     * @param string $salt A random string to change the keys each encryption
     * @param string $key  The supplied key to encrypt with
     *
     * @returns array An array of keys (a cipher key, a mac key, and a IV)
     */
    protected function getKeys($salt, $key) {
        $ivSize = mcrypt_get_iv_size($this->cipher, $this->mode);
        $keySize = mcrypt_get_key_size($this->cipher, $this->mode);
        $length = 2 * $keySize + $ivSize;

        $key = $this->pbkdf2('sha512', $key, $salt, $this->rounds, $length);

        $cipherKey = substr($key, 0, $keySize);
        $macKey = substr($key, $keySize, $keySize);
        $iv = substr($key, 2 * $keySize);
        return array($cipherKey, $macKey, $iv);
    }

    /**
     * Stretch the key using the PBKDF2 algorithm
     *
     * @see http://en.wikipedia.org/wiki/PBKDF2
     *
     * @param string $algo   The algorithm to use
     * @param string $key    The key to stretch
     * @param string $salt   A random salt
     * @param int    $rounds The number of rounds to derive
     * @param int    $length The length of the output key
     *
     * @returns string The derived key.
     */
    protected function pbkdf2($algo, $key, $salt, $rounds, $length) {
        $size   = strlen(hash($algo, '', true));
        $len    = ceil($length / $size);
        $result = '';
        for ($i = 1; $i <= $len; $i++) {
            $tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true);
            $res = $tmp;
            for ($j = 1; $j < $rounds; $j++) {
                 $tmp  = hash_hmac($algo, $tmp, $key, true);
                 $res ^= $tmp;
            }
            $result .= $res;
        }
        return substr($result, 0, $length);
    }

    protected function pad($data) {
        $length = mcrypt_get_block_size($this->cipher, $this->mode);
        $padAmount = $length - strlen($data) % $length;
        if ($padAmount == 0) {
            $padAmount = $length;
        }
        return $data . str_repeat(chr($padAmount), $padAmount);
    }

    protected function unpad($data) {
        $length = mcrypt_get_block_size($this->cipher, $this->mode);
        $last = ord($data[strlen($data) - 1]);
        if ($last > $length) return false;
        if (substr($data, -1 * $last) !== str_repeat(chr($last), $last)) {
            return false;
        }
        return substr($data, 0, -1 * $last);
    }
}

请注意,我正在使用PHP 5.6中添加的功能:hash_equals。如果低于5.6,则可以使用此替代函数,该函数使用双重HMAC验证实现定时安全比较功能:

function hash_equals($a, $b) {
    $key = mcrypt_create_iv(128, MCRYPT_DEV_URANDOM);
    return hash_hmac('sha512', $a, $key) === hash_hmac('sha512', $b, $key);
}

用法:

$e = new Encryption(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC);
$encryptedData = $e->encrypt($data, $key);

然后,解密:

$e2 = new Encryption(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC);
$data = $e2->decrypt($encryptedData, $key);

请注意,我$e2第二次向您展示了不同的实例仍将正确解密数据。

现在,它是如何工作的/为什么要在另一个解决方案上使用它:

  1. 按键

    • 按键不直接使用。而是通过标准PBKDF2派生来扩展密钥。

    • 用于加密的密钥对于每个加密的文本块都是唯一的。因此,提供的密钥将成为“主密钥”。因此,此类为密码和身份验证密钥提供密钥轮换。

    • 重要说明,该$rounds参数配置为具有足够强度的真随机密钥(至少128位加密安全随机数)。如果要使用密码或非随机密钥(或者随机性较小,则CS随机性为128位),则必须增加此参数。我建议密码至少为10000(您能负担得起的越多越好,但是它将增加运行时的信息)...

  2. 数据的完整性

    • 更新版本使用ENCRYPT-THEN-MAC,这是确保加密数据真实性的更好方法。
  3. 加密:

    • 它使用mcrypt实际执行加密。我建议使用MCRYPT_BLOWFISHMCRYPT_RIJNDAEL_128cypher和MCRYPT_MODE_CBC模式。它足够强大,并且仍然相当快(加密和解密周期在我的计算机上大约需要1/2秒)。

现在,从第一个列表的第3点开始,将为您提供的功能是这样的:

function makeKey($userKey, $serverKey, $userSuppliedKey) {
    $key = hash_hmac('sha512', $userKey, $serverKey);
    $key = hash_hmac('sha512', $key, $userSuppliedKey);
    return $key;
}

您可以在makeKey()函数中对其进行拉伸,但是由于稍后将对其进行拉伸,因此这样做没有太大意义。

至于存储大小,取决于纯文本。Blowfish使用8字节的块大小,因此您将拥有:

  • 盐16字节
  • hmac的64个字节
  • 数据长度
  • 填充,以便数据长度%8 == 0

因此,对于16个字符的数据源,将有16个字符的数据要加密。因此,由于填充,实际的加密数据大小为16个字节。然后为salt添加16字节,为hmac添加64字节,总存储大小为96字节。因此充其量只有80个字符的开销,最糟糕的是87个字符的开销...

希望对您有帮助...

注意: 12/11/12:我刚刚使用更好的加密方法,使用更好的派生密钥并修复了MAC生成来更新此类。


3
有人不明白这意味着“休息”。@IRC在课程上做得很好,那真是该死的漂亮代码。
jcolebrand 2011年

1
以下返回false。知道为什么吗?$ x =新加密(MCRYPT_BlOWFISH,MCRYPT_MODE_CBC); $ test = $ x-> encrypt(“ test”,“ a”); echo var_dump($ x-> decrypt($ test,“ a”));
The Wavelength

2
哦,又在解密函数改变两个-64s到-128帮助(所以你得到$enc = substr($data, 128, -128)$mac = substr($data, -128);
cosmorogers

4
@ircmaxell自上次修改代码以来已经有一段时间了,所以我想知道它是否是最新的。我需要在金融应用程序中使用类似的内容,如果您在此类课程中还可以的话,那会很好:-)
nt.bas

2
警告!mcrypt扩展已经废弃了将近十年,并且使用起来也相当复杂。因此,不建议使用OpenSSL,而建议将OpenSSL从内核中删除并放入PHP 7.2中的PECL中。 th1.php.net/manual/en/migration71.deprecated.php
vee

15

如何在PHP中加密和解密密码? 通过实现许多加密算法之一。(或使用许多库之一)

什么是最安全的密码加密算法? 有很多不同的算法,没有一种是100%安全的。但是其中许多对于商业甚至军事目的来说都是足够安全的

我在哪里存储私钥? 如果您决定实施公钥-加密算法(例如RSA),则不存储私钥。用户具有私钥。您的系统具有公共密钥,可以将其存储在您希望的任何位置。

除了存储私钥之外,要求用户在需要解密密码的任何时间都输入私钥是一个好主意吗?(可以信任此应用程序 的用户如果您的用户可以记住很长的素数,那么-是的,为什么不呢。但是通常,您需要提出一个系统,该系统将允许用户将其密钥存储在某个地方。

密码可以通过什么方式被盗和解密?我需要注意什么? 这取决于所使用的算法。但是,请始终确保您不要向用户发送未加密的密码。在客户端上对其进行加密/解密,或者使用https(或用户其他加密方式来保护服务器与客户端之间的连接)。

但是,如果您仅需要以加密方式存储密码,我建议您使用简单的XOR密码。这种算法的主要问题是它很容易被频率分析破坏。但是,由于通常密码不是由较长的英文文本制成的,所以我认为您不必为此担心。XOR密码的第二个问题是,如果您同时拥有加密和解密形式的消息,则可以轻松地找到用于加密的密码。同样,在您的情况下这不是什么大问题,因为它只会影响已经受到其他方式损害的用户。


关于答案3,当您说用户具有私钥时,我不明白这是什么意思。您不建议用户手动将私钥传递到应用程序中,那么私钥又如何传递给应用程序呢?
HyderA 2011年

嗯,这有点问题。私钥可以存储在文本文件中,然后复制粘贴到应用程序中。密钥也可以存储在服务器上,但是在这种情况下,仍然应该使用其他加密算法(例如XOR)对其进行加密。在这种情况下,使用XOR是足够安全的,因为只有一对密码消息对,并且消息是相当随机的,因此不使用频率分析冷。
伊凡

4
我当然不建议您自己实施加密算法,存在太多潜在的陷阱,并且现有库已经过许多人的测试和分析。
长耳

XOR的主要问题是,如果某人窃取了您的应用程序数据并且仅知道一个用户密码,则他们可以解密该用户的所有其他密码。
长耳朵

1
@Ivan:是的,但是这是我认为DIY真的糟糕的一种情况,除非您真正了解加密技术。存在强大的密码,为什么不使用它们呢?
ircmaxell

13
  1. 您追求的PHP功能是Mcrypt(http://www.php.net/manual/zh/intro.mcrypt.php)。

对于本示例,手册中的示例进行了略微编辑):

<?php
$iv_size = mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$key = "This is a very secret key";
$pass = "PasswordHere";
echo strlen($pass) . "\n";

$crypttext = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $pass, MCRYPT_MODE_ECB, $iv);
echo strlen($crypttext) . "\n";
?>

您将使用mcrypt_decrypt解密密码。

  1. 最好的算法是比较主观的-问5个人,得到5个答案。就个人而言,如果默认值(Blowfish)对您来说不够好,那么您可能会遇到更大的问题!

  2. 鉴于PHP需要加密它-不确定您可以将其隐藏在任何地方-欢迎对此发表评论。标准的PHP最佳编码实践当然适用!

  3. 考虑到加密密钥仍然会存在于您的代码中,如果您的应用程序的其余部分是安全的,则不确定会获得什么。

  4. 显然,如果加密的密码和加密密钥被盗,则游戏结束。

我会回答一个问题-我不是PHP加密专家,但是,我认为我的回答是标准做法-我欢迎其他人提出意见。


$pass = $text。我认为他是为了解决这个问题而更改的,并且没有注意到第二次出现。
HyderA 2011年

3
有两件事要注意。首先,MCRYPT_MODE_ECB不使用IV。其次,如果确实如此,则需要存储IV,因为没有它就无法解密数据……
ircmaxell 2011年

“最好的算法是很主观的-问5个人,得到5个答案。就个人而言,如果默认的(河豚)对您来说不够好,那么您可能会遇到更大的问题!” 这是完全错误的。任何加密专家都会或多或少地同意gist.github.com/tqbf/be58d2d39690c3b366ad,其中特别排除了河豚
Scott Arciszewski 2016年

6

许多用户建议使用mcrypt ...是正确的,但是我想更进一步以使其易于存储和传输(因为有时加密值可能使它们难以使用curl或json等其他技术进行发送) 。

使用mcrypt成功加密后,请通过base64_encode运行它,然后将其转换为十六进制代码。一旦使用十六进制代码,就可以通过多种方式轻松进行传输。

$td = mcrypt_module_open('tripledes', '', 'ecb', '');
$iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
$key = substr("SUPERSECRETKEY",0,mcrypt_enc_get_key_size($td));
mcrypt_generic_init($td, $key, $iv);
$encrypted = mcrypt_generic($td, $unencrypted);
$encrypted = $ua."||||".$iv;
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
$encrypted = base64_encode($encrypted);
$encrypted = array_shift(unpack('H*', $encrypted));

在另一边:

$encrypted = pack('H*', $encrypted);
$encrypted = base64_decode($encrypted);
list($encrypted,$iv) = explode("||||",$encrypted,2);
$td = mcrypt_module_open('tripledes', '', 'ecb', '');
$key = substr("SUPERSECRETKEY",0,mcrypt_enc_get_key_size($td));
mcrypt_generic_init($td, $key, $iv);
$unencrypted = mdecrypt_generic($td, $encrypted);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);


2
好吧-那是在2011年:P
布拉德利

5

如果您希望能够在没有用户交互的情况下设置用户密码,我只会建议使用公钥加密(这对于重置密码和共享密码非常方便)。

公钥

  1. OpenSSL的扩展,特别是openssl_public_encryptopenssl_private_decrypt
  2. 如果您的密码适合密钥大小,这将是简单的RSA-填充,否则您需要一个对称层
  3. 存储每个用户的两个密钥,私钥的密码是他们的应用程序密码

对称的

  1. 这个Mcrypt扩展
  2. AES-256可能是一个安全的选择,但这本身可能就是一个SO问题。
  3. 您不需要-这将是他们的应用程序密码

4。是的-用户每次必须输入应用程序密码,但是将其存储在会话中会引发其他问题

5

  • 如果有人窃取了应用程序数据,则它与对称密码一样安全(对于公用密钥方案,它用于通过密码保护私钥)。
  • 您的应用程序绝对应该只能通过SSL(最好使用客户端证书)进行访问。
  • 考虑添加第二个身份验证因素,每个会话仅使用一次,例如通过SMS发送的令牌。

避免使用mcrypt,请谨慎使用openssl_private_decrypt()
Scott Arciszewski '16

2

我尝试过类似的方法,但请注意,我不是密码专家,也不对php任何编程语言都有深入的了解。这只是一个主意。我的想法是将key一些database不易预测的位置存储在某个文件中(或手动输入)(当然,总有一天会解密任何东西,这是为了延长解密时间)并加密敏感信息。

$iv_size = mcrypt_get_iv_size(MCRYPT_BLOWFISH , MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$key = "evenifyouaccessmydatabaseyouwillneverfindmyemail";
$text = "myemail@domain.com";
echo "Key : ".$key."<br/>";
echo "Text : ".$text . "<br/>";
echo "Md5 : ".md5($text). "<br/>";
echo "Sha1 : ".sha1($text). "<br/>";



$crypttext = mcrypt_encrypt(MCRYPT_BLOWFISH , $key, $text, MCRYPT_MODE_ECB, $iv);
echo "Crypted Data : ".$crypttext."<br>";

$base64 = base64_encode($crypttext);
echo "Encoded Data : ".$base64."<br/>";
$decode =  base64_decode($base64);


$decryptdata = mcrypt_decrypt(MCRYPT_BLOWFISH , $key, $crypttext, MCRYPT_MODE_ECB, $iv);

echo "Decoded Data : ".ereg_replace("?", null ,  $decryptdata); 
//event if i add '?' to the sting to the text it works, I don't know why.

请注意,这只是一个概念。此代码的任何改进都是非常可观的。


2

密码用于硬件设备,因此无法检查哈希

h?我不明白 您只是说密码必须是可恢复的吗?

就像其他人所说的那样,mcrypt扩展提供了对许多加密功能的访问-但是,您正在邀请用户将他们所有的鸡蛋放在一个篮子里-一个可能成为攻击者目标的鸡蛋-如果您甚至不知道,如何开始解决问题,那么您正在给用户带来伤害。您无权了解如何保护数据。

大多数安全漏洞的产生不是因为基础算法存在缺陷或不安全-而是由于在应用程序代码中使用算法的方式存在问题。

话虽如此,有可能建立一个合理的安全系统。

仅当您要求用户创建另一个(特定)用户可以读取的安全消息时,才应考虑使用非对称加密。原因是它的计算量很大。如果您只想提供一个供用户输入和检索自己的数据的存储库,则对称加密就足够了。

但是,如果将用于解密消息的密钥存储在与加密消息相同的位置(或存储加密消息的位置),则系统不安全。使用与解密密钥相同的令牌对用户进行身份验证(或在非对称加密的情况下,使用令牌作为私钥密码)。由于您至少需要将令牌存储在至少暂时进行解密的服务器上,因此您可能要考虑使用不可搜索的会话存储基板,或者将令牌直接传递给与将存储密码的会话相关联的守护程序内存中的令牌并按需执行消息解密。


1

使用password_hashpassword_verify

<?php
/**
 * In this case, we want to increase the default cost for BCRYPT to 12.
 * Note that we also switched to BCRYPT, which will always be 60 characters.
 */
$options = [
    'cost' => 12,
];
echo password_hash("rasmuslerdorf", PASSWORD_BCRYPT, $options)."\n";
?>

并解密:

<?php
// See the password_hash() example to see where this came from.
$hash = '$2y$07$BCryptRequires22Chrcte/VlQH0piJtjXl.0t1XkA8pw9dMXTpOq';

if (password_verify('rasmuslerdorf', $hash)) {
    echo 'Password is valid!';
} else {
    echo 'Invalid password.';
}
?>
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.