Encrypt With PHP, Decrypt With JavaScript (Cryptojs)

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>

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.

Encrypt with CryptoJS and decrypt with PHP

You're not doing the same thing on both sides.

IV

You did parse the IV in CryptoJS, but forgot to do it in PHP:

$iv_dec = pack('H*', "101112131415161718191a1b1c1d1e1f");

To fix that your IV is wrong, you probably noticed that the first 16 bytes are gibberish. That happens when the IV is wrong. Note that CryptoJS uses CBC mode by default, so the IV has only influence on the first block during decryption. Remove this:

$ciphertext_dec = substr($ciphertext_dec, 16);

Padding

You probably noticed that most plaintexts don't come out right. They end with some strange repeated characters at the end. This is the PKCS#7 padding that is applied by default in CryptoJS. You have to remove the padding yourself in PHP. Good thing is that Maarten Bodewes has provided a proper copy paste solution for this here.

trim() might be appropriate for ZeroPadding, but not when a proper padding scheme like the one defined in PKCS#7 is used. You may remove the trim() call altogether, because it is not useful and may result in unexpected plaintext, becauses zero bytes and whitespace is trimmed from the beginning and end.

Decrypt Crypto-js encrypted text with key with PHP

The following solution is not from my side but from @Artjom B., so all credits go to him. You will find the source here: https://stackoverflow.com/a/27678978/8166854.

To your problem: you run the CryptoJs encryption with a passphrase and not with a key. According to the docs (https://cryptojs.gitbook.io/docs/#the-cipher-algorithms) section cipher algorithms the (internal AES) key is derived from the passphrase with an outdated and unsecure function that should no longer be in use.

Artjom B. was able to make this key derivation available on PHP. As a side note: it is not necessary to present an
initialization vector (IV) to the encryption function as the IV is as well derived from the passphrase, so I'm leaving it
out in the following code.

This is the result on PHP-side:

solution for https://stackoverflow.com/questions/65234428/decrypt-crypto-js-encrypted-text-with-key-with-php
string(3) "123"
decryptedtext: 123

This is the code, please obey the warning:
This code is provided for achieve compatibility between different programming languages. It is not necessarily fully secure. Its security depends on the complexity and length of the password, because of only one iteration and the use of MD5. I would recommend to use at least a 20 character password with alphanumeric characters which is ideally randomly generated.

<?php

/*
source: https://stackoverflow.com/a/27678978/8166854 author: Artjom B.
Security notice: This code is provided for achieve compatibility between different programming languages.
It is not necessarily fully secure. Its security depends on the complexity and length of the password,
because of only one iteration and the use of MD5. I would recommend to use at least a 20 character password
with alphanumeric characters which is ideally randomly generated.
*/

function evpKDF($password, $salt, $keySize = 8, $ivSize = 4, $iterations = 1, $hashAlgorithm = "md5") {
$targetKeySize = $keySize + $ivSize;
$derivedBytes = "";
$numberOfDerivedWords = 0;
$block = NULL;
$hasher = hash_init($hashAlgorithm);
while ($numberOfDerivedWords < $targetKeySize) {
if ($block != NULL) {
hash_update($hasher, $block);
}
hash_update($hasher, $password);
hash_update($hasher, $salt);
$block = hash_final($hasher, TRUE);
$hasher = hash_init($hashAlgorithm);
// Iterations
for ($i = 1; $i < $iterations; $i++) {
hash_update($hasher, $block);
$block = hash_final($hasher, TRUE);
$hasher = hash_init($hashAlgorithm);
}
$derivedBytes .= substr($block, 0, min(strlen($block), ($targetKeySize - $numberOfDerivedWords) * 4));
$numberOfDerivedWords += strlen($block)/4;
}
return array(
"key" => substr($derivedBytes, 0, $keySize * 4),
"iv" => substr($derivedBytes, $keySize * 4, $ivSize * 4)
);
}

function decrypt($ciphertext, $password) {
$ciphertext = base64_decode($ciphertext);
if (substr($ciphertext, 0, 8) != "Salted__") {
return false;
}
$salt = substr($ciphertext, 8, 8);
$keyAndIV = evpKDF($password, $salt);
$decryptPassword = openssl_decrypt(
substr($ciphertext, 16),
"aes-256-cbc",
$keyAndIV["key"],
OPENSSL_RAW_DATA, // base64 was already decoded
$keyAndIV["iv"]);
return $decryptPassword;
}

echo 'solution for https://stackoverflow.com/questions/65234428/decrypt-crypto-js-encrypted-text-with-key-with-php' . PHP_EOL;
$key = "Secret Passphrase";
$strg = "U2FsdGVkX1+EaW3J1GE1k/EU5h6C+nxBH364Xhez+b0=";
$rawText = decrypt($strg, $key);
var_dump($rawText);
echo 'decryptedtext: ' . $rawText . PHP_EOL;
?>

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>

AES Encryption in PHP and Decryption in Javascript

The following steps must be implemented in the CryptoJS code:

  • Separate IV, HMAC and ciphertext (after Base64 decoding)
  • Calculate the HMAC for the ciphertext
  • Check the authenticity of the ciphertext. The ciphertext is authentic if the received and calculated HMAC are identical.
  • Perform decryption, only if the ciphertext is authentic

The following code is a possible implementation. As key 0123456789012345 was applied and with the PHP code the used ciphertext was generated:

var ciphertext = 'WqfMfCxKg7U7h5S1mbx7mSHOkkkIrUUpg++mX4ZdWt0I26VfKn7bsi60Oo/SIsWQGyC4dF5z081NvjTXwZGjIpguA0k/QqIM/GDEpCojaro=';
var key = '0123456789012345';

// Convert key and ciphertext into WordArrays
var ciphertextWA = CryptoJS.enc.Base64.parse(ciphertext);
var keyWA = CryptoJS.enc.Utf8.parse(key);

// Separate IV, HMAC and ciphertext
var ivWA = CryptoJS.lib.WordArray.create(ciphertextWA.words.slice(0, 4));
var hmacWA = CryptoJS.lib.WordArray.create(ciphertextWA.words.slice(4, 4 + 8));
var actualCiphertextWA = CryptoJS.lib.WordArray.create(ciphertextWA.words.slice(4 + 8));

// Authenticate
var hmacCalculatedWA = CryptoJS.HmacSHA256(actualCiphertextWA, keyWA);
if(CryptoJS.enc.Base64.stringify(hmacCalculatedWA) === CryptoJS.enc.Base64.stringify(hmacWA)) {

// Decrypt if authentication is successfull
var decryptedMessageWA = CryptoJS.AES.decrypt({ciphertext: actualCiphertextWA}, keyWA, {iv: ivWA});
var decryptedMessage = CryptoJS.enc.Utf8.stringify(decryptedMessageWA);
console.log(decryptedMessage);
} else {
console.log('Authentication failed!');
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

How to convert AES Decrypt from Javascript to php

As mentioned in the other answer, in the context of AES, CryptoJS can process both, a key (16, 24 or 32 bytes) or a password. This depends on the type of the second parameter in CryptoJS.AES.encrypt()/decrypt(). A WordArray is interpreted as a key, a string as a password.

In the current case a string is passed, which is thus processed as a password. CryptoJS generates an 8 bytes salt during encryption and derives a 32 bytes key and a 16 bytes IV from the salt and the password. The key derivation function is the OpenSSL function EVP_BytesToKey(), which additionally requires a digest and an iteration count and for which CryptoJS uses the values MD5 and 1.

A possible implementation of EVP_BytesToKey() in PHP for creating a key/IV pair for AES-256/CBC is:

// from: https://gist.github.com/ezimuel/67fa19030c75052b0dde278a383eda1b
function EVP_BytesToKey($salt, $password) {
$bytes = '';
$last = '';

// 32 bytes key + 16 bytes IV = 48 bytes.
while(strlen($bytes) < 48) {
$last = hash('md5', $last . $password . $salt, true);
$bytes.= $last;
}
return $bytes;
}

CryptoJS uses the OpenSSL format for the ciphertext, which consists of the ASCII encoding of Salted__ followed by the 8 bytes salt and the actual ciphertext. The posted ciphertext is the Base64 encoding of this value. During decryption, the salt and actual ciphertext must be separated:

// Separate salt and ciphertext
$dataB64 = 'U2FsdGVkX1+S8UNrljj2STY8bBrYmr1qUbD2GYuJgIja1rzXY2y4BBkTf9GQxUGNyfRxP/BxiGIU7EFjnA2nTrM06ySr9bJySTjDDTqlDnY=';
$data = base64_decode($dataB64);
$salt = substr($data, 8, 8);
$ciphertext = substr($data, 16);

Now the key and IV can be determined as follows:

// Derive key and iv
$passphrase = '87434313.47913419';
$keyiv = EVP_BytesToKey($salt, $passphrase);
$key = substr($keyiv, 0, 32); // hex encoded: e8db19b984ed9196fff1ce9150b73eafc4cb13abe69e6dcc1ea1528dd88982ff
$iv = substr($keyiv, 32, 16); // hex encoded: 47e26ab2bf3b66eda871d4929cc91029

and the actual ciphertext can be decrypted, e.g. with phpseclib:

use phpseclib\Crypt\AES;

$cipher = new AES('cbc');
$cipher->setKey($key);
$cipher->setIV($iv);
$plaintext = $cipher->decrypt($ciphertext);
echo json_decode($plaintext); // https://xcdn-209.bato.to/7002/32e/60af5c1a22f39459ededde23/

Since CryptoJS applies the OpenSSL format, the ciphertext is compatible with OpenSSL and can also be decrypted as follows:

openssl enc -d -aes-256-cbc -p -pass pass:87434313.47913419 -md md5 -A -a -in <file containing the U2FsdGVk...>

Note that EVP_BytesToKey() is deemed insecure. The more secure way is to use a reliable key derivation function like Argon2 or PBKDF2 (the latter is also supported by CryptoJS) to derive key and IV and perform encryption with these values.



Related Topics



Leave a reply



Submit