在PHP中确定性地从密码派生32字节的密钥

时间:2018-08-10 17:36:28

标签: php laravel encryption hash aes

今天我了解到“密码” 倾向于表示一个可记住的任意数量字符的字符串,而“ key” 则表示一个高度随机的位字符串(具体长度取决于所使用的加密算法。

所以今天我首先听说了Key derivation function的概念。

我对如何从任意长度的密码(在PHP中)中派生32字节的密钥感到困惑。

以下方法可行,但忽略了{[3]应该随机生成的盐”中的the instructionSodium也是如此):

$salt = 'this salt remains constant';
$iterations = 10;
$length = 32;
$aesKey = hash_pbkdf2('sha256', $somePasswordOfArbitraryLength, $salt, $iterations, $length, false);

以下方法也可以使用,但感觉也不正确:

$hash = password_hash($somePasswordOfArbitraryLength, PASSWORD_BCRYPT, ['cost' => $iterations]);
$aesKey = substr($hash, -$length);//this smells weird

在所有搜索中,我很惊讶我没有在PHP中找到确定性地从密码中派生32字节密钥的“正式”方式。

我应该怎么做?

P.S。我在PHP中使用Laravel,并想使用AES-256-CBC加密,如下所示:

$encrypter = new \Illuminate\Encryption\Encrypter($aesKey, 'AES-256-CBC');
$encryptedText = $encrypter->encrypt($text);

Laravel的encryption helper(例如Crypt::encryptString('Hello world.'))似乎不符合我的要求,因为我希望根据每个人的密码分别加密每个用户的数据。

我使用的任何密钥派生函数每次都需要生成相同的密钥,因为我将使用对称加密来解密该用户使用该密钥加密的字符串。

P.P.S。感谢问题123向我介绍某些概念。

3 个答案:

答案 0 :(得分:3)

对于hash-pbkdf2,您说:

  

“以下方法有效,但忽略了“ [盐]应该随机生成”的指示。

好吧,解决方法是随机生成盐,并将其与密文一起存储。有关如何在PHP中生成安全的随机字节的方法,请参见this question。然后可以将输出用作加密的密钥。当然,密钥将始终使用存储的盐和已存储的密码来重新生成,并且不需要存储。请注意,密钥由原始字节组成;最好是从hash-pbkdf2(最后一个参数)中检索原始密钥。

请注意,迭代次数应尽可能多。如今,通常认为100,000左右是最佳选择,但是越高越安全。攻击者需要花费大量时间来计算每个密码的结果密钥,并且由于密码仅包含30到60位(对于好的密码而言),确实有助于抵御字典攻击。

答案 1 :(得分:2)

如果希望每次解密或加密存储的字符串时让他们重新发送密码,则必须使用一致的密码哈希并将盐和迭代存储在某个位置。

如果使用password_hash函数,由于随机生成的盐,您将永远不会获得相同的值。

>>> password_hash('abc', PASSWORD_BCRYPT)
=> "$2y$10$xR8tZQd0ljF5Ks3QrQt7i.vAbv.xVUc97uh.fX4w0mi/A647HlEWS"
>>> password_hash('abc', PASSWORD_BCRYPT)
=> "$2y$10$KzZWeg.o/4TyJVryWrz/oeWQ6VGj0JnPDW.d.Cp0svu8k6qKBcbWu"

您可以在选项中添加一个空格,但是password_hash已弃用,因此建议您坚持使用第一个解决方案。

您不需要为每个人使用相同的盐,您可以生成随机盐并将其存储在某个地方,例如users表。

请记住,通过这种类型的密钥派生,您每次用户更改密码时都需要重新加密所有值。

答案 2 :(得分:0)

这是我更新的功能

但是我仍然希望得到专家的回答,因为这是非常非正式的和本土的,并且使我怀疑它是否违反了安全性的“最佳实践”。

我很惊讶没有找到内置在PHP中的简单函数。

/**
 * It seems like we just need a way of getting a 32-byte key when all we have is a human-memorizable password and a salt for that user. But this function feels home-grown; what is the most secure way to do PBE (password-based encryption)?
 * 
 * @param string $password
 * @param string $salt      Since this function must be deterministic (return a value consistently based on the inputs), it must accept a salt as an argument rather than generate a random salt every time. Storing a different salt for each user improves security.
 * @param int $length
 * @return string
 */
public static function deriveKey($password, $salt, $length = self::KEY_BYTES) {
    $iterations = max([intval(config('hashing.bcrypt.rounds')), 15]);
    $chars = 2 * $length; //In hex, a byte is always expressed as 2 characters. See more comments below and https://stackoverflow.com/a/43132091/.
    $rawOutput = false; //Default is false. When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits. Hexit = hexadecimal digit (like "bit" = binary digit). There are 16 hexits: the numbers 0 to 9 and the letters A to F.
    $key = hash_pbkdf2('sha256', $password, $salt, $iterations, $chars, $rawOutput); //A sha256 is 256 bits long (32 bytes), but the raw_output argument will determine how many characters the result has. https://stackoverflow.com/a/2241014/ and https://crypto.stackexchange.com/q/34995/
    return $key;
}

我想知道HaliteLibsodium-php是否提供这种功能。

似乎Libsodium具有crypto_pwhash函数,这可能正是我正在寻找的(并使用Argon2)。