Duplicate php openssl_encrypt in JavaScript - javascript

I'm trying to duplicate PHP string encryption using JavaScript. Here is the PHP code:
<?php
$iv = "1234567890123456";
$key = "aaaaaaaaaaaaaaaa";
$input = "texttexttexttext";
$encrypted = openssl_encrypt($input, "AES-256-CBC", $key, 0, $iv);
echo $encrypted;
// "ZwY1i+vqP3acszeDiscCTx/R4a6d2AtkcInmN9OTCNE="
However, when I try to duplicate it in JavaScript it gives a different ciphertext:
var aesjs = require("aes-js");
var base64 = require("js-base64");
var iv = aesjs.utils.utf8.toBytes("1234567890123456");
var key = aesjs.utils.utf8.toBytes("aaaaaaaaaaaaaaaa");
var text = aesjs.utils.utf8.toBytes("texttexttexttext");
var aesCbc = new aesjs.ModeOfOperation.cbc(key, iv);
var encryptedBytes = aesCbc.encrypt(text);
var b64encoded = base64.Base64.encode(encryptedBytes);
console.log(b64encoded);
// "MTcyLDIsNjAsMTU5LDcxLDEwLDE4Myw4LDE…wyMTIsMjIyLDk3LDEyNCw1MywxNzIsMjIy"
I have no clue on how to make it give the same output. Any ideas?

Some things are going wrong:
First, the output from the JavaScript code is actually the base64 encoding of the string 172,2,60,159,71,10,183,8,1,…, not the encoding of the raw byte-buffer. I do not really know how to fix this idiomatically, but by using the aes.js hex-encoding utility function, we can convert it to base64:
var hex = aesjs.utils.hex.fromBytes(encryptedBytes);
var buf = Buffer.from(hex, 'hex');
console.log(buf.toString('base64'));
// rAI8n0cKtwiu1N5hfDWs3g==
The second problem is that in aes.js you are using AES128 encryption (aaaaaaaaaaaaaaaa is 128 bits long), but you are using AES256 encryption in the PHP code. We should update the PHP code (or the JS code):
$encrypted = openssl_encrypt($input, "AES-128-CBC", $key, 0, $iv);
echo $encrypted;
// rAI8n0cKtwiu1N5hfDWs3rPbz0UmvlbW+LJliYox03c=
We almost have the same output. But wait, the PHP output is twice as long. What happened?
Well, OpenSSL uses PKCS#7 padding. The Javascript code is unpadded however. To fix this, you should use PKCS#7 padding for the javascript text. For this you can just use the pkcs7 module. Another option is to use AES in counter (CTR) mode instead of CBC mode, if this is an option for you.
This is the PHP code that I have in the end:
<?php
$iv = "1234567890123456";
$key = "aaaaaaaaaaaaaaaa";
$input = "texttexttexttext";
$encrypted = openssl_encrypt($input, "AES-128-CBC", $key, 0, $iv);
echo $encrypted;
// output: 'rAI8n0cKtwiu1N5hfDWs3rPbz0UmvlbW+LJliYox03c='
And this is the JavaScript code:
var aesjs = require("aes-js");
var base64 = require("js-base64");
var pkcs7 = require("pkcs7");
var iv = aesjs.utils.utf8.toBytes("1234567890123456");
var key = aesjs.utils.utf8.toBytes("aaaaaaaaaaaaaaaa");
var text = aesjs.utils.utf8.toBytes("texttexttexttext");
var aesCbc = new aesjs.ModeOfOperation.cbc(key, iv);
var encryptedBytes = aesCbc.encrypt(pkcs7.pad(text));
var hex = aesjs.utils.hex.fromBytes(encryptedBytes);
var buf = Buffer.from(hex, 'hex');
console.log(buf.toString('base64'));
// output: 'rAI8n0cKtwiu1N5hfDWs3rPbz0UmvlbW+LJliYox03c='
PS I personally prefer using CTR mode, because PKCS#7 implementations sometimes expose padding oracles which break the encryption. (I checked the mentioned pkcs#7 library which should be good, but please don't try to implement this yourself.)

Related

How to decrypt AES256 data which was encrypted on PHP and get value in Javascript?

I have encrypted some value using aes-256-cbc mode on PHP like this:
public function encrypt(string $data): string
{
$iv = $this->getIv();
$encryptedRaw = openssl_encrypt(
$data,
$this->cryptMethod, //aes-256-cbc
$this->key,
OPENSSL_RAW_DATA,
$iv
);
$hash = hash_hmac('sha256', $encryptedRaw, $this->key, true);
return base64_encode( $iv . $hash . $encryptedRaw );
}
Then I tried to decrypt it on PHP and it works fine:
public function decrypt(string $data): string
{
$decoded = base64_decode($data);
$ivLength = openssl_cipher_iv_length($this->cryptMethod);
$iv = substr($decoded, 0, $ivLength);
$hmac = substr($decoded, $ivLength, $shaLength = 32);
$decryptedRaw = substr($decoded, $ivLength + $shaLength);
$originalData = openssl_decrypt(
$decryptedRaw,
$this->cryptMethod,
$this->key,
OPENSSL_RAW_DATA,
$iv
);
So I'm new to JavaScript and I don't know how to realize the same decrypt method as on php.
Example of encrypted string and it's key:
encrypted string lUIMFpajICh/e44Mwkr0q9xdyJh5Q8zEJHi8etax5BRl78Vsyh+wDknmBga1L8p8SDZA6WKz1CvAAREFGreRAQ== secret key - 9SJ6O6IwmItSRICbXgdJ
Example what I found returns empty string:
const decodedString = base64.decode(
`lUIMFpajICh/e44Mwkr0q9xdyJh5Q8zEJHi8etax5BRl78Vsyh+wDknmBga1L8p8SDZA6WKz1CvAAREFGreRAQ==`
);
const CryptoJS = require("crypto-js");
var key = CryptoJS.enc.Latin1.parse("9SJ6O6IwmItSRICbXgdJ");
var iv = CryptoJS.enc.Latin1.parse(decodedString.slice(0, 16));
var ctx = CryptoJS.enc.Base64.parse(
"lUIMFpajICh/e44Mwkr0q9xdyJh5Q8zEJHi8etax5BRl78Vsyh+wDknmBga1L8p8SDZA6WKz1CvAAREFGreRAQ=="
);
var enc = CryptoJS.lib.CipherParams.create({ ciphertext: ctx });
console.log(
CryptoJS.AES.decrypt(enc, key, { iv: iv }).toString(CryptoJS.enc.Utf8)
);
}
What I did wrong?
The key used in the PHP code is only 20 bytes in size and thus too small for AES-256 (AES-256 requires a 32 bytes key). PHP/OpenSSL implicitly pads the key with 0x00 values to the required key length. In the CryptoJS code, this must be done explicitly.
Furthermore, in the CryptoJS code, IV (the first 16 bytes), HMAC (the following 32 bytes) and ciphertext (the rest) are not separated correctly.
Also, the authentication is missing. To do this, the HMAC for the ciphertext must be determined using the key and compared with the HMAC sent. Decryption only takes place if authentication is successful.
If all of this is taken into account, the posted code can be fixed e.g. as follows:
var key = CryptoJS.enc.Utf8.parse("9SJ6O6IwmItSRICbXgdJ".padEnd(32, "\0")); // pad key
var ivMacCiphertext = CryptoJS.enc.Base64.parse("lUIMFpajICh/e44Mwkr0q9xdyJh5Q8zEJHi8etax5BRl78Vsyh+wDknmBga1L8p8SDZA6WKz1CvAAREFGreRAQ==")
var iv = CryptoJS.lib.WordArray.create(ivMacCiphertext.words.slice(0, 4)); // get IV
var hmac = CryptoJS.lib.WordArray.create(ivMacCiphertext.words.slice(4, 4 + 8)); // get HMAC
var ct = CryptoJS.lib.WordArray.create(ivMacCiphertext.words.slice(4 + 8)); // get Ciphertext
var hmacCalc = CryptoJS.HmacSHA256(ct, key);
if (hmac.toString() === hmacCalc.toString()) { // authenticate
var dt = CryptoJS.AES.decrypt({ciphertext: ct}, key, { iv: iv }).toString(CryptoJS.enc.Utf8); // decrypt
console.log(dt);
} else {
console.log("Decryption failed");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
A few thoughts for you:
Check that your encoding/decoding is working properly. For each stage
of the process, endode/decode, then console log the output and
compare input to output, and also between PHP and javascript.
CBC mode uses padding to fill out the blocks. Check that both stacks
are using the same padding type.
Rather than using CBC and a separate HMAC, how about jumping to AEAD (like AES
GCM) which avoids the padding issue, and also incorporates the MAC
into the encryption, so is a more simple interface?

PHP decryption from JS breaks on longer values

When I'm trying to encrypt and decrypt a short word with several characters the following code does work. However if I try that with: gmdv4hwHN7SrHwEhX0Sb6dskWkVezAUlmjTvHOV6QSAySI8pOrOsPrQoYVQpWP7j It's like there's a max amount of characters. How do I resolve this issue?
JS
var CryptoJS = require("crypto-js")
var message = 'gmdv4hwHN7SrHwEhX0Sb6dskWkVezAUlmjTvHOV6QSAySI8pOrOsPrQoYVQpWP7j';
var key = '59b6ab45d379b89d794c87b74a511';
var iv = '0aaff094b6bc297a';
var encrypted = CryptoJS.AES.encrypt(
message,
CryptoJS.enc.Hex.parse(key),
{ iv: CryptoJS.enc.Hex.parse(iv) }
).toString()
Output: rQAT7R3TJUU+iLIzY+MNpoer7/br60oQrxo2O7BmI0b/O668bT7L5/cJUQbAFFDcwX4+8g3kjem6pCMGU7u9srVM7yauPmw8lcW9IiWSrbg=
PHP
<?php
$ciphertext = "rQAT7R3TJUU+iLIzY+MNpoer7/br60oQrxo2O7BmI0b/O668bT7L5/cJUQbAFFDcwX4+8g3kjem6pCMGU7u9srVM7yauPmw8lcW9IiWSrbg=";
$key = '59b6ab45d379b89d794c87b74a511';
$iv = '0aaff094b6bc297a';
var_dump (\openssl_decrypt(
base64_decode($ciphertext),
'aes-256-cbc',
$key,
OPENSSL_RAW_DATA,
$iv
));
Output: bool(false)

I tried crypto-js but the output is not correct, please see my code and correct me where I am wrong

I have a simple_crypt function in my backend which is working properly, now what I want is to make a similar function for javascript which for exactly the same as the php one.
So I have researched and got the CryptoJS library, my 'Key' and 'iv' values are correct as compared to the PHP one but when I encrypt my string the output is totally different.
This is my working PHP code and I want to convert this into javascript.
<?php
function simple_crypt( $string ) {
$secret_key = '1234567890';
$secret_iv = '0987654321';
$output = false;
$encrypt_method = "AES-256-CBC";
$key = hash( 'sha256', $secret_key );
$iv = substr( hash( 'sha256', $secret_iv ), 0, 16 );
echo "Key : ".$key."<br>";
echo "iv : ".$iv."<br>";
$output = openssl_encrypt( $string, $encrypt_method, $key, 0, $iv );
return $output;
}
$e = simple_crypt("text");
echo $e;
echo "<br>";
?>
This is my JS code in which I am getting the issue, please have a look and tell me where I am wrong in this js code.
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js" integrity="sha512-nOQuvD9nKirvxDdvQ9OMqe2dgapbPB7vYAMrzJihw5m+aNcf0dX53m6YxM4LgA9u8e9eg9QX+/+mPu8kCNpV2A==" crossorigin="anonymous"></script>
<script type="text/javascript">
function simple_crypt(string) {
var secret_key, secret_iv, output, key, iv;
secret_key = '1234567890';
secret_iv = '0987654321';
output = false;
key = CryptoJS.SHA256(secret_key).toString();
iv = CryptoJS.SHA256(secret_iv).toString().substr(0, 16);
console.log("key",key);
console.log("iv",iv);
var encrypted = CryptoJS.AES.encrypt(string, key, {iv: iv});
return (encrypted.toString());
}
console.log(simple_crypt("text"));
</script>
Here is the output:
PHP: T4F65n4AVlmkkb5LLFhRIQ==
JS: U2FsdGVkX18HJGpPYZPm6crBcxA7TfbZZ9Sc/4qHGBk=
So that both codes produces the same result, the key and IV in the NodeJS Code must be the same as in the PHP code and passed as WordArrays. For this, the key and IV you have generated must be further processed as follows:
key = CryptoJS.enc.Utf8.parse(key.substr(0, 32));
iv = CryptoJS.enc.Utf8.parse(iv);
In the PHP code, the SHA256 hash is returned as hex string. With hex encoding the number of bytes doubles, i.e. a SHA256 hash is hex encoded 64 bytes. PHP implicitly considers only the first 32 bytes regarding the key for AES-256, i.e. ignores the last 32 bytes. In the CryptoJS code this must happen explicitly (for the IV this happens, but for the key this is missing).
By parsing with the UTF8 encoder, key and IV are converted into WordArrays. If the key is passed as a string (as in the code posted in the question), then CryptoJS interprets the value as a password and uses a key derivation function to derive key and IV (which is incompatible with the logic in the PHP code).
With the above changes, the CryptoJS code gives the same result as the PHP code:
function simple_crypt(string) {
var secret_key, secret_iv, output, key, iv;
secret_key = '1234567890';
secret_iv = '0987654321';
output = false;
key = CryptoJS.SHA256(secret_key).toString();
iv = CryptoJS.SHA256(secret_iv).toString().substr(0, 16);
key = CryptoJS.enc.Utf8.parse(key.substr(0, 32));
iv = CryptoJS.enc.Utf8.parse(iv);
console.log("key",key.toString());
console.log("iv",iv.toString());
var encrypted = CryptoJS.AES.encrypt(string, key, {iv: iv});
return (encrypted.toString());
}
console.log(simple_crypt("text")); // T4F65n4AVlmkkb5LLFhRIQ==
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
Please note the following:
Using SHA256 to derive the key from a password is insecure. For this purpose, a reliable key derivation function such as PBKDF2 should be used.
For security reasons, a key/IV pair may only be applied once. Therefore, the IV is usually randomly generated for each encryption. The IV is not a secret and is commonly sent to the recipient along with the ciphertext (usually prepended). Alternatively, the IV can be derived together with the key using a KDF (in combination with a randomly generated salt).

AES 256 on the client side (JS) and in the server (PHP)

I'm trying to encrypt and decrypt data on the server side and the client using the same type of operation, which is AES-256.
On the server I use PHP and client I use CryptoJS so far I could only encrypt and decrypt the client on the server, see the code:
JS
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/pbkdf2.js"></script>
<script>
var salt = CryptoJS.lib.WordArray.random(128/8);
var key256Bits500Iterations = CryptoJS.PBKDF2("Secret Passphrase", salt, { keySize: 256/32, iterations: 500 });
var iv = CryptoJS.enc.Hex.parse('101112131415161718191a1b1c1d1e1f');
var encrypted = CryptoJS.AES.encrypt("Message", key256Bits500Iterations, { iv: iv });
var data_base64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
var iv_base64 = encrypted.iv.toString(CryptoJS.enc.Base64);
var key_base64 = encrypted.key.toString(CryptoJS.enc.Base64);
</script>
PHP
<?php
$encrypted = base64_decode("data_base64"); // data_base64 from JS
$iv = base64_decode("iv_base64"); // iv_base64 from JS
$key = base64_decode("key_base64"); // key_base64 from JS
$plaintext = rtrim( mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $key, $encrypted, MCRYPT_MODE_CBC, $iv ), "\t\0 " );
How can I encrypt and decrypt data on both sides (client and server) so that communicate in the same language using PHP and CryptoJS?
Your code looks fine apart from a padding mismatch. CryptoJS uses PKCS#5/PKCS#7 padding by default whereas MCrypt only supports ZeroPadding.
If you're only sending textual plaintexts, then you can safely use
CryptoJS.AES.encrypt("Message", key, { iv: iv, padding: CryptoJS.pad.ZeroPadding });
If not, then you should use proper pkcs7unpad in PHP:
$plaintext = pkcs7unpad( mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $key, $encrypted, MCRYPT_MODE_CBC, $iv ), 16 );
Other problems with your code are that you directly use CryptoJS.AES.encrypt(...).toString(). This will create an OpenSSL formatted string which is not purely the ciphertext. You need to use
CryptoJS.AES.encrypt(...).ciphertext.toString(CryptoJS.enc.Base64);
to also be sure about the encoding.
Right now, this is only obfuscation, since you're sending the key along with the ciphertext. I suspect that you want to derive the key in PHP too. If yes, then you will only need to send the random salt along with the ciphertext under the assumption that the server knows the password.
PHP provides a PBKDF2 implementation from version 5.5 onwards.
Full JavaScript part without PBKDF2 involvement:
var message = 'My string - Could also be an JS array/object';
var iv = 'a1a2a3a4a5a6a7a8b1b2b3b4b5b6b7b8';
var key = 'c1c2c3c4c5c6c7c8d1d2d3d4d5d6d7d8c1c2c3c4c5c6c7c8d1d2d3d4d5d6d7d8'; // 256-bit hex encoded
var keyBytes = CryptoJS.enc.Hex.parse(key);
var ivBytes = CryptoJS.enc.Hex.parse(iv);
var encrypt = CryptoJS.AES.encrypt(message, keyBytes, {
iv: ivBytes,
padding: CryptoJS.pad.ZeroPadding
}).ciphertext.toString(CryptoJS.enc.Base64);
produces:
j86KHBVRsDGKUnOiYdkEotsFL/lY/1tzz/h3Ay+vlEX11fC055m7vaF6q7w13eUj
Full PHP part without PBKDF2 involvement:
<?php
$iv = 'a1a2a3a4a5a6a7a8b1b2b3b4b5b6b7b8';
$key = 'c1c2c3c4c5c6c7c8d1d2d3d4d5d6d7d8c1c2c3c4c5c6c7c8d1d2d3d4d5d6d7d8';
$ct = 'j86KHBVRsDGKUnOiYdkEotsFL/lY/1tzz/h3Ay+vlEX11fC055m7vaF6q7w13eUj';
$ivBytes = hex2bin($iv);
$keyBytes = hex2bin($key);
$ctBytes = base64_decode($ct);
$decrypt = rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $keyBytes, $ctBytes, MCRYPT_MODE_CBC, $ivBytes));
echo $decrypt;
produces:
My string - Could also be an JS array/object
The same is possible with the OpenSSL extension:
<?php
$iv = 'a1a2a3a4a5a6a7a8b1b2b3b4b5b6b7b8';
$key = 'c1c2c3c4c5c6c7c8d1d2d3d4d5d6d7d8c1c2c3c4c5c6c7c8d1d2d3d4d5d6d7d8';
$ct = 'j86KHBVRsDGKUnOiYdkEotsFL/lY/1tzz/h3Ay+vlEX11fC055m7vaF6q7w13eUj';
$ivBytes = hex2bin($iv);
$keyBytes = hex2bin($key);
$ctBytes = base64_decode($ct);
$decrypt = openssl_decrypt($ctBytes, "aes-256-cbc", $keyBytes, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $ivBytes);
echo($decrypt);

Decrypting PHP aes encryption in javascript

I've been programming for years now, but never had to do anything with encryption/decryption.
So I've got the following PHP:
function base64_url_decode($input) {
return base64_decode(strtr($input, '-_,', '+/='));
}
function AESdecrypt($phrase, $user_key){
$key = pack('H*', $user_key);
$ciphertext_dec = base64_url_decode($phrase);
# may remove 00h valued characters from end of plain text
$plaintext_dec = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key,
$ciphertext_dec, MCRYPT_MODE_ECB);
return $plaintext_dec;
}
echo AESdecrypt(
"-GZmiQhJYnzw0FXTR6QoLryPNlgcScbfgZnmzgA35tydozNwsKWdXvIEtqWjhJIGCYdVVKB0lBKUTx-TXxHnIQn680mZIZ8lG7HNTMgprxM,",
"2dd9bb29d2e25c18bdc12d7b75f6f5d0ef3d99ef310a0319e2796bb30278b24c557f78b6c958faa55d70ce081f2607a0e62b9fa01e2483f9a75b032b7fd9678c"
);
The output of this is
1kYJjUajo8bIlsT5CVCSqYglD_dQX-fjuIHkEFzfJouiI7Nx29IEtZ8QwTvIH6yx5uI,
I've created a JS fiddle to simulate what I have in javascript: http://jsfiddle.net/NdT3P/3/ (please scroll down to line 545 since I've pasted in some libraries I was using).
Relevant Javascript code:
var user_key = "2dd9bb29d2e25c18bdc12d7b75f6f5d0ef3d99ef310a0319e2796bb30278b24c557f78b6c958faa55d70ce081f2607a0e62b9fa01e2483f9a75b032b7fd9678c";
user_key = pack('H*', user_key);
console.log(user_key); // same as the php functions
var decrypted_wallet = "-GZmiQhJYnzw0FXTR6QoLryPNlgcScbfgZnmzgA35tydozNwsKWdXvIEtqWjhJIGCYdVVKB0lBKUTx-TXxHnIQn680mZIZ8lG7HNTMgprxM,"
decrypted_wallet = Base64.decode(strtr(decrypted_wallet, '-_,', '+/='));
console.log(decrypted_wallet); //same as the php function
var decrypted = CryptoJS.AES.decrypt(user_key,decrypted_wallet,{ mode: CryptoJS.mode.ECB });
console.log(decrypted.toString(CryptoJS.enc.Utf8)); // empty string
Anyone could point in the right direction?

Categories

Resources