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

176

我正在创建一个应用程序,用于存储密码,用户可以检索并查看密码。 这些密码用于硬件设备,因此无法根据哈希值进行检查。

我需要知道以下几点:

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

  2. 使用哪种最安全的算法来加密密码?

  3. 私钥应该存储在哪里?

  4. 不存储私钥,要求用户在需要解密密码时输入私钥是否是好主意? (此应用程序的用户可信任)

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


2
注意:Libsodium现在已经编译到PHP核心中,适用于版本>=7.2。这是“首选”解决方案,因为它充满了现代方法,而mcrypt则被认为已过时并已被删除。 - Exhibitioner
8个回答

220

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

  1. How do I encrypt and decrypt a password in PHP?

    See below for a strong class that takes care of everything for you:

  2. What is the safest algorithm to encrypt the passwords with?

    safest? any of them. The safest method if you're going to encrypt is to protect against information disclosure vulnerabilities (XSS, remote inclusion, etc.). If it gets out, the attacker can eventually crack the encryption (no encryption is 100% un-reversible without the key - As @NullUserException points out this is not entirely true. There are some encryption schemes that are impossible to crack such as one-time pad).

  3. Where do I store the private key?

    I would use three keys. One is user supplied, one is application specific and the other is user specific (like a salt). The application specific key can be stored anywhere (in a configuration file outside of the web-root, in an environmental variable, etc.). The user specific one would be stored in a column in the db next to the encrypted password. The user supplied one would not be stored. Then, you'd do something like this:

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

    The benefit there, is that any two of the keys can be compromised without the data being compromised. If there's a SQL injection attack, they can get the $userKey, but not the other two. If there's a local server exploit, they can get $userKey and $serverKey, but not the third $userSuppliedKey. If they go beat the user with a wrench, they can get the $userSuppliedKey, but not the other two (but then again, if the user is beaten with a wrench, you're too late anyway).

  4. Instead of storing the private key, is it a good idea to require users to enter the private key any time they need a password decrypted? (Users of this application can be trusted)

    Absolutely. In fact, that's the only way I would do it. Otherwise you'd need to store an unencrypted version in a durable storage format (shared memory, such as APC or Memcached, or in a session file). That's exposing yourself to additional compromises. Never store the unencrypted version of the password in anything except a local variable.

  5. In what ways can the password be stolen and decrypted? What do I need to be aware of?

    Any form of compromise of your systems will let them view encrypted data. If they can inject code or get to your filesystem, they can view decrypted data (since they can edit the files that decrypt the data). Any form of replay or MITM attack will also give them full access to the keys involved. Sniffing the raw HTTP traffic will also give them the keys.

    Use SSL for all traffic. And make sure nothing on the server has any kind of vulnerabilities (CSRF, XSS, SQL injection, privilege escalation, remote code execution, etc.).

这是一个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 的版本,则可以使用此替代函数,该函数使用 double HMAC verification 实现了一个 timing-safe comparison 函数:
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位加密安全随机数)的真随机密钥。 如果您要使用密码或非随机密钥(或比128位CS随机更少的随机性),则必须增加此参数。 对于密码,建议最低10000(您能负担得起的越多越好,但会增加运行时间)...
2. 数据完整性
- 更新版本使用ENCRYPT-THEN-MAC,这是一种更好的方法,可确保加密数据的真实性。
3. 加密:
  • 它使用mcrypt来执行加密。我建议使用MCRYPT_BLOWFISHMCRYPT_RIJNDAEL_128密码和MCRYPT_MODE_CBC模式。它足够强大,而且仍然相当快(在我的机器上,加密和解密循环大约需要半秒钟)。

现在,关于第一个列表中的第3点,这将给您一个像这样的函数:

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

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

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

  • 16个字节用于盐
  • 64个字节用于hmac
  • 数据长度
  • 填充以使数据长度%8 == 0

因此,对于16个字符的数据源,将有16个字符的数据要加密。因此,实际加密数据的大小为16个字节,因为填充。然后添加16个字节的盐和64个字节的hmac,总存储大小为96个字节。因此,最多有80个字符的开销,最坏的情况下有87个字符的开销...


3
有人不理解“break”的含义。@IRC,你在课上做得很好,那是非常棒的代码。 - jcolebrand
1
以下代码返回 false,有任何想法为什么吗?$x = new Encryption(MCRYPT_BlOWFISH, MCRYPT_MODE_CBC);$test = $x->encrypt("test", "a"); echo var_dump($x->decrypt($test, "a")); - The Wavelength
2
在解密函数中,将两个“-64”更改为“-128”有所帮助(因此您可以得到$enc = substr($data, 128, -128)$mac = substr($data, -128);)。 - cosmorogers
4
@ircmaxell:自上次代码修订以来已经有一段时间了,所以我想知道它是否是最新的。我需要使用类似的东西来开发金融应用程序,如果你能确认这个类是可行的,那就太好了 :-) - nt.bas
2
警告! mcrypt扩展已经被废弃了将近十年,而且使用起来相当复杂。因此,在PHP 7.2中,它已被弃用,取而代之的是OpenSSL,它将从核心移除并进入PECL。 http://th1.php.net/manual/en/migration71.deprecated.php - vee
显示剩余9条评论

15

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

通过实现多种加密算法(或使用多个库)来完成。

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

有很多不同的算法,其中没有一个是100%安全的。但其中许多对于商业甚至军事目的来说都足够安全。

我应该将私钥存储在哪里?

如果您决定实现公钥密码算法(例如RSA),则无需存储私钥。用户拥有私钥。您的系统具有公钥,可以将其存储在任何您希望的地方。

不将私钥存储在服务器上,要求用户每次需要解密密码时输入私钥是否是一个好主意?(该应用程序的用户可以信任)

如果您的用户能够记住极长的质数,那么可以。但通常您需要想出一种允许用户在某处存储其密钥的系统。

密码可以以何种方式被窃取和解密?我需要注意什么?

这取决于所使用的算法。但是,请始终确保不要将密码明文发送给用户或从用户那里接收明文密码。可以在客户端对其进行加密/解密,或使用HTTPS(或其他密码学手段)来保护服务器和客户端之间的连接。

但如果你只需要以加密方式存储密码,我建议你使用简单的XOR密码。这种算法的主要问题是它容易被频率分析攻破。然而,通常密码不是由长段的英文文本组成的,所以我认为你不用担心这个问题。XOR密码的第二个问题是,如果你有一条消息的加密和解密形式,你可以轻松地找到用于加密的密码。同样,在你的情况下,这不是一个大问题,因为它只影响已经被其他手段攻击的用户。


在第三个答案中,当你说用户拥有私钥时,我不明白这是什么意思。你不建议用户手动将私钥传递到应用程序中,那么私钥又是如何传递到应用程序中的呢? - HyderA
4
我不建议自己实现加密算法,因为存在太多潜在的风险,而现有的库已经被许多人测试和分析过。 - Long Ears
1
@Ivan:是的,但这是我认为除非你真正了解密码学,否则DIY真的非常糟糕的情况之一。存在着强大的密码,为什么不使用它们呢? - ircmaxell
@Ivan:说得好。我只是想指出,人们不应该试图为任何他们关心的事情构建自己的算法(即使是简单的异或密码)。这是一个很好的学习经验,但对于实际用例,最好使用已经过审查的公共算法... - ircmaxell
这是一个非常冗长的回答。问题:“我怎样才能实现这个?”回答:“有很多方法,找出并选择其中一个。” - Madbreaks
显示剩余4条评论

13
  1. 你需要的PHP模块是Mcrypt

    以下示例略有修改,仅供参考:

<?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 来解密您的密码。

最好的算法是相对主观的 - 询问五个人,得到五个答案。就我个人而言,如果默认的(Blowfish)无法满足您的要求,那么您可能有更大的问题!
鉴于PHP需要加密,我不确定您能否将其隐藏在任何位置。当然,适用于标准PHP最佳编码实践!
考虑到加密密钥无论如何都会出现在您的代码中,除非应用程序的其余部分安全,否则我不知道您会获得什么好处。
显然,如果加密密码和加密密钥被盗,那么游戏结束。
我要在我的回答中加入一条说明。我不是PHP密码学专家,但我认为我的回答符合标准实践。

$pass = $text。我认为他更改了这个以迎合问题,并没有注意到第二次出现。 - HyderA
3
有两件事需要注意。首先,MCRYPT_MODE_ECB不使用IV。其次,如果它使用了IV,你需要存储IV,因为没有IV你无法解密数据... - ircmaxell
最好的算法相当主观-问5个人,得到5个答案。如果默认算法(Blowfish)对您来说不够好,那么您可能有更大的问题!这是完全错误的。任何加密专家都会在 https://gist.github.com/tqbf/be58d2d39690c3b366ad 上基本上同意,该文明确排除了Blowfish。 - Scott Arciszewski

6
很多用户建议使用mcrypt,这是正确的,但我想更进一步,使其易于存储和传输(有时使用其他技术如cURLJSON发送加密值可能会变得困难)。
在成功使用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);

Mcrypt不是一个很好的选择。 - Scott Arciszewski
2
好吧 - 那是在2011年 :P - Bradley

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

公钥

  1. OpenSSL扩展,特别是openssl_public_encryptopenssl_private_decrypt
  2. 假设您的密码适合密钥大小-填充,则这将是直接的RSA,否则您需要对称层
  3. 为每个用户存储两个密钥,私钥的密码短语是他们的应用程序密码

对称

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

两者都有

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

5

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

避免使用mcrypt,对于openssl_private_decrypt()要小心。 - Scott Arciszewski

3
“密码是用于硬件设备的,所以检查哈希值是行不通的。”
“嗯?我不明白。你是说密码必须可恢复吗?”
正如其他人所说,mcrypt扩展提供了许多加密功能——然而,你邀请用户把所有的鸡蛋放在一个篮子里——这个篮子可能成为攻击者的目标——如果你甚至不知道如何开始解决问题,那么你对用户做了一件不负责任的事情,你无法理解如何保护数据。
大多数安全漏洞并非因为底层算法存在缺陷或不安全,而是由于应用程序代码中使用算法的问题。
话虽如此,构建一个相当安全的系统是有可能的。
只有在需要用户创建一个能被另一个(特定的)用户读取的安全消息时,才应考虑使用非对称加密。原因是它的计算成本很高。如果你只想为用户提供输入和检索自己数据的存储库,对称加密就足够了。
然而,如果您将用于解密消息的密钥存储在与加密消息相同的位置(或存储加密消息的位置),那么系统就不安全了。使用相同的令牌用于用户身份验证和解密密钥(或在非对称加密的情况下,将令牌用作私钥密码短语)。由于您需要在至少暂时存储解密所需的令牌的服务器上存储令牌,因此您可能希望考虑使用非可搜索的会话存储基质,或直接将令牌传递给与会话相关联的守护进程,该守护进程将在内存中存储令牌并按需执行消息解密。

2
我尝试过类似的方法,但请注意我不是密码学专家,也没有深入了解 PHP 或任何编程语言。这只是一个想法。
我的想法是在某个文件或数据库中存储一个“密钥”(或手动输入),其位置不能轻易预测(当然,任何东西都会有一天被解密。概念是延长解密时间),并对敏感信息进行加密。
代码:
$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 "SHA-1: " . sha1($text) . "<br/>";

$crypttext = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $text, MCRYPT_MODE_ECB, $iv);
echo "Encrypted 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);
// Even if I add '?' to the sting to the text it works. I don't know why.

请注意,这只是一个概念。非常欢迎对此代码进行任何改进。

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.';
}
?>

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