Encrypt in PHP Openssl and Decrypt in JavaScript Cryptojs

OpenSSL Encrypt-Decrypt with CryptoJS

If the PHP code is executed with the plaintext $string = "test"; then the encryption part provides:

MWNjdVlVL1hBWGN2UFlpMG9yMGZBUT09

Regarding the PHP code the following should be noted:

  1. The encryption process Base64 encodes twice. Once explicitly with base64_encode, once implicitly by default (s. openssl_encrypt, 4th parameter). This redundancy is not necessary, i.e. one of the two Base64 encodings should be removed. Analogous for decryption.
  2. The hash function returns by default the data hex encoded in lower case. Thus $key is 64 bytes in size. For AES-256 OpenSSL implicitly uses only the first 32 bytes.

The CryptoJS code you posted could be modified as follows to implement this functionality (JavaScript):

var Sha256 = CryptoJS.SHA256;
var Hex = CryptoJS.enc.Hex;
var Utf8 = CryptoJS.enc.Utf8;
var Base64 = CryptoJS.enc.Base64;
var AES = CryptoJS.AES;

var secret_key = 'thisIsK3y';
var secret_iv = 'tHis1s1v';

var key = Sha256(secret_key).toString(Hex).substr(0,32); // Use the first 32 bytes (see 2.)
var iv = Sha256(secret_iv).toString(Hex).substr(0,16);

// Encryption
var output = AES.encrypt("test", Utf8.parse(key), {
iv: Utf8.parse(iv),
}).toString(); // First Base64 encoding, by default (see 1.)

var output2ndB64 = Utf8.parse(output).toString(Base64); // Second Base64 encoding (see 1.)
console.log(output2ndB64); // MWNjdVlVL1hBWGN2UFlpMG9yMGZBUT09

// Decryption
var decrypted = AES.decrypt(output, Utf8.parse(key), {
iv: Utf8.parse(iv),
}).toString(Utf8);
console.log(decrypted); // test
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

CryptoJS and openssl_decrypt not produce same result

Both codes differ in two ways:

  • The PHP code applies AES-256, but since only a 16 bytes key is used (because of the hex decoding), PHP automatically pads it with 0 values to a length of 32 bytes. In the CryptoJS code, the key length determines the mode, thus AES-128 is applied. So that both codes produce the same result, the key must be extended in the CryptoJS code analogously to the PHP code, or AES-128 must be used in the PHP code.
  • In the PHP code, openssl_encrypt() returns the ciphertext Base64 encoded by default, so the ciphertext is currently Base64 encoded twice. Therefore remove the explicit base64_encode() or use OPENSSL_RAW_DATA as the 4th parameter so that the raw data is returned. Similarly for openssl_decrypt().

When these issues are fixed, both codes provide the same ciphertext on my machine. Note that a static IV is insecure (see also the comment), but you probably only do this for testing purposes.


Example: The following code uses your unmodified CryptoJS code, i.e. AES-128:

function aes_encrypt(str_to_encrypt){
if(str_to_encrypt==null)
return "";

var key = CryptoJS.enc.Hex.parse("0123456789abcdef0123456789abcdef");
var iv = CryptoJS.enc.Hex.parse("abcdef9876543210abcdef9876543210");

var encrypted = CryptoJS.AES.encrypt(str_to_encrypt,key, {'mode': CryptoJS.mode.CBC, iv: iv});
var encryptedString = encrypted.toString();
return encryptedString;
}

function aes_decrypt(str_to_decrypt){
if(str_to_decrypt==null)
return "";

var key = CryptoJS.enc.Hex.parse("0123456789abcdef0123456789abcdef");
var iv = CryptoJS.enc.Hex.parse("abcdef9876543210abcdef9876543210");

var decrypted = CryptoJS.AES.decrypt(str_to_decrypt,key, {'mode': CryptoJS.mode.CBC, iv: iv });
var decryptedString = decrypted.toString(CryptoJS.enc.Utf8);
return decryptedString;
}

var ciphertext = aes_encrypt('The quick brown fox jumps over the lazy dog');
var decrypted = aes_decrypt(ciphertext);
console.log(ciphertext.replace(/(.{56})/g,'$1\n'));
console.log(decrypted);
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

CryptoJs - Encrypt/Decrypt by PHP and Javascript - Simple Output Encrypted String

In the PHP code the following should be considered:

  • $passphrase does not denote a passphrase, but the key. This key must be 32 bytes in size for the choice aes-256-cbc. If it is too short, it is filled with 0 values, if it is too long, it is truncated. This is a common source of error, so a key of exactly 32 bytes should be used. If you want to work with a passphrase, you have to use a KDF (like PBKDF2).
  • In the fourth parameter flags are set, and no boolean expression (like true). If the data should be returned in binary form, the OPENSSL_RAW_DATA flag must be set.
  • Static IVs are insecure, usually a new IV is generated for each encryption, which is sent to the recipient together with the ciphertext. Since the IV is not secret, it is usually placed in front of the ciphertext on byte level without encryption.

The following sample PHP code (based on the posted code):

function myCrypt($value, $key, $iv){
$encrypted_data = openssl_encrypt($value, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($encrypted_data);
}

function myDecrypt($value, $key, $iv){
$value = base64_decode($value);
$data = openssl_decrypt($value, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
return $data;
}

$valTxt="MyText";
$key="01234567890123456789012345678901"; // 32 bytes
$vector="1234567890123412"; // 16 bytes
$encrypted = myCrypt($valTxt, $key, $vector);
$decrypted = myDecrypt($encrypted, $key, $vector);
print($encrypted . "\n");
print($decrypted . "\n");

returns the following result:

1SF+kez1CE5Rci3H6ff8og==
MyText

The corresponding CryptoJS code for decryption is:

var DataEncrypt = "1SF+kez1CE5Rci3H6ff8og==";
var DataKey = CryptoJS.enc.Utf8.parse("01234567890123456789012345678901");
var DataVector = CryptoJS.enc.Utf8.parse("1234567890123412");
var decrypted = CryptoJS.AES.decrypt(DataEncrypt, DataKey, { iv: DataVector });
var decrypted = CryptoJS.enc.Utf8.stringify(decrypted);
console.log(decrypted);
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

decryption works using PHP (openssl) and not working using javascript (cryptojs)

AES is only defined for 16/24/32 bytes keys. You are using a 40 bytes key. PHP implicitly cuts the key to 32 bytes, CryptoJS does not, but processes the key without error message due to a bug (#293), with a wrong result, of course.

Also, the ciphertext must be passed as CipherParams object or Base64 encoded, the IV must be Utf8 encoded, PKCS7 padding should be used, and the decrypted data is a base64 string (and still needs to be Base64 decoded).

The following CryptoJS code decrypts the sample ciphertext:

function decryptData(encrypted, pass) {

let decryptedWA = CryptoJS.AES.decrypt(
encrypted, // Pass ciphertext Base64 encoded (or as CipherParams object)
CryptoJS.enc.Utf8.parse(pass.substring(0, 32)), // Truncate key to 32 bytes
{
iv: CryptoJS.enc.Utf8.parse('h7oehNIHWGNIHxyN'), // UTF8 encode the IV
mode: CryptoJS.mode.CBC, // default
padding: CryptoJS.pad.Pkcs7 // default // Apply PKCS7 padding
}
);

let decryptedB64 = decryptedWA.toString(CryptoJS.enc.Utf8);
let decrypted = CryptoJS.enc.Base64.parse(decryptedB64).toString(CryptoJS.enc.Utf8); // Base64 decode the decrypted data

return decrypted;
}

var ciphertext = "rFWejB1Pj6W3Gh1bheFqDZPMO9POKbhGPOP6eAH9BSk=";
var key = "7f7720b911c2ecbb22637ed7adef41e82d44b6a0";
console.log(decryptData(ciphertext, key));
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

openssl_decrypt PHP to CryptoJS

CryptoJS uses the WordArray type and provides encoders for conversion, e.g. the Hex encoder.

In the PHP code, aes128 is applied, which is an alias for aes-128-cbc and requires an IV. If none is specified, PHP implicitly uses a zero vector, which must be explicitly specified in the CryptoJS code. The CBC mode itself does not need to be explicitly specified, since it is the default.

Moreover the PHP code disables padding, which must be explicitly specified in the CryptoJS code, since PKCS7 padding is the default. Note that OPENSSL_ZERO_PADDING does not enable Zero padding, but disables padding, i.e. the CryptoJS counterpart is NoPadding.

Your CryptoJS code must be changed as follows to be functionally equivalent to the PHP code:

let encData = CryptoJS.enc.Hex.parse("D5F630E93F36C21293012D78E5A384F1");
let key = CryptoJS.enc.Hex.parse("A254FE00A791AA74386E8DEF3712B256");
let iv = CryptoJS.enc.Hex.parse("00000000000000000000000000000000");
let data = CryptoJS.AES.decrypt(
{ciphertext: encData},
key,
{iv: iv, padding: CryptoJS.pad.NoPadding}
).toString(CryptoJS.enc.Hex);
console.log(data); // c704469332aa61804601008a92dc10e5
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

Encrypt with Crypto.JS and decrypt with PHP 7.3

Quoting this php.net comment:

Also, MCRYPT_RIJNDAEL_256 is not AES-256, it's a different variant of the Rijndael block cipher. If you want AES-256 in mcrypt, you have to use MCRYPT_RIJNDAEL_128 with a 32-byte key. OpenSSL makes it more obvious which mode you are using (i.e. 'aes-128-cbc' vs 'aes-256-ctr').

This means that you've been using AES-256 before, and not AES-128.

Furthermore, CryptoJS uses CBC mode by default, as correctly noted by @Topaco.

Putting this together:

$result = openssl_decrypt($data, 'aes-256-cbc', $key, $options=OPENSSL_RAW_DATA, $iv);

should give the same result, as your previous mcrypt_decrypt solution.



Related Topics



Leave a reply



Submit