0

I thought I understood this, but my program won't decrypt and says the key is wrong, so I realize I need help. I thought the algorithm went:

Encryption:

  1. Get user password P
  2. Call hash_pbkdf2 to stretch P into a key K_pass_1
  3. Call another key-stretching algorithm (I don't know which, haven't done this yet) to turn K_pass_1 into K_auth_1
  4. Encrypt data with K_auth_1 K_pass_1

Decryption:

  1. Get user password P
  2. Call hash_pbkdf2 to stretch P into a key K_pass_2
  3. as above
  4. Decrypt data with K_auth_2 K_pass_2

Is that not correct? (Edit: It turns out it is, but that was too high level - my problem was more specific.)

Edit: Here is my code:

<?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>';
Miryafa
  • 163
  • 1
  • 2
  • 15
  • 3
    Where is your code along with test data, ket, iv modes. Add all that to the question. – zaph May 27 '16 at 22:47
  • 3
    This is why you don't write your own hashing functions. – Sverri M. Olsen May 27 '16 at 22:52
  • 2
    Why do you need to decrypt a password? – Mark Baker May 27 '16 at 23:00
  • @MarkBaker What are you talking about? I'm not decrypting a password anywhere. – Miryafa May 29 '16 at 03:44
  • 1
    @Miryafa - If you're not going to decrypt passwords, then why do you want to encrypt them? Why not hash them instead? – Mark Baker May 29 '16 at 09:15
  • @MarkBaker I'm not encrypting passwords anywhere in this algorithm – Miryafa May 31 '16 at 13:10
  • @zaph Added code and test data to the question – Miryafa May 31 '16 at 13:55
  • @SverriM.Olsen Where am I writing my own hashing functions? – Miryafa May 31 '16 at 13:55
  • 1
    After step 2 do you have the same key? Add that, in hex, to the echo and provide the output to the question. If so the problem is in the encryption. You need to know which part is failing, the key extension or the encryption. – zaph May 31 '16 at 14:38
  • @zaph No, testing it with var_dump shows the first $encryption_key is different from the second. – Miryafa May 31 '16 at 14:45
  • 1
    See the new answer. – zaph May 31 '16 at 14:55
  • 1
    I don't understand why you're calling `password_hash()` on `$ciphertext` but you should be aware that bcrypt only operates on the first 72 bytes of input. I'm thinking you might want [`hash_hmac()`](http://ee1.php.net/manual/en/function.hash-hmac.php) instead. – Sammitch May 31 '16 at 16:57
  • @Sammitch Thank you, that's important! I was using `password_hash()` to verify message integrity, and bcrypt for the reasons in the function header comments. I'll use `hash_hmac()` and [`hash_equals()`](http://php.net/manual/en/function.hash-equals.php) – Miryafa May 31 '16 at 18:31

3 Answers3

3

The problem is in the function:

function generate_key_from_password($password)

the line:

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

The same salt needs to be used in order to derive the same key.

A salt needs to be created outside of the generate_key_from_password function and passed in and it needs to be the same salt for encryption and decryption. This is usually done by creating a salt in the encryption function, passing it into the PBKDF2 function and prepending the salt to the encrypted output in the same manner as the iv. Then the same salt is available to the decryption function.

It is little things like this that make using encryption securely difficult. See RNCryptor-php and RNCryptor-Spec for an example that also includes authentication, the iteration count and a version.

zaph
  • 111,848
  • 21
  • 189
  • 228
1

Skip step 3, it is not necessary.

Make sure the key and iv are exactly the correct length. Insure you are using CBC mode and PKCS#7 (or PKCS#5).

zaph
  • 111,848
  • 21
  • 189
  • 228
  • Would you please explain why you say step 3 is not necessary? It's drawn directly from NIST 800-132, section 4: "The derived keying material is called a Master Key (MK), denoted as mk. The MK is used either 1) to generate one or more Data Protection Keys (DPKs) to protect data, or 2) to generate an intermediate key to protect one or more existing DPKs or generated from the MK using an approved Key Derivation Function (KDF) as defined in [2]. The MK shall not be used for other purposes." – Miryafa May 29 '16 at 03:46
  • 1
    At the level of encryption in general app use Option 1a is sufficient: *the MK (or a segment of the MK) is used directly as the DPK*. We generally use the MK directly for encryption. There are reasons for deriving a DPK in larger system, etc. *Note that the DPK may be a set of keys used for encryption and integrity* but that is not applicable here. – zaph May 29 '16 at 06:06
  • It sounds like you're reaching for HKDF (a key-splitting algorithm), not "another key-stretching algorithm". – Scott Arciszewski May 30 '16 at 18:50
  • Thanks for the replies. I added my code to the question to clarify what's going on. Also I tried using pkcs7 before encrypting and setting the OPENSSL_RAW_DATA option to 0 as in [this question](http://stackoverflow.com/questions/10916284/how-to-encrypt-decrypt-data-in-php), but I get the same result. – Miryafa May 31 '16 at 14:07
1

Use the optional 5th parameter $length for 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"

You probably also want raw output, so read the above-linked doc page for the method and caveats to that.

Sammitch
  • 30,782
  • 7
  • 50
  • 77
  • 1
    128 for `$length` does not make any sense. – zaph May 27 '16 at 22:54
  • 1
    @zaph there are 2 persistent problems in computing: naming things, cache invalidation, and off-by-one errors. – Sammitch May 27 '16 at 22:57
  • Thanks for replying. I've already been using the 5th parameter $length, and I added my code to the question to help make that clear. – Miryafa May 31 '16 at 14:05