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 choiceaes-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, theOPENSSL_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
JavaScript Does Not Fire After Appending
Difference Between Properties and Attributes in Html
Frame Buster Buster ... Buster Code Needed
Using an HTML Button to Call a JavaScript Function
How to Implement "Select All" Check Box in Html
Submitting a Form on 'Enter' With Jquery
Why Does This Simple Jsfiddle Not Work
Referenceerror: Event Is Not Defined Error in Firefox
How to Add/Remove Several Classes in One Single Instruction With Classlist
What Are Cookies and Sessions, and How Do They Relate to Each Other
Set Cookie and Get Cookie With JavaScript
How to Launch HTML Using Chrome At "--Allow-File-Access-From-Files" Mode
Get File Size, Image Width and Height Before Upload
How to Get Js Variable to Retain Value After Page Refresh
Way to Ng-Repeat Defined Number of Times Instead of Repeating Over Array
Difference Between ≪Script Src="Foo.Js"≫≪/Script≫ and ≪Script Src="Foo.Js"/≫