Как растянуть пароль пользователя для шифрования в PHP?

Я думал, что понял это, но моя программа не расшифровывает и говорит, что ключ неверен, поэтому я понимаю, что мне нужна помощь. Я думал, что алгоритм пошел:

Шифрование:

  1. Получить пароль пользователя P
  2. Вызовите hash_pbkdf2, чтобы растянуть P в ключ K_pass_1.
  3. Вызвать другой алгоритм растяжения ключей (не знаю какой, еще не делал этого), чтобы превратить K_pass_1 в K_auth_1
  4. Шифровать данные с помощью K_auth_1 K_pass_1

Расшифровка:

  1. Получить пароль пользователя P
  2. Вызовите hash_pbkdf2, чтобы растянуть P в ключ K_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>';

person Miryafa    schedule 27.05.2016    source источник
comment
Где ваш код вместе с тестовыми данными, режимами ket, iv. Добавьте все это к вопросу.   -  person zaph    schedule 28.05.2016
comment
Вот почему вы не пишете свои собственные хеш-функции.   -  person Sverri M. Olsen    schedule 28.05.2016
comment
Зачем нужно расшифровывать пароль?   -  person Mark Baker    schedule 28.05.2016
comment
@MarkBaker О чем ты говоришь? Я нигде не расшифровываю пароль.   -  person Miryafa    schedule 29.05.2016
comment
@Miryafa - Если вы не собираетесь расшифровывать пароли, то зачем вам их шифровать? Почему бы не хешировать их вместо этого?   -  person Mark Baker    schedule 29.05.2016
comment
@MarkBaker Я нигде не шифрую пароли в этом алгоритме   -  person Miryafa    schedule 31.05.2016
comment
@zaph Добавлен код и тестовые данные к вопросу   -  person Miryafa    schedule 31.05.2016
comment
@SverriM.Olsen Где я пишу свои собственные функции хеширования?   -  person Miryafa    schedule 31.05.2016
comment
После шага 2 у вас есть тот же ключ? Добавьте это в шестнадцатеричном формате к эхо и предоставьте вывод на вопрос. Если да, то проблема в шифровании. Вам нужно знать, какая часть выходит из строя, расширение ключа или шифрование.   -  person zaph    schedule 31.05.2016
comment
@zaph Нет, тестирование с помощью var_dump показывает, что первый $ encryption_key отличается от второго.   -  person Miryafa    schedule 31.05.2016
comment
Смотрите новый ответ.   -  person zaph    schedule 31.05.2016
comment
Я не понимаю, почему вы вызываете password_hash() для $ciphertext, но вы должны знать, что bcrypt работает только с первыми 72 байтами ввода. Я думаю, вам может понадобиться вместо этого hash_hmac().   -  person Sammitch    schedule 31.05.2016
comment
@Sammitch Спасибо, это важно! Я использовал password_hash() для проверки целостности сообщения и bcrypt по причинам, указанным в комментариях к заголовку функции. Я буду использовать hash_hmac() и hash_equals()   -  person Miryafa    schedule 31.05.2016


Ответы (3)


Проблема в функции:

function generate_key_from_password($password)

линия:

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

Для получения одного и того же ключа необходимо использовать одну и ту же соль.

Соль должна быть создана вне функции generate_key_from_password и передана, и она должна быть одной и той же солью для шифрования и дешифрования. Обычно это делается путем создания соли в функции шифрования, передачи ее в функцию PBKDF2 и добавления соли к зашифрованному выводу таким же образом, как и iv. Затем та же самая соль доступна для функции расшифровки.

Именно такие мелочи затрудняют безопасное использование шифрования. См. RNCryptor-php и RNCryptor-Spec для примера, который также включает аутентификацию, количество итераций и версию.

person zaph    schedule 31.05.2016

Пропустить шаг 3, это не обязательно.

Убедитесь, что ключ и iv имеют правильную длину. Убедитесь, что вы используете режим CBC и PKCS#7 (или PKCS#5).

person zaph    schedule 27.05.2016
comment
Не могли бы вы объяснить, почему вы говорите, что шаг 3 не нужен? Он взят непосредственно из NIST 800-132, раздел 4: Производный ключевой материал называется мастер-ключом (MK), обозначаемым как mk. MK используется либо 1) для создания одного или нескольких ключей защиты данных (DPK) для защиты данных, либо 2) для создания промежуточного ключа для защиты одного или нескольких существующих DPK или сгенерирован из MK с использованием утвержденной функции деривации ключей (KDF). ), как определено в [2]. МК не может быть использован для других целей. - person Miryafa; 29.05.2016
comment
На уровне шифрования общего использования приложений достаточно Варианта 1а: МК (или сегмент МК) используется непосредственно как DPK. Обычно мы используем МК напрямую для шифрования. Есть причины для получения DPK в более крупной системе и т. д. Обратите внимание, что DPK может быть набором ключей, используемых для шифрования и обеспечения целостности, но здесь это неприменимо. - person zaph; 29.05.2016
comment
Похоже, вы пытаетесь использовать HKDF (алгоритм разделения ключей), а не другой алгоритм растяжения ключей. - person Scott Arciszewski; 30.05.2016
comment
Спасибо за ответы. Я добавил свой код в вопрос, чтобы прояснить, что происходит. Также я попытался использовать pkcs7 перед шифрованием и установить для параметра OPENSSL_RAW_DATA значение 0, как в этот вопрос, но я получаю тот же результат. - person Miryafa; 31.05.2016

Используйте необязательный 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"

Вероятно, вам также нужен необработанный вывод, поэтому прочитайте указанную выше страницу документа, чтобы узнать о методе и предостережениях по этому поводу.

person Sammitch    schedule 27.05.2016
comment
128 для $length не имеет никакого смысла. - person zaph; 28.05.2016
comment
@zaph есть 2 постоянные проблемы в вычислениях: именование вещей, недействительность кеша и ошибки «один за другим». - person Sammitch; 28.05.2016
comment
Спасибо за ответ. Я уже использовал 5-й параметр $length и добавил свой код в вопрос, чтобы прояснить это. - person Miryafa; 31.05.2016