Search code examples
javascriptphpcryptographyaes

Crypto-js local side to php server side


I have an app that use crypto-Js AES. The simulate working code is:

var ciphertext = CryptoJS.AES.encrypt('My_message', 'My_secret_key');
console.log(ciphertext.toString()); 

the answer is:

U2FsdGVkX1/Dd3uAr/mdw5lVoBvq0UX5LHnNoX24JAM=

when I try to reproduce it server side I never get the same answer:

$passphrase='My_secret_key';
$value='My_message';
$salt = openssl_random_pseudo_bytes(8);
$salt ='';
$salted = '';
$dx = '';
while (strlen($salted) < 48) {
 $dx = md5($dx.$passphrase.$salt, true);
 $salted .= $dx;
}
$key = substr($salted, 0, 32);
$iv  = substr($salted, 32,16);
$encrypted_data = openssl_encrypt($value, 'aes-256-cbc', $key, true, $iv);
echo base64_encode($encrypted_data);

server side answer:

3jSTl1yR55lfTbz7f0o3Yw==

I must miss something but can't point out what. Local side can't touched. All help is welcome


Solution

  • If the second parameter in CryptoJS.AES.encrypt is passed as string, it is interpreted as a passphrase from which the actual key and IV are derived, [1]. This is achieved by using the functionality of the OpenSSL-function EVP_BytesToKey with an iteration count of 1 and the MD5-digest, [2] [3] (note that CryptoJS doesn't consider the switch of the default digest from MD5 to SHA256 from OpenSSL version 1.1.0c on, [4]).

    CryptoJS.AES.encrypt returns a CipherParams-object that encapsulates ciphertext, key, IV, and salt, [5]. In addition CipherParams#toString() returns the result in OpenSSL-format as Base64-encoded string. The OpenSSL-format consists of a 16-byte header and the subsequent ciphertext. The header starts with the ASCII-encoded string Salted__ followed by an 8-byte salt. The salt is randomly generated each time and used together with the password to derive the key / IV. This creates a different key / IV each time.

    The PHP-code is functionally identical: Key and IV are derived with an analog logic from a passphrase using a freshly generated salt each time (for the proof, see below). However, some minor changes are necessary:

    • The following line must be removed: $salt ='';

    • In the current code, only the Base64-encoded ciphertext is displayed. For a Base64-encoded output of the result in OpenSSL-format the code must be instead:

      echo base64_encode('Salted__'.$salt.$encrypted_data); 
      
    • The 4th parameter in openssl_encrypt should be changed from true to OPENSSL_RAW_DATA. Both are functionally identical, but the use of OPENSSL_RAW_DATA is more transparent.

    The JavaScript- and PHP-code generate a new salt each time and thus a different key and IV, which changes the ciphertext each time. That's the way it should be. Since the salt is stored together with the ciphertext, it is possible to decrypt the ciphertext at any time using the passphrase.

    Proof that both codes use the same logic to derive key and IV: The new salt / ciphertext generated each time prevents a direct comparison of the results of both codes. In order to perform this comparison without much effort, it is best to use the salt generated in the JavaScript-code in the PHP-code as well. The salt in the JavaScript-code can be determined as hexadecimal string with:

    console.log(ciphertext.salt.toString(CryptoJS.enc.Hex)); 
    

    This salt is to be used in the PHP-code instead of the randomly generated salt (of course only for this one comparison):

    $salt = hex2bin('<Salt from JavaScript-Code as hexadecimal string>'); 
    

    A comparison of both outputs now proves that they are equal, showing that both codes are functionally identical.