【问题标题】:How to stretch user password for encryption in PHP?如何在 PHP 中延长用户密码以进行加密?
【发布时间】:2016-05-27 22:40:30
【问题描述】:

我以为我明白这一点,但我的程序不会解密并说密钥错误,所以我意识到我需要帮助。我以为算法去了:

加密:

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

解密:

  1. 获取用户密码P
  2. 调用 hash_pbkdf2 将 P 拉伸成密钥 K_pass_2
  3. 同上
  4. 使用 K_auth_2 K_pass_2
  5. 解密数据

这不正确吗? (编辑:事实证明是这样,但这太高级了——我的问题更具体。)

编辑:这是我的代码:

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

【问题讨论】:

  • 您的代码以及测试数据、ket、iv 模式在哪里。将所有内容添加到问题中。
  • 这就是为什么你不编写自己的散列函数。
  • 为什么需要解密密码?
  • @Miryafa - 如果你不打算解密密码,那你为什么要加密它们呢?为什么不把它们散列呢?
  • 在第 2 步之后,您是否拥有相同的密钥?以十六进制将其添加到回声中,并将输出提供给问题。如果是这样,问题出在加密上。您需要知道哪个部分失败了,是密钥扩展还是加密。

标签: php encryption passwords symmetric-key


【解决方案1】:

问题出在函数中:

function generate_key_from_password($password)

行:

$salt = openssl_random_pseudo_bytes($GLOBALS['key_size'], $strong);

需要使用相同的盐才能派生相同的密钥。

需要在generate_key_from_password 函数之外创建一个盐并传入,并且它必须是相同的盐以用于加密和解密。这通常是通过在加密函数中创建盐,将其传递给 PBKDF2 函数并以与 iv 相同的方式将盐添加到加密输出来完成的。然后解密函数可以使用相同的盐。

正是这样的小事使安全地使用加密变得困难。有关还包括身份验证、迭代计数和版本的示例,请参阅 RNCryptor-phpRNCryptor-Spec

【讨论】:

    【解决方案2】:

    跳过第3步,没有必要。

    确保 key 和 iv 的长度完全正确。确保您使用的是 CBC 模式和 PKCS#7(或 PKCS#5)。

    【讨论】:

    • 您能解释一下为什么您说不需要第 3 步吗?它直接来自 NIST 800-132 第 4 节:“派生的密钥材料称为主密钥 (MK),表示为 mk。MK 用于 1) 生成一个或多个数据保护密钥 (DPK) 以保护数据,或 2) 生成中间密钥以保护一个或多个现有 DPK,或使用 [2] 中定义的经批准的密钥派生函数 (KDF) 从 MK 生成。MK 不得用于其他目的。"
    • 在一般应用程序使用的加密级别,选项 1a 就足够了:MK(或 MK 的一部分)直接用作 DPK。我们一般直接使用MK进行加密。在更大的系统等中派生 DPK 是有原因的。请注意,DPK 可能是一组用于加密和完整性的密钥,但这不适用于此处。
    • 听起来您正在使用 HKDF(一种密钥分割算法),而不是“另一种密钥拉伸算法”。
    • 感谢您的回复。我将我的代码添加到问题中以澄清发生了什么。我还尝试在加密之前使用 pkcs7 并将 OPENSSL_RAW_DATA 选项设置为 0,如 this question,但我得到了相同的结果。
    【解决方案3】:

    hash_pbkdf2() 使用可选的第五个参数$length

    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"
    

    您可能还需要原始输出,因此请阅读上面链接的文档页面了解方法和注意事项。

    【讨论】:

    • 128 for $length 没有任何意义。
    • @zaph 计算中存在 2 个持久性问题:命名事物、缓存失效和非一错误。
    • 感谢您的回复。我已经在使用第 5 个参数 $length,我将代码添加到问题中以帮助说明这一点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-12-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-07
    • 1970-01-01
    相关资源
    最近更新 更多