如何在 PHP 中拉伸用户密码进行加密?

How to stretch user password for encryption in PHP?

我以为我明白了,但我的程序无法解密并说密钥错误,所以我意识到我需要帮助。我认为算法是这样的:

加密:

  1. 获取用户密码P
  2. 调用hash_pbkdf2将P拉伸成一个keyK_pass_1
  3. 调用另一个密钥扩展算法(我不知道是哪个,还没做过)把K_pass_1变成K_auth_1
  4. K_auth_1加密数据K_pass_1

解密:

  1. 获取用户密码P
  2. 调用hash_pbkdf2将P拉伸成一个keyK_pass_2
  3. 同上
  4. 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