Can't write Node.js analog to PHP hash checking - php

Following PHP lines works great, but I can't do such in Node
$secret_key = hash('sha256', XXXX, true);
$hash = hash_hmac('sha256', YYYY, $secret_key);
As documentation sais hash() returns raw binary data, but it seems like utf8 string. Trying to do such in Node.js
const secret = crypto.createHash('sha256')
const secret_key = secret.update(XXXX).digest('utf8')
const hmac = crypto.createHmac('sha256', secret_key)
const result = hmac.update(YYYY).digest('hex')
So PHP's $hash and Node.js result are not the same. Have tried secret key with 'hex' with no success. How to reproduce it in Node exactly as in PHP?

If you leave out the encoding of the first digest altogether, then you get equal strings:
const secret = crypto.createHash('sha256')
const secret_key = secret.update('XXXX').digest()
const hmac = crypto.createHmac('sha256', secret_key)
const result = hmac.update('YYYY').digest('hex')
console.log(result);
Corresponding PHP code:
<?php
$secret_key = hash('sha256', 'XXXX', true);
$hash = hash_hmac('sha256', 'YYYY', $secret_key);
echo $hash;
PHP: c4888731de466cefaa5c831b54132d3d9384310eb1be36f77f3f6542266cb307
NodeJS: c4888731de466cefaa5c831b54132d3d9384310eb1be36f77f3f6542266cb307

I guess your mistake is making node export your secret key as "utf8" instead of a hexadecimal representation.
In PHP your key seems to be presented as hex values as well.
Try using "hex" in the first case as well and see what happens:
const secret = crypto.createHash('sha256')
const secret_key = secret.update(XXXX).digest('hex')
const hmac = crypto.createHmac('sha256', secret_key)
const result = hmac.update(YYYY).digest('hex')

Related

Malformed UTF-8 data Error: Malformed UTF-8 data on decrypting data in CryptoJS

I tried to decrypt my api response object which was encrypted in php. Encryption is working and decrypting in PHP worked perfectly too. But decrypting it in ionic 5 always return "Malformed UTF-8 data error".
When decrypted back in PHP:
Array
(
[name] => fagbemi Ayodele
[username] => fagbemiayodele48
[email] => fagbemiayodele48#yahoo.com
[createTime] => 2022-08-05 03:28:41
[updateTime] =>
[gender] => male
[telephone] => 07069605705
[constellation] => 0
[userId] => 130
[loginTime] => 1672319617
[avatar] => http://localhost:83/forum_new/assets/logo/avatar.png
)
PHP encryption code:
function encrypt($value, string $passphrase)
{
$salt = openssl_random_pseudo_bytes(8);
$salted = '';
$dx = '';
$data = [];
while (strlen($salted) < 48) {
$dx = md5($dx . $passphrase . $salt, true);
$salted .= $dx;
}
$key = substr($salted, 0, 32);
$iv = substr($salted, 32, 16);
$encrypted_data = openssl_encrypt(json_encode($value), 'aes-256-cbc', $key, true, $iv);
$data["ct"] = base64_encode($encrypted_data);
$data["iv"] = bin2hex($iv);
$data["s"] = bin2hex($salt);
return json_encode($data);
}
Ionic 5 (Typescript) decryption code
async base64Decrypt(jsonStr: string, passphrase: string): Promise<any> {
const json = JSON.parse(jsonStr);
const salt = CryptoJS.enc.Hex.parse(json["s"]);
const iv = CryptoJS.enc.Hex.parse(json["iv"]);
const ct = CryptoJS.enc.Base64.parse(json["ct"]);
const concatedPassphrase = passphrase + salt;
const md5 = [];
md5[0] = CryptoJS.MD5(concatedPassphrase);
let result = md5[0];
for (let i = 1; i < 3; i++) {
md5[i] = CryptoJS.MD5(CryptoJS.lib.WordArray.create(md5[i - 1]).concat(concatedPassphrase));
result = CryptoJS.lib.WordArray.create(result).concat(md5[i]);
}
const key = result.toString(CryptoJS.enc.Hex).substring(0, 32);
const data = CryptoJS.AES.decrypt({ciphertext: ct}, CryptoJS.enc.Hex.parse(key), {iv: iv});
return JSON.parse(data.toString(CryptoJS.enc.Utf8));
}
decryption key
"key":"8cd826ce5315e72a"
Encrypted data:
{"ct":"2A5q0EUDjgp4JLO341jEtJz7hoJzCm6vr9AKmz3TygAmiEXz8+phACCjUwBAadf4KRUCI\/CFLrd7zjr3gp8ttsBZv3yQwYjCKVyYnMc4hhugJkstNi80ySnMCGMzKoampUBs2gtxgbEtI0a6UPJMAC3Lq8LcdQxKNdwItfbv4JbBa1+N\/akIos8\/4IgHT+47AImd6VuXouSf\/K3NUxzUDzEmBmENozi21yELHZbjkC4EcsNKoYp8mDRQA6hqRVaVqM+TtkSZNdw5MtOz5mvp\/FJ76SRXC8gXP1jL6y93s+z7e1Vlje79pTRhSCcLFDdczib+H2t2fnDZOXwaQmjZQXUV2FoloLkfWzvCF\/Rh\/AX3wtGLpp43qAH8ec6vrFOgFD1TqQqWFloOmelmp6aDMhULTG33fHmHPMNbkHWRA+k=","iv":"d6ac6a0fb929e15909942e2aceae8762","s":"22a3d46564b07b91"}
How can I resolve the error?
ERROR Error: Uncaught (in promise): Error: Malformed UTF-8 data Error:
Malformed UTF-8 data
at Object.stringify
The PHP code implements the logic of EVP_BytesToKey() for key derivation, which is also the internal key derivation function of CryptoJS.
Therefore, decryption can be significantly simplified on the JavaScript side. To do this, the passphrase must be passed as string and salt and ciphertext encapsulated in a CipherParams object:
var passphrase = "8cd826ce5315e72a";
var jsonStr = `{"ct":"2A5q0EUDjgp4JLO341jEtJz7hoJzCm6vr9AKmz3TygAmiEXz8+phACCjUwBAadf4KRUCI\/CFLrd7zjr3gp8ttsBZv3yQwYjCKVyYnMc4hhugJkstNi80ySnMCGMzKoampUBs2gtxgbEtI0a6UPJMAC3Lq8LcdQxKNdwItfbv4JbBa1+N\/akIos8\/4IgHT+47AImd6VuXouSf\/K3NUxzUDzEmBmENozi21yELHZbjkC4EcsNKoYp8mDRQA6hqRVaVqM+TtkSZNdw5MtOz5mvp\/FJ76SRXC8gXP1jL6y93s+z7e1Vlje79pTRhSCcLFDdczib+H2t2fnDZOXwaQmjZQXUV2FoloLkfWzvCF\/Rh\/AX3wtGLpp43qAH8ec6vrFOgFD1TqQqWFloOmelmp6aDMhULTG33fHmHPMNbkHWRA+k=","iv":"d6ac6a0fb929e15909942e2aceae8762","s":"22a3d46564b07b91"}`;
var json = JSON.parse(jsonStr);
var cipherParams = {ciphertext: CryptoJS.enc.Base64.parse(json.ct), salt: CryptoJS.enc.Hex.parse(json.s)}; // apply a CipherParams object
var decryptedData = CryptoJS.AES.decrypt(cipherParams, passphrase).toString(CryptoJS.enc.Utf8); // decrypt and UTF-8 decode
var decryptedDataJSON = JSON.parse(decryptedData); // convert to JavaScript object
console.log(decryptedDataJSON);
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
A few notes:
CryptoJS derives the IV internally, along with the key, so it does not need to be explicitly passed to the decrypting side.
Instead of a CipherParams object, salt and ciphertext can alternatively be passed in the Base64 encoded OpenSSL format to CryptoJS.AES.decrypt(). The OpenSSL format corresponds to the ASCII encoding of Salted__, followed by the 8 bytes salt and the ciphertext. In this case, it would be efficient to format the data accordingly already on the PHP side:
...
return base64_encode("Salted__" . $salt . $encrypted_data);
In openssl_encrypt(), OPENSSL_RAW_DATA should be passed as 4th parameter instead of true (although both are numerically identical, the latter is more transparent).
EVP_BytesToKey() is considered insecure nowadays. Instead (at least) the more reliable PBKDF2 should be applied, which is supported by both PHP and CryptoJS.

Webhook Authenticity from Node.js to PHP

I need help translating this Node.js code into PHP. It's basically for authenticating whether the signature on my header is the same as the signature that has been encoded using base64 and HMAC-SHA256.
const crypto = require('crypto');
const timestamp = "1570350275357";
const msg = {'key1':'world','key2':'world'};
const secretKey = "mysecretsecret";
const decodedKey = Buffer.from(secretKey, 'base64').toString('utf8');
const signature = crypto.createHmac('SHA256', decodedKey).update(timestamp + '.' + msg).digest('base64');
const signatureHeader = "+DCfT1wIMUiaZnlZB4u59/d5wkXKA89lv67Ov66vnyc=";
assert(signature === signatureHeader);
So I have my request body which is {'key1':'world','key2':'world'} , the timestamp (x-duda-signature-timestamp header) is 1570350275357 and a secret key which is mysecretsecret.
It also calculates for my signature base64(hmac-sha256(secret-key, timestamp + "." + message)) which results in +DCfT1wIMUiaZnlZB4u59/d5wkXKA89lv67Ov66vnyc= , which is also the value to be found on this x-duda-signature header of the request.
I've already tried putting the pieces together on PHP and it just won't return a signature that is similar to my signature header which is +DCfT1wIMUiaZnlZB4u59/d5wkXKA89lv67Ov66vnyc=.
$getHeaders = apache_request_headers();
$timestamp = "1570350275357";
$signatureHeader = "+DCfT1wIMUiaZnlZB4u59/d5wkXKA89lv67Ov66vnyc=";
$secretKey = "mysecretsecret";
//Request Body
$message = '{'key1':'world','key2':'world'}';
$signature = base64_encode(hash_hmac('sha256',$timestamp.".".$message,$secretKey));
print_r($signature); // the value should be equal to ```$signatureHeader```
Any idea on where I went wrong? Thanks in advance!

Decrypting PHP openssl_ecrypt with NodeJS crypto errors

We have a legacy PHP system that encrypted some data via openssl_encrypt. The PHP code is pretty straight forward. (All values are randomly generated for this example, but are the same format and lengths as the real values and reproduce the same errors).
$in = '12345';
$method = 'AES-256-CBC';
$key = '5fjfwc7kp84z5yet358t';
$options = 0;
$iv = '8x69nt6qnptg3x4j';
openssl_encrypt($in, $method, $key, $options, $iv);
Decrypting via PHP is also pretty straight forward.
$in = 'yy03+cUpsq5uGWclBLtwIA==';
$method = 'AES-256-CBC';
$key = '5fjfwc7kp84z5yet358t';
$options = 0;
$iv = '8x69nt6qnptg3x4j';
openssl_decrypt($in, $method, $key, $options, $iv);
However, when trying to port it over to Node crypto I keep getting errors on key length, iv length, and numerous other errors as I try different approaches.
const input = Buffer.from('yy03+cUpsq5uGWclBLtwIA==');
const iv = Buffer.from('8x69nt6qnptg3x4j');
const key = Buffer.from('5fjfwc7kp84z5yet358t');
let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv, 0);
let clearText = decipher.update(input, 'base64', 'utf8');
clearText += decipher.final('utf8');
I've probably tried half a dozen or more examples in NodeJS and all produce errors and fail to decrypt entirely.
Current error is "Invalid key length" which remains the error even if I restrict it to 16 characters.
Padding and base64 processing was the solution. Working code looks closer to this:
const keyStr = '5fjfwc7kp84z5yet358t';
const diff = Math.abs(keyStr.length - 32);
const padding = Buffer.alloc(diff, 0x00);
const input = Buffer.from('yy03+cUpsq5uGWclBLtwIA==', 'base64');
const iv = Buffer.from('8x69nt6qnptg3x4j');
let key = Buffer.from('5fjfwc7kp84z5yet358t');
key = Buffer.concat([key, padding]);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv, 0);
let clearText = decipher.update(input, 'base64', 'utf8');
clearText += decipher.final('utf8');

Encrypt with CryptoJS to Decypt with openssl_decrypt on PHP 7 returns always false

I need in my front (ionic) encrypt a UUID to send it to the backend.
I use CryptoJS, this is my code to encrypt the UUID.
const UUID = 'ABCD1234';
const privateKey = 'f38d09938ead31a57eca34d2a0df1c44';
const salt = CryptoJS.lib.WordArray.random(16);
const iv = CryptoJS.lib.WordArray.random(16);
const key = CryptoJS.PBKDF2(privateKey, salt, {
hasher: CryptoJS.algo.SHA512,
keySize: 4,
iterations: 1
});
const encripted = CryptoJS.AES.encrypt(UUID, key, {
iv
});
const cp = {};
cp.mid = CryptoJS.enc.Base64.stringify(encripted.ciphertext);
cp.iv = CryptoJS.enc.Hex.stringify(iv);
cp.s = CryptoJS.enc.Hex.stringify(salt);
So I send the CP in headers to the server (Slim Framework 3).
I receive like this:
$mid = $request->getHeader('HEADER-MID')[0];
$iv = $request->getHeader('HEADER-IV')[0];
$salt = $request->getHeader('HEADER-S')[0];
//Example Data Received
//mid: losv78Amn1zpgRe5/4hYFA==
//iv: 2d198339d178c053c37e36b7d03e8a3b
//salt: fb07dd1f61d72148bc1423af8cd1f295
$ct = base64_decode($mid);
$salt = hex2bin($salt);
$iv = hex2bin($iv);
$key = hash_pbkdf2("sha512", $privateKey, $salt, 1, 32);
$decrypted = openssl_decrypt($ct, 'AES-256-CBC', hex2bin($key), OPENSSL_RAW_DATA, $iv);
The key is generated correctly in back and front.
But the openssl_decrypt function return false and with openssl_error_string() get this:
error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length
I do not know what I'm doing wrong.
Thanks in advance.

Encrypt with CakePHP 3.0 and decrypt with NodeJS

I have administration site developed with CakePHP 3.0 framework and i use default Security::encrypt($text, $key, $hmacSalt = null) to encrypt token for API authorization.
I also have simple NodeJS service for real time communication and i want to use the same token for API and this real time communication.
I try to rewrite CakePHP decryption function to NodeJS on different ways but i can't get correct results. Below is CakePHP decrypt function:
public static function decrypt($cipher, $key)
{
$method = 'AES-256-CBC';
$ivSize = openssl_cipher_iv_length($method);
$iv = mb_substr($cipher, 0, $ivSize, '8bit');
echo "---- IV --- \r\n";
var_dump($iv);
$cipher = mb_substr($cipher, $ivSize, null, '8bit');
echo "---- KEY --- \r\n";
var_dump($key);
echo "---- CIPHER LAST --- \r\n";
var_dump($cipher);
return openssl_decrypt($cipher, $method, $key, OPENSSL_RAW_DATA, $iv);
}
Result from CakePHP:
---- IV ---
string(16) "��r�N3U�Y6Q�#��"
---- KEY ---
string(32) "1c494314996afe280bc5981c4e185f79"
---- CIPHER LAST ---
string(160) "~a�xh�z��+���M����j*!�(����f�ZG;�)w��Kl�3�m��Z��ە��OR9~���6[X�/��n��B6��C��˟f��!6��1���|S��*�mG+���OR�kr��t�;�+�㟱��"���<i����e:��"
Here is my simple code in NodeJS:
var buf = new Buffer(socket.handshake.query.token, 'base64').toString('utf8', 64);
var iv = buf.substr(0,16);
console.log("-----IV------")
console.log(iv);
var key = sha256(config.tokenKey+config.tokenSalt).substr(0,32);
console.log("-----KEY------")
console.log(key);
var cipher = buf.substr(16);
console.log("------CIPHER-----");
console.log(cipher);
var decipher = crypto.createDecipheriv('AES-256-CBC', key, iv);
//decipher.setAutoPadding(false);
var dec = decipher.update(cipher);
dec += decipher.final('utf-8');
Result from NodeJS:
-----IV------
��r�N3U�Y6Q�#��
-----KEY------
1c494314996afe280bc5981c4e185f79
------CIPHER-----
~a�xh�z��+���M���
��j*!�(����f�ZG;�)w��Kl��m���Z����ە��OR9~���6[X�/��n��B6��C��˟f���!6��1���|S��*�mG+���OR�kr��t�;�+�㟱��"���<i����e:��
crypto.js:239
this._handle.initiv(cipher, toBuf(key), toBuf(iv));
Error: Invalid IV length
I try to create IV on different ways, but it doesnt work, even if i succeed to avoid exception i dont get correct result, I assume that issue is in "8bit" encoding in PHP code.
If someone know how to solve this i would be very thankful!
I'm not overly familiar with Node.js, but what I can see is that you screw up the data when you convert the input to an UTF-8 string, you need to work with binary data, not with a string.
I'd suggest to work with buffers until you actually need to convert something to a string, at least that's how I did it when I had to decrypt data that was encrypted with CakePHP:
var data = Buffer.from(socket.handshake.query.token, 'base64');
var key = config.tokenKey;
var salt = config.tokenSalt;
var hmacSize = 64;
var ivSize = 16;
var keySize = 32;
var encrypted = data.slice(hmacSize);
key = crypto
.createHash('sha256')
.update(key + salt)
.digest('hex')
.substr(0, keySize);
var iv = encrypted.slice(0, ivSize);
encrypted = encrypted.slice(ivSize);
var decipher = crypto.createDecipheriv('AES-256-CBC', key, iv);
var decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
console.log(decrypted.toString('utf8'));

Categories