Hi there StackOverflow community,
After researching for countless of hours, I'm unable to find an explanation as to why my ouputs differ between javascript and my laravel application.
I could use input type hidden to make a post from my web browser, but that would defeat the purpose of having a secure client side processing and I fear that if I don't find the reason as to why this is happening, then decryption (which I plan to do through php) would not work either.
my php code is as follows:
$payload = "this is my plaintext";
$binary_signature = "";
$private_key = openssl_pkey_get_private(file_get_contents(storage_path('privatekey.pem')), 'enc123456789');
openssl_sign($payload, $binary_signature, $private_key, OPENSSL_ALGO_SHA256);
$signature = base64_encode($binary_signature);
$new_payload = $payload."&sign=".$signature; // where my actual plaintext is also used in my javascript code
$key = "thisismykey";
$iv = "\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0";
$encryption = openssl_encrypt($new_payload, 'AES-256-CBC', $key,OPENSSL_RAW_DATA, $iv);
dd(base64_encode($encyrption));
which outputs the following:
AEyHK+4DQWYjw8GeVV3mfzUJtk7ylxZINeryAdFptEbDyKOVbmNg8z32J2JgxGpFsQKpXxWaqDLf0IPNIq1jof0rKWhhDpaWzvTd0Tq/zgze7oGtZzEIqtdRDqax3ZvPkzNfuO/O14iW/YTwFkm9FLy9kGIirZDUTuAcOIjXGCgxhrhZHLn+V6SZpW5dYnH8u5rPDCeez2/HkUPI71YjD6hZ0DRjIkiXCyjPYH4fjNykz4yXo8hD+489Zxm8QPq1O1dyjR9JXSdrlYMWdixt6w0vz8EtPC8gZ+bDP/N/UEK07M52VB693zYb5uD1u7WuUUtsOjkr5ocF6QbEW7sjzI4q9yAxqvxRW/bkKqodcVqtglW6YsdJjrTR0EfA/Or/QF3e3QWVM5/2g4rT3ccE17OP6Rp/46yTpW9lOgS6Qiz2hY95GoaxbLfHB/Vb0Es+UppwDu8bd/u2Qax5erBi5ObZu3AjKNpTem45paspsKH3/vc2Jc810XrVQPjnDdZ8VrXvCgPiulywn5Mj28O7uUQ5bay3Zxy3bmHb7ESDEVMKiSEoru3LzDJ7wwPlidJzPcfWtuiMEMsPOv1Y6LaxtlizWM5/zYJFX/RA4d+KBl+Rn6BoPZDcX/6eh3oUoNhy
My JS Code (the plaintext is received through an ajax call which has the same sign method as you see from the php code, the encryption plaintext is of the same value from my php code)
function encrypt(plaintext, secretkey) {
var randomSeed = "";
randomSeed = secretkey;
var key = CryptoJS.enc.Utf8.parse(randomSeed.toUpperCase());
var iv = CryptoJS.enc.Utf8.parse("0000000000000000");
var encrypt = {};
encrypt = CryptoJS.AES.encrypt(plaintext, key, {
mode : CryptoJS.mode.CBC,
iv : iv
});
console.log("encryption is "+encrypt.toString());
return encrypt.toString(encrypt.toString());
}
Returns the correct value which is:
jLmAUr+JyCjbpctU4z6+dlF61jbHRphwTS0iAk3IRiy3jkfCtaCSWdiIO0awuX6G1jAlZroTiAuMl9OW0zj0q4HitndfGFtFUoMMCqZTzvMr6cy1TyG9EFz20T6ByrBnOvGuoVjv3Flufuk2Ghz5in2W2A3T+wF+SPXX/bIAnHtE3uW0bPl2q5tn6KyUI1uoQaYcMZKRPyzAQS7WSSwSOmAcVrRuDANgZQuO+3mh86QAdeFaYqFdZUnxD4c2kkbkGy17SUFfSK8Qjv+8tkTcYXV9QRRdWjGZiQQeyAr3PDKA4SDVzrcMNwJjTaLJiZv0Iau66HGpbf2yvRDLtIOoXQmnhs6NKTZpcSwZ07hHqVvBZmNRq+jqZOGw1s8GRH+Bz4yxSRycTS0DEddhyMoxhZcUc5wt42vDOYIEH2nuw/uu4gjrwpx0rVO1ssoZYRxvBaA6zSC4N04Wdn4JE2/LtXertDLEdLBtmk3c3n4QDU0tK5v31HMY7P7+fdQXU62niVxCNPSt9dpYa82IUrQuigNXgrbphQvZNmmcONi/4pnxJjKcKYpCn/1KhkBVUAhYm6UKJJvMNAo0M+cfsvReImrJx6IzPRdzTTFAQF5kW2NFkV4EIb0DtCF679RtdAhg5ShaP7QhqYL6EgFCs9WnJTACC26TmV20DAqUiuIYULLtjDW4qFOWi/y8D1JOWTar
I don't understand why my PHP encryption is giving the wrong output while my JavaScript encryption is giving the correct output
I'm hoping someone could give me an insight as to what I'm doing wrong from my PHP side. Wha I'm hoping to achieve is that my PHP encryption will output the same result as my JavaScript encryption.
Thank you in advance :)
I have solved my issue.
To those having a similar issue, here's a brief explanation. Crypto JS uses the following:
var key = CryptoJS.enc.Utf8.parse(randomSeed.toUpperCase());
var iv = CryptoJS.enc.Utf8.parse("0000000000000000");
Which translates to a word array if you console log the output. PHP needs to have the similar word array value that cryptoJS produces for it's AES encryption method. To solve this, you have to convert your $key and iv to be in hex format and format your code in php such as:
$key = pack("H*", "4a424d56595753555047553830334d42505a314f414256414c5a565239324659");
$iv = pack("H*", "30303030303030303030303030303030");
Then when you proceed to using openssl_encrypt
$encrypted_data = openssl_encrypt($plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
you will get the same output as crpytoJS.
Hope this helps.
Related
I'm using php 8.0.11, i have to generate a SHA256 encrypted messagesignature.When i test the API in postman with javascipt code in Pre-request script it give the right encrypted messagesignature, i converted the script to php when i test it in php it sends a different wrong encrypted messagesignature (key & msg are fake) :
javascript code (Pre-request script in postman):
let msg='mymessage'
const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,"myapipkey");
hmac.update(msg);
const messageSignature = hmac.finalize().toString();
pm.globals.set("messageSignature",messageSignature);
console.log('messageSi:',pm.globals.get('messageSignature'))
````
php code:
````php
$data_to_hash = "mymessage";
$data_hmac=hash('sha256', $data_to_hash);
$ctx = hash_init('sha256', HASH_HMAC, 'myapipkey');
hash_update($ctx, $data_hmac);
$result = hash_final($ctx);
echo $result;
````
A simple change to the PHP code should give the correct result.
It looks like you were hashing twice (or something like that!)
$data_to_hash = "mymessage";
$ctx = hash_init('sha256', HASH_HMAC, 'myapipkey');
hash_update($ctx, $data_to_hash);
$result = hash_final($ctx);
echo $result;
In any case, the output of the above code will be:
898786a1fa80da9b463c1c7c9045377451c40cf3684cbba73bdfee48cd3a5b8f
Which is the same as the JavaScript code, both match the output given here:
https://codebeautify.org/hmac-generator
With Algorithm = 'SHA256', Key = 'myapipkey' and Plaintext = 'mymessage'.
I have this code in PHP (can't modify)
<?php
$myPlain = "123456789012345678900000";
$myKey = md5($myPlain, true);
$myKey .= substr($myKey, 0,8);
$encrypted = openssl_encrypt($myPlain, 'des-ede3', $myKey, OPENSSL_RAW_DATA);
print(base64_encode($encrypted));
This code returns
FTYDrZTZMjVBv5Fk/xcfFxJASaizzrmoPts7fGDvWjc=
When I try to replicate this in NodeJS
function testEde3(myKey, myPlain) {
try {
let md5Key = crypto.createHash('md5').update(myKey, 'utf-8').digest("base64").substr(0, 24);
console.log(md5Key); //outputs 4o1aJrSWN3bSfjuIX6VXgA==
console.log(md5Key.length); //outputs 24
const cipher = crypto.createCipheriv('des-ede3', md5Key, null);
let encrypted = cipher.update(myPlain, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
} catch ( ex ) {
return ex;
}
}
const myKey = "123456789012345678900000";
const myPlain = "123456789012345678900000";
const hash = testEd3(myKey, myPlain);
console.log(`Hash is: ${hash}`);
The output is
Hash is: lDQX9OGsopKOt6P9WQwekGsKDQGFpfGW50zbs3HrOfQ=
I'm thinking the problem is on MD5. If I try to encrypt without md5, the results are the same.
Thanks
The key in Php code consists of the 16 bytes of the MD5 hash, to which the first 8 bytes of the MD5 hash are appended, resulting in a 24 bytes key (as required for 3DES). This is currently not implemented in the NodeJS code, but can be achieved e.g. by:
let md5Key = crypto.createHash('md5').update(myKey, 'utf-8').digest();
md5Key = Buffer.concat([md5Key, md5Key.slice(0, 8)]);
With this change, the NodeJS code generates the ciphertext of the PHP code with the same input data.
des-ede3 means 3DES in ECB mode. Note that 3DES is outdated and slow and should be replaced by AES. ECB mode is generally insecure. Encrypting the key also makes little sense (but may only be for testing purposes).
Setup:
session_start();
function set_encryption_method() {
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity']) > 3600) {
unset($_SESSION['cipher']);
unset($_SESSION['iv']);
unset($_SESSION['last_activity']);
}
$cipher = 'aes-256-cbc';
$iv = random_bytes(16);
if (in_array($cipher, openssl_get_cipher_methods())) {
if (!isset($_SESSION['cipher'])) {
$_SESSION['cipher'] = $cipher;
}
if (!isset($_SESSION['iv'])) {
$_SESSION['iv'] = $iv;
}
$_SESSION['last_activity'] = time();
} else {
die('Encryption method not supported!');
}
}
set_encryption_method();
Encrypt:
function encrypt_string($key, $string) {
// $key is a constant stored in a database
return rawurlencode(base64_encode(openssl_encrypt($string, $_SESSION['cipher'], $key, 0, $_SESSION['iv'])));
}
Decrypt:
function decrypt_string($key, $encrypted) {
// $key is a constant stored in a database
return openssl_decrypt(rawurldecode(base64_decode($encrypted)), $_SESSION['cipher'], $key, 0, $_SESSION['iv']);
}
When decrypt_string() is called with the appropriate parameters, it throws this error: digital envelope routines evp_decrypt_final_ex: bad decrypt. If I hardcode the iv, then it works correctly.
What am I doing wrong?
The error message is (indirectly) caused by the fact that your are using different IVs for encryption and decryption. From your description it is not clear how that can happen, but let me propose some suggestions that will avoid your issue altogether.
First, with EAS-CBC it is not a good idea to use the same IV + key combination multiple times. You can find some discussion on that in the answers to Is AES in CBC mode secure if a known and/or fixed IV is used?. You did not mention how you are using the different functions, but during 3600 seconds, you are using the same IV + key combinations.
To work around this, you could generate a random IV for every encryption that you do. You could then store the IV together with the encrypted data; the IV is not required or supposed to be secret. The following two functions are modifications of yours that do exactly that, by concatenating the two after encryption and splitting the two before decryption:
function encrypt_string($key, $string) {
// $key is a constant stored in a database
$iv = random_bytes(16);
$ciphtxt = openssl_encrypt($string, $_SESSION['cipher'], $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv.$ciphtxt);
}
function decrypt_string($key, $encrypted) {
// $key is a constant stored in a database
$combo = base64_decode($encrypted);
$iv = substr($combo, 0, 16);
$ciphtxt= substr($combo, 16);
return openssl_decrypt($ciphtxt, $_SESSION['cipher'], $key, OPENSSL_RAW_DATA, $iv);
}
Note the use of the flag OPENSSL_RAW_DATA. As the documentation for openssl_encrypt mentions (not too clearly), the result will be base64-ed if you do not give that flag. You were doing the base64-ing yourself so I added the flag. This also makes it a little easier to handle the (de-)concatenation of the IV and the ciphertext.
A few words about the occurrence of that bad decrypt error: that is emitted when the padding bytes are different than expected. Since you were using an incorrect IV for decryption, the AES decryption did not result in the original data, with a high probability that the padding bytes therefore did not have the right value.
First point to address: Why are you using URL encode/decode?
Those are for passing parameters through browsers; and the browser generally does the decoding for you so as a base rule - if you've written "urldecode" in your PHP, you've probably gone wrong as you're possibly decoding something already decoded. (Or are not using in the right place, or using when unnecessary)
However, Base64 is (or should be) url-safe, so I'd suggest knocking out the urlencode/decode and see what happens.
Second point to address: you need to undo the urlencode/base64 conversion in the reverse order you set.
So rawurlencode(base64_encode(openssl_encrypt( reversed is openssl_decrypt(base64_encode(rawurldecode (you have the base64 and url decode flipped). But knocking out the urlencode/decode entirely (unless you're adamant you need it) will rectify this too.
My problem is as follows:
I have a PHP script that is responsible for encrypting a string using AES-256-CBC encryption. This script uses the openssl lib and returns an X result.
<?php
class AES
{
const PRIVATE_KEY = 'abcdefghijklmnnoabcdefghijklmnno';
const ENCRYPT_METHOD = 'aes-256-cbc';
const VECTOR = 'abcdefghijklmnno';
public function encryptData($data)
{
while(strlen($data) < 16) $data .= "\0";
return openssl_encrypt($data, self::ENCRYPT_METHOD, self::PRIVATE_KEY, OPENSSL_ZERO_PADDING, self::VECTOR);
}
public function encryptDataL($data)
{
return openssl_encrypt($data, self::ENCRYPT_METHOD, self::PRIVATE_KEY, 0, self::VECTOR);
}
public function decryptData($data)
{
return openssl_decrypt($data, self::ENCRYPT_METHOD, self::PRIVATE_KEY, OPENSSL_ZERO_PADDING, self::VECTOR);
}
}
$aes = new AES();
echo $aes->encryptData("abcdefghijkl");
echo "\n";
echo $aes->encryptDataL("{\"REQUEST\": [{\"MSISDN\": \"32156489721\",\"IDPRODUCT\": 123,\"IDOPERATOR\": 12345,\"OUTPUTFORMAT\": \"JSON\"}],\"OUTPUTFORMAT\": \"json\"}");
?>
when I run a JS script, responsible for doing the same, but using the Crypto lib, the result obtained is different from the previous X.
const crypto = require('crypto');
const cipher = crypto.createCipheriv('aes-256-cbc', 'abcdefghijklmnnoabcdefghijklmnno', 'abcdefghijklmnno');
let crypted = cipher.update(data, 'utf8', 'base64');
crypted += cipher.final('base64');
The results of the scripts differ, even though, in theory, the encryption should be the same.
An example of return is as follows:
For the php script: input -> ^y3Hk3JKGGgA output -> eTqD5Op389QS/TOoui5kAQ==
For the js script: input -> ^y3Hk3JKGGgA output -> HHfskOE1N+QxdGt9MTai5A==
The desired result is the PHP script, but I need to run the code in JS, can someone explain to me what I may be doing wrong?
I tried different ways to execute the createCipheriv method, but they all return the same result (different from what I need, which is the result obtained through the PHP script)
Thank you in advance.
Thank you guys for trying to help, indeed I posted the question lacking some informations (actually when the question was made I didn't have all the pieces of information I needed).
But posting here some facts and the solution encountered for my case.
The different results in the cases above only happen for the first PHP function ("encryptData"), responsible for encrypting small texts. The second, responsible for encrypting large texts (more than 16 bits) worked fine, both in PHP and JS scripts.
The solution I encountered was making the padding needed for the AES-256 algorithm by myself. The padding function provided by the Crypto lib didn't work, at least for my case.
So I disabled the padding in my cypher class and make sure that the data sent to be encrypted was padded correctly until the length is multiple of 16. The end's code is below.
encryptWithAES256(data) {
// added padding until data length is multiple of 16
let paddedData = data;
while (paddedData.length % 16 !== 0) {
paddedData += '\0';
}
// ciphers data
const cipher = crypto.createCipheriv('aes-256-cbc', encodeKey, IV);
cipher.setAutoPadding(false);
let crypted = cipher.update(paddedData, 'utf8', 'base64');
crypted += cipher.final('base64');
return crypted;
}
I have code written in node.js:
var encodeKey = crypto.createHash('sha256').update(pass, 'utf-8').digest();
var cipher = crypto.createDecipheriv('aes-256-cbc', encodeKey, iv);
console.log(cipher.update(text, 'hex', 'utf8') + cipher.final('utf8'));
i have text, pass and iv vars. When i try translate this code into PHP, i have different results. This is PHP code:
$encodeKey = hash('sha256', $pass);
$decrypted = openssl_decrypt($text, 'aes-256-cbc', $encodeKey, 0, $iv);
Where is the bug? Thanks a lot for any suggestion!
Your JS code...
var encodeKey = crypto.createHash('sha256').update(pass, 'utf-8').digest();
...and your PHP code...
$encodeKey = hash('sha256', $pass);
Are not the same. They're producing different keys, which will result in different encryption.
Try this instead:
$encodeKey = hash('sha256', $pass, true);
However, this is only part of the problem.
AES-CBC is vulnerable to chosen-ciphertext attacks.
IVs should be randomly generated per message, not held constant. (It isn't clear from your code if this is static or not.)
Recommendation: Switch to Sodium-Plus (JS) and ext/sodium (PHP).
If you upgrade PHP to 7.2 or newer, you should get libsodium for free. Otherwise, sodium_compat will do the trick.