Search code examples
phpencryptionopensslphp-openssl

openssl_decrypt() can't decrypt text encrypted on the commandline


For testing purposes, I wrote encrypt.bash and decrypt.bash, to prove that the encrypted data saved to encrypted.txt can successfully be decrypted.

Here are the bash files:

encrypt.bash

#!/bin/bash

message="This is my message, I hope you can see it. It's very long now."
key="sup3r_s3cr3t_p455w0rd"

echo "$message" | openssl enc \
    -aes-256-ctr \
    -e \
    -k "$key" \
    -iv "504914019097319c9731fc639abaa6ec" \
    -out encrypted.txt

decrypt.bash

#!/bin/bash

key="sup3r_s3cr3t_p455w0rd"

decrypted=$(openssl enc \
    -aes-256-ctr \
    -d \
    -k "$key" \
    -iv "504914019097319c9731fc639abaa6ec" \
    -in encrypted.txt)

echo "Decrypted message: $decrypted"

Running bash decrypt.bash outputs the following:

Decrypted message: This is my message, I hope you can see it. It's very long now.

Where I'm struggling is reading the encrypted.txt file with PHP and decrypting it with openssl_decrypt. As far as I can tell, I'm using all the same settings, and working with binary data correctly, but obviously I'm doing something wrong.

decrypt.php

<?php
$key = "sup3r_s3cr3t_p455w0rd";

$encrypted = file_get_contents("encrypted.txt");
$iv = hex2bin("504914019097319c9731fc639abaa6ec");
$decrypted = openssl_decrypt(
    $encrypted,
    "aes-256-ctr",
    $key,
    0,
    $iv,
);

echo "Decrypted message: $decrypted";

Running php decrypt.php outputs the following:

Decrypted message: ��c�������Pb�j��

It seems so simple when boiled down like this, but I am struggling to see where the bug exists in my code.


Solution

  • The -k option does not specify a key, but a password. From this password, together with a randomly generated 8 bytes salt, the key is derived using the derivation function EVP_BytesToKey(). The encrypted data is returned in OpenSSL format, which consists of the ASCII encoding of Salted__, followed by the 8 bytes salt and the actual ciphertext.

    A simplified PHP implementation of this key derivation function that is sufficient here is (since the IV is explicitly specified here with -iv, it is not derived along with the key):

    // from: https://gist.github.com/ezimuel/67fa19030c75052b0dde278a383eda1b
    function EVP_BytesToKey($salt, $password) {
        $bytes = '';
        $last = '';
        
        // 32 bytes key
        while(strlen($bytes) < 32) {
            $last = hash('sha256', $last . $password . $salt, true); // md5 before v1.1.0 
            $bytes.= $last;
        }
        return $bytes;
    }
    

    Extracting the salt and actual ciphertext is:

    $password = "sup3r_s3cr3t_p455w0rd";
    $encrypted = file_get_contents("<path to enc file>");
    $salt = substr($encrypted, 8, 8);
    $key = EVP_BytesToKey($salt, $password);
    $ciphertext = substr($encrypted, 16);
    

    In addition, since the raw data is passed, the corresponding OPENSSL_RAW_DATA flag must be set:

    $iv = hex2bin("504914019097319c9731fc639abaa6ec");
    $decrypted = openssl_decrypt($ciphertext, "aes-256-ctr", $key, OPENSSL_RAW_DATA, $iv);
    

    Note that as of OpenSSL v1.1.0 the default digest is SHA256, before MD5. The digests used in EVP_BytesToKey() must be identical for compatibility. Also be aware that EVP_BytesToKey() is considered insecure nowadays.