如何在 PHP 中拉伸用户密码进行加密?
How to stretch user password for encryption in PHP?
我以为我明白了,但我的程序无法解密并说密钥错误,所以我意识到我需要帮助。我认为算法是这样的:
加密:
- 获取用户密码P
- 调用hash_pbkdf2将P拉伸成一个keyK_pass_1
调用另一个密钥扩展算法(我不知道是哪个,还没做过)把K_pass_1变成K_auth_1
- 用
K_auth_1加密数据K_pass_1
解密:
- 获取用户密码P
- 调用hash_pbkdf2将P拉伸成一个keyK_pass_2
同上
- 用
K_auth_2解密数据K_pass_2
这不对吗? (编辑:事实证明是这样,但级别太高了 - 我的问题更具体。)
编辑:这是我的代码:
<?php
$GLOBALS['key_size'] = 32; // 256 bits
class WeakCryptographyException extends Exception {
public function errorMessage() {
$errorMsg = 'Error on line '.$this->getLine().' in '.$this->getFile()
.': <b>'.$this->getMessage().'</b>There was a problem creating strong pseudo-random bytes: system may be broken or old.';
return $errorMsg;
}
}
class FailedCryptographyException extends Exception {
public function errorMessage() {
$errorMsg = 'Error on line '.$this->getLine().' in '.$this->getFile()
.': <b>'.$this->getMessage().'</b>There was a problem with encryption/decryption.';
return $errorMsg;
}
}
class InvalidHashException extends Exception {
public function errorMessage() {
$errorMsg = 'Error on line '.$this->getLine().' in '.$this->getFile()
.': <b>'.$this->getMessage().'</b>Password verification failed.';
return $errorMsg;
}
}
function generate_key_from_password($password) {
$iterations = 100000;
$salt = openssl_random_pseudo_bytes($GLOBALS['key_size'], $strong);
$output = hash_pbkdf2("sha256", $password, $salt, $iterations, $GLOBALS['key_size'], true);
if ($strong) {
return $output;
} else {
// system did not use a cryptographically strong algorithm to produce the pseudo-random bytes
throw new WeakCryptographyException();
}
}
/** Encrypts the input data with Authenticated Encryption. We specifically use
* openssl_encrypt($data, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv), where $iv is a 256-bit nonce
* generated with openssl_random_pseudo_bytes. Then we hash the output with bcrypt and prepend the hash and iv to
* the ciphertext to create an 'authenticated ciphertext' that can be fed directly into the my_decrypt method.
*
* @param $data string; The data to be encrypted
* @param $encryption_key string; A 256-bit key (which PHP reads as a string of characters)
* @return string The authenticated ciphertext, with the format: $hash . $iv . $ciphertext
* @throws FailedCryptographyException If there are errors during encryption
* @throws WeakCryptographyException If the openssl_random_pseudo_bytes method fails to use a cryptographically strong
* algorithm to produce pseudo-random bytes.
*
* Note that in creating a hash for the ciphertext, we use bcrypt instead of sha2. In particular, the difference in lines is:
* bcrypt: password_hash($ciphertext, PASSWORD_DEFAULT);
* sha2: hash_hmac('sha256', $ciphertext, $auth_key, true);
*
* And we chose this despite the fact that sha2 is the only acceptable hashing algorithm for NIST, because:
* 1. bcrypt is also widely considered a cryptographically secure hashing algorithm.
* 2. sha2 is not supported by PHP 5's password_hash method and bcrypt is.
* 3. PHP's password_verify method uses a hash created by the password_hash method and compares hashes in a way that is
* safe against timing attacks. There is no known way to make this comparison for other hashes in PHP.
*/
function my_openssl_encrypt($data, $encryption_key) {
$iv_size = 16; // 128 bits to match the block size for AES
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
if (!$strong) {
// system did not use a cryptographically strong algorithm to produce the bytes, don't consider them pseudo-random
throw new WeakCryptographyException();
}
$ciphertext = openssl_encrypt(
$data, // data
'AES-256-CBC', // cipher and mode
$encryption_key, // secret key
OPENSSL_RAW_DATA, // options: we use openssl padding
$iv // initialisation vector
);
if (!$ciphertext) {
$errormes = "";
while ($msg = openssl_error_string())
$errormes .= $msg . "<br />";
throw new FailedCryptographyException($errormes);
}
$auth = password_hash($ciphertext, PASSWORD_DEFAULT);
$auth_enc_name = $auth . $iv . $ciphertext;
return $auth_enc_name;
}
/** Decrypts a ciphertext encrypted with the method my_openssl_encrypt. First checks if the hash of the ciphertext
* matches the hash supplied in the input ciphertext, then decrypts the message if so. We specifically use
* openssl_decrypt($enc_name, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv), where $iv is a 256-bit nonce
* stored with the ciphertext.
*
* @param $ciphertext string; An authenticated ciphertext produced by my_openssl_encrypt
* @param $encryption_key string; A 256-bit key (which PHP reads as a string of characters)
* @return string The decrypted plaintext
* @throws FailedCryptographyException If there are errors during decryption
* @throws InvalidHashException If the password hash doesn't match the stored hash (this will almost always happen when
* any bits in the ciphertext are changed)
*/
function my_openssl_decrypt($ciphertext, $encryption_key) {
// verification
$auth = substr($ciphertext, 0, 60);
$iv = substr($ciphertext, 60, 16);
$enc_name = substr($ciphertext, 76);
if (password_verify($enc_name, $auth)) {
// perform decryption
$output = openssl_decrypt(
$enc_name,
'AES-256-CBC',
$encryption_key,
OPENSSL_RAW_DATA,
$iv
);
if (!$output) {
$errormes = "";
while ($msg = openssl_error_string())
$errormes .= $msg . "<br />";
throw new FailedCryptographyException($errormes);
}
return $output;
} else {
throw new InvalidHashException();
}
}
// Testing
function testEnc($message)
{
$encryption_key = generate_key_from_password("123456");
$auth_ciphertext = my_openssl_encrypt($message, $encryption_key);
$encryption_key = generate_key_from_password("123456");
$plaintext = my_openssl_decrypt($auth_ciphertext, $encryption_key);
echo "<p>Original message: " . $message .
"</p><p>Encryption (hex): " . bin2hex($auth_ciphertext) .
"</p><p>Plaintext: " . $plaintext . "</p>";
echo "<p>Bytes of input: " . (strlen($message) * 2) .
"<br />Bytes of ciphertext: " . (strlen($auth_ciphertext) * 2) . "</p>";
}
echo '<p>Test 1: ';
testEnc('Hello World');
echo '</p>';
跳过第3步,不需要。
确保密钥和 iv 的长度完全正确。确保您使用的是 CBC 模式和 PKCS#7(或 PKCS#5)。
将可选的第 5 个参数 $length
用于 hash_pbkdf2()
:
var_dump(hash_pbkdf2("sha256", "foobar", "salty", 100));
// string(64) "5d808ee6539c7d0437e857a586c844900bf0969d1af70aea4c3848550d9038ab"
var_dump(hash_pbkdf2("sha256", "foobar", "salty", 100, 32));
// string(32) "5d808ee6539c7d0437e857a586c84490"
var_dump(hash_pbkdf2("sha256", "foobar", "salty", 100, 128));
// string(128) "5d808ee6539c7d0437e857a586c844900bf0969d1af70aea4c3848550d9038abb2853bf0cf24c9d010555394f958fa647a04b232f993c35916977b4ef5a57dcc"
您可能还需要原始输出,因此请阅读上面链接的文档页面以了解方法和注意事项。
问题出在函数中:
function generate_key_from_password($password)
行:
$salt = openssl_random_pseudo_bytes($GLOBALS['key_size'], $strong);
需要使用相同的盐才能导出相同的密钥。
需要在generate_key_from_password
函数外部创建盐并传入,并且加密和解密需要相同的盐。这通常是通过在加密函数中创建盐,将其传递给 PBKDF2 函数并以与 iv 相同的方式将盐添加到加密输出来完成的。然后解密函数可以使用相同的盐。
正是这样的小事情使得安全地使用加密变得困难。有关还包括身份验证、迭代计数和版本的示例,请参阅 RNCryptor-php and RNCryptor-Spec。
我以为我明白了,但我的程序无法解密并说密钥错误,所以我意识到我需要帮助。我认为算法是这样的:
加密:
- 获取用户密码P
- 调用hash_pbkdf2将P拉伸成一个keyK_pass_1
调用另一个密钥扩展算法(我不知道是哪个,还没做过)把K_pass_1变成K_auth_1- 用
K_auth_1加密数据K_pass_1
解密:
- 获取用户密码P
- 调用hash_pbkdf2将P拉伸成一个keyK_pass_2
同上- 用
K_auth_2解密数据K_pass_2
这不对吗? (编辑:事实证明是这样,但级别太高了 - 我的问题更具体。)
编辑:这是我的代码:
<?php
$GLOBALS['key_size'] = 32; // 256 bits
class WeakCryptographyException extends Exception {
public function errorMessage() {
$errorMsg = 'Error on line '.$this->getLine().' in '.$this->getFile()
.': <b>'.$this->getMessage().'</b>There was a problem creating strong pseudo-random bytes: system may be broken or old.';
return $errorMsg;
}
}
class FailedCryptographyException extends Exception {
public function errorMessage() {
$errorMsg = 'Error on line '.$this->getLine().' in '.$this->getFile()
.': <b>'.$this->getMessage().'</b>There was a problem with encryption/decryption.';
return $errorMsg;
}
}
class InvalidHashException extends Exception {
public function errorMessage() {
$errorMsg = 'Error on line '.$this->getLine().' in '.$this->getFile()
.': <b>'.$this->getMessage().'</b>Password verification failed.';
return $errorMsg;
}
}
function generate_key_from_password($password) {
$iterations = 100000;
$salt = openssl_random_pseudo_bytes($GLOBALS['key_size'], $strong);
$output = hash_pbkdf2("sha256", $password, $salt, $iterations, $GLOBALS['key_size'], true);
if ($strong) {
return $output;
} else {
// system did not use a cryptographically strong algorithm to produce the pseudo-random bytes
throw new WeakCryptographyException();
}
}
/** Encrypts the input data with Authenticated Encryption. We specifically use
* openssl_encrypt($data, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv), where $iv is a 256-bit nonce
* generated with openssl_random_pseudo_bytes. Then we hash the output with bcrypt and prepend the hash and iv to
* the ciphertext to create an 'authenticated ciphertext' that can be fed directly into the my_decrypt method.
*
* @param $data string; The data to be encrypted
* @param $encryption_key string; A 256-bit key (which PHP reads as a string of characters)
* @return string The authenticated ciphertext, with the format: $hash . $iv . $ciphertext
* @throws FailedCryptographyException If there are errors during encryption
* @throws WeakCryptographyException If the openssl_random_pseudo_bytes method fails to use a cryptographically strong
* algorithm to produce pseudo-random bytes.
*
* Note that in creating a hash for the ciphertext, we use bcrypt instead of sha2. In particular, the difference in lines is:
* bcrypt: password_hash($ciphertext, PASSWORD_DEFAULT);
* sha2: hash_hmac('sha256', $ciphertext, $auth_key, true);
*
* And we chose this despite the fact that sha2 is the only acceptable hashing algorithm for NIST, because:
* 1. bcrypt is also widely considered a cryptographically secure hashing algorithm.
* 2. sha2 is not supported by PHP 5's password_hash method and bcrypt is.
* 3. PHP's password_verify method uses a hash created by the password_hash method and compares hashes in a way that is
* safe against timing attacks. There is no known way to make this comparison for other hashes in PHP.
*/
function my_openssl_encrypt($data, $encryption_key) {
$iv_size = 16; // 128 bits to match the block size for AES
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
if (!$strong) {
// system did not use a cryptographically strong algorithm to produce the bytes, don't consider them pseudo-random
throw new WeakCryptographyException();
}
$ciphertext = openssl_encrypt(
$data, // data
'AES-256-CBC', // cipher and mode
$encryption_key, // secret key
OPENSSL_RAW_DATA, // options: we use openssl padding
$iv // initialisation vector
);
if (!$ciphertext) {
$errormes = "";
while ($msg = openssl_error_string())
$errormes .= $msg . "<br />";
throw new FailedCryptographyException($errormes);
}
$auth = password_hash($ciphertext, PASSWORD_DEFAULT);
$auth_enc_name = $auth . $iv . $ciphertext;
return $auth_enc_name;
}
/** Decrypts a ciphertext encrypted with the method my_openssl_encrypt. First checks if the hash of the ciphertext
* matches the hash supplied in the input ciphertext, then decrypts the message if so. We specifically use
* openssl_decrypt($enc_name, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv), where $iv is a 256-bit nonce
* stored with the ciphertext.
*
* @param $ciphertext string; An authenticated ciphertext produced by my_openssl_encrypt
* @param $encryption_key string; A 256-bit key (which PHP reads as a string of characters)
* @return string The decrypted plaintext
* @throws FailedCryptographyException If there are errors during decryption
* @throws InvalidHashException If the password hash doesn't match the stored hash (this will almost always happen when
* any bits in the ciphertext are changed)
*/
function my_openssl_decrypt($ciphertext, $encryption_key) {
// verification
$auth = substr($ciphertext, 0, 60);
$iv = substr($ciphertext, 60, 16);
$enc_name = substr($ciphertext, 76);
if (password_verify($enc_name, $auth)) {
// perform decryption
$output = openssl_decrypt(
$enc_name,
'AES-256-CBC',
$encryption_key,
OPENSSL_RAW_DATA,
$iv
);
if (!$output) {
$errormes = "";
while ($msg = openssl_error_string())
$errormes .= $msg . "<br />";
throw new FailedCryptographyException($errormes);
}
return $output;
} else {
throw new InvalidHashException();
}
}
// Testing
function testEnc($message)
{
$encryption_key = generate_key_from_password("123456");
$auth_ciphertext = my_openssl_encrypt($message, $encryption_key);
$encryption_key = generate_key_from_password("123456");
$plaintext = my_openssl_decrypt($auth_ciphertext, $encryption_key);
echo "<p>Original message: " . $message .
"</p><p>Encryption (hex): " . bin2hex($auth_ciphertext) .
"</p><p>Plaintext: " . $plaintext . "</p>";
echo "<p>Bytes of input: " . (strlen($message) * 2) .
"<br />Bytes of ciphertext: " . (strlen($auth_ciphertext) * 2) . "</p>";
}
echo '<p>Test 1: ';
testEnc('Hello World');
echo '</p>';
跳过第3步,不需要。
确保密钥和 iv 的长度完全正确。确保您使用的是 CBC 模式和 PKCS#7(或 PKCS#5)。
将可选的第 5 个参数 $length
用于 hash_pbkdf2()
:
var_dump(hash_pbkdf2("sha256", "foobar", "salty", 100));
// string(64) "5d808ee6539c7d0437e857a586c844900bf0969d1af70aea4c3848550d9038ab"
var_dump(hash_pbkdf2("sha256", "foobar", "salty", 100, 32));
// string(32) "5d808ee6539c7d0437e857a586c84490"
var_dump(hash_pbkdf2("sha256", "foobar", "salty", 100, 128));
// string(128) "5d808ee6539c7d0437e857a586c844900bf0969d1af70aea4c3848550d9038abb2853bf0cf24c9d010555394f958fa647a04b232f993c35916977b4ef5a57dcc"
您可能还需要原始输出,因此请阅读上面链接的文档页面以了解方法和注意事项。
问题出在函数中:
function generate_key_from_password($password)
行:
$salt = openssl_random_pseudo_bytes($GLOBALS['key_size'], $strong);
需要使用相同的盐才能导出相同的密钥。
需要在generate_key_from_password
函数外部创建盐并传入,并且加密和解密需要相同的盐。这通常是通过在加密函数中创建盐,将其传递给 PBKDF2 函数并以与 iv 相同的方式将盐添加到加密输出来完成的。然后解密函数可以使用相同的盐。
正是这样的小事情使得安全地使用加密变得困难。有关还包括身份验证、迭代计数和版本的示例,请参阅 RNCryptor-php and RNCryptor-Spec。