Can't decrypt CryptoJS-encrypted strings in PHP - javascript

First of all, I'm a newbie in this subject. This is the first time I need to send encrypted data from front-end to back-end in a secure way.
I'm trying to use CryptoJS to encrypt some sensitive data (credit card information) and send it to my PHP backend, where I need to decrypt it to use with the integration with my payment gateway.
This is my JS code:
let card_number = form.find("input#card_number").val();
let card_holder = form.find("input#card_holder").val();
let card_expiration_date = form.find("input#card_expiration_date").val();
let card_cvv = form.find("input#card_cvv").val();
let key = globalSettings.encryption_key;
card_number = CryptoJS.AES.encrypt(card_number, key);
card_holder = CryptoJS.AES.encrypt(card_holder, key);
card_expiration_date = CryptoJS.AES.encrypt(card_expiration_date, key);
card_cvv = CryptoJS.AES.encrypt(card_cvv, key);
formData = {
card_number: card_number.toString(),
card_holder: card_holder.toString(),
card_expiration_date: card_expiration_date.toString(),
card_cvv: card_cvv.toString(),
};
For note, the globalSettings object is sent from backend with configured keys. The generated formData object is sent to back-end with no problem. But, when trying to decrypt the values with openssl_decrypt, I'm only getting false results.
Here is my PHP code:
$card_number = $request->get("card_number");
$card_holder = $request->get("card_holder");
$card_expiration_date = $request->get("card_expiration_date");
$card_cvv = $request->get("card_cvv");
$key = ENCRYPTION_KEY;
$method = "AES-256-CBC";
$card_number = openssl_decrypt($card_number, $method, $key);
$card_holder = openssl_decrypt($card_holder, $method, $key);
$card_expiration_date = openssl_decrypt($card_expiration_date, $method, $key);
$card_cvv = openssl_decrypt($card_cvv, $method, $key);
Before applying openssl_decrypt, the variables are exact the same as sent from front-end. But, after applying openssl_decrypt, they are all changed to false, and I can't figure out why and how to fix it to get back decrypted information.

Related

Encrypt in JavaSscript with NaCl, Decrypt With PHP libsodium using KeyPair

i want to use NaCl library in the client side and libsodium in php to encrypt/decrypt messages between the client and the server using keypair(public,private).
When i encrypt with libsodium and decrypt with NaCl everything works, But when i try to encrypt with NaCl and decrypt with libsodium i get no result.
Encrypting with NaCl in the client side
var message = "Sample Message";
var bytes = nacl.util.decodeUTF8(message);
var client_public = nacl.util.hex2bytearray('35e9f8477b60f61747722698d98791c4e0818ff75da3943a30e13dc0a457d402');
var client_private = nacl.util.hex2bytearray('6382cd5ef713375b6fe3835a763c72dfdc7ddcade7b8bcb9640065582708ef2c');
var server_public = nacl.util.hex2bytearray('1f348bac3b60d3076fc1a32d4e205df67356a9ef16d4b3f391c0e4817f3b987f');
var sharedkey = nacl.box.before(server_public, client_private);
var nonce = nacl.util.hex2bytearray('c031122a5f57e253686d9f6082d1061a1546e5567290c7f0'); // 24 bytes
var cipher = nacl.box.after(bytes, nonce, sharedkey );
var cihper_with_nonce = nacl.util.bytearray2hex(nonce) + bytearray2hex(cipher); // will be sent to the server
//c031122a5f57e253686d9f6082d1061a1546e5567290c7f0b2c05c9fb1483fe9b1bd2d4ae261832a54569748dd7925511b3d236e23e8
nacl.util.hex2bytearray = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
nacl.util.bytearray2hex = bytes => bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
nacl.util.decodeUTF8=function(e){;var n,r=unescape(encodeURIComponent(e)),t=new Uint8Array(r.length);for(n=0;n<r.length;n++)t[n]=r.charCodeAt(n);return t}
Decrypting with libsodium in PHP
$cihper_with_nonce = 'c031122a5f57e253686d9f6082d1061a1546e5567290c7f0b2c05c9fb1483fe9b1bd2d4ae261832a54569748dd7925511b3d236e23e8';// received from the client
$server_public = hex2bin('1f348bac3b60d3076fc1a32d4e205df67356a9ef16d4b3f391c0e4817f3b987f');// generated with libsodium
$server_private = hex2bin('cea48d3c14bc9ad26bf3f62932db5d7336b8212866c14dec64670f295ec89d5d');// generated with libsodium
$client_public = hex2bin('35e9f8477b60f61747722698d98791c4e0818ff75da3943a30e13dc0a457d402');// generated with NaCl
$sharedkey = sodium_crypto_box_keypair_from_secretkey_and_publickey($server_private, $client_public );
$nonce = hex2bin(substr($cihper_with_nonce, 0, 48));//24 bytes
$cihper = hex2bin(substr($cihper_with_nonce, 48));
$message = sodium_crypto_box_open($cihper , $nonce, $sharedkey );
var_dump($message);// bool(false)
Am i using the Library the Wrong way or there is something i am doing it wrong?
Because when i encrypt with php and decrypt with NaCl everything works and i did the same way in reverse for the Decryption.
all the bytes(hex) used are generated with the same library.
NaCl Source nacl util
libsodium Source libsodium

How to translate this encryption function from PHP to Javascript

I am moving a web app from PHP to a JS-based framework. The app uses mcrypt_encrypt and base64 for encryption. I tried using the mcrypt module in Javascript but I'm not getting the same result.
The original PHP function looks like this
function safe_b64encode($string) {
$data = base64_encode($string);
$data = str_replace(array('+', '/', '='), array('-', '_', ''), $data);
return $data;
}
function encrypt($value) {
$text = $value;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$crypttext = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, ENCRYPTION_KEY, $text, MCRYPT_MODE_ECB, $iv);
return trim(safe_b64encode($crypttext));
}
My JS version looks like this
const MCrypt = require('mcrypt').MCrypt
const rijndael128Ecb = new MCrypt('rijndael-128', 'ecb')
const iv = rijndael128Ecb.generateIv()
rijndael128Ecb.validateKeySize(false)
rijndael128Ecb.open(ENCRYPTION_KEY, iv)
let cipherText = rijndael128Ecb.encrypt('sometext')
cipherText = Buffer.concat([iv, cipherText]).toString('base64')
cipherText = cipherText.replace('+','-').replace('/','_').replace('=','')
I think you're nearly there, you just need to use the same algorithm, you were using 128-bit Rijndael, I switched to 256-bit in Node.js, it's working now.
// Surely this key is uncrackable...
const ENCRYPTION_KEY = 'abcdefghijklmnop';
const MCrypt = require('mcrypt').MCrypt;
function encryptRijndael256(plainText, encryptionKey) {
const rijndael256Ecb = new MCrypt('rijndael-256', 'ecb');
const iv = rijndael256Ecb.generateIv();
rijndael256Ecb.validateKeySize(false);
rijndael256Ecb.open(encryptionKey, iv);
let cipherText = rijndael256Ecb.encrypt(plainText);
cipherText = cipherText.toString('base64');
cipherText = cipherText.replace('+','-').replace('/','_').replace('=','')
return cipherText;
}
const plainText = 'sometext';
const cipherText = encryptRijndael256(plainText, ENCRYPTION_KEY);
console.log("Cipher text: ", cipherText);
I'm getting the following ciphertext (with the trivial and insecure!) key I'm using:
k3ZQ8AbnxhuO8TW1VciCsNtvSrpbOxlieaWX9qwQcr8
for the result in both PHP and JavaScript.

Verifying signature in NodeJS

In my logic i am hashing some data based on a secret key. Later I want to verify that signature. I am using the crypto package in nodejs. Specifically, in the verifier.verify function, the docs require a publicKey. How would i go upon doing this as i am using the secret in config?
Any help would be awesome!
let data = {
"data": 15
}
config: {
secret: 'mgfwoieWCVBVEW42562tGVWS',
}
let stringData = JSON.stringify(data)
const hash = crypto.createHmac('sha256', config.secret)
.update(stringData, 'utf-8')
.digest('base64')
const verifier = crypto.createVerify('sha256')
let ver = verifier.verify(publicKey, stringData, 'base64')
console.log(ver);
If you want to verify a particular signature in node you can use the following
config: {
secret: "IloveMyself"
};
var cipherPassword = crypto.createCipher('aes-256-ctr', config.secret);
var dbpassword = cipherPassword.update(dbpassword, 'utf-8', 'hex');
This will be for creating encryption of the password. Now to verify the password/signature again in NodeJS, you need to do the following:
var cipher = crypto.createDecipher('aes-256-ctr', config.secret);
var dbPassword = cipher.update(dbpassword, 'hex', 'utf-8');
This will give decrypted password/signature which you can compare easily. I hope it helps!

Having trouble correctly creating JWT for google service account auth

I'm trying to create a script on scriptr.io that creates a JWT/JWS to send to google's token endpoint in order to get an auth_token for my service account. I'm using the CryptoJS library in order to do the encrypting. I'm able to generate all 3 parts of the JWT, but I'm doing something wrong when doing so. I believe it has something to do with the last of the three parts of the string (so, the signature part), but I could be wrong.
var cryptoJs = {};
cryptoJs['SHA256'] = require('CryptoJS/rollups/sha256.js').CryptoJS.SHA256
var pHeader = {"alg":"RS256","typ":"JWT"}
var sHeader = JSON.stringify(pHeader);
var encodedHeader = Base64EncodeUrl(btoa(sHeader));
console.log("encodedHeader: " + encodedHeader);
var now = new Date();
var oneHourExpiration = ((now.getTime()-now.getMilliseconds())/1000)+3000;//3000, not 3600 which is 1 hour
var pClaim = {};
pClaim.iss = "-------#---iam.gserviceaccount.com";
pClaim.scope = "https://www.googleapis.com/auth/spreadsheets";
pClaim.aud = "https://www.googleapis.com/oauth2/v3/token";
pClaim.exp = oneHourExpiration;
pClaim.iat = Math.floor(Date.now()/1000);
console.log("exp: " + pClaim.exp);
console.log("iat: " + pClaim.iat);
var sClaim = JSON.stringify(pClaim);
var encodedClaim = Base64EncodeUrl(btoa(sClaim));
console.log("encodedClaim: " + encodedClaim);
var byteArray = encodedHeader + "." + encodedClaim;
console.log("byteArray: " + byteArray);
var secret = "-----BEGIN PRIVATE KEY-----\n.....MIIE.....=\n-----END PRIVATE KEY-----\n";
var signature = cryptoJs.SHA256(byteArray, secret);
var encodedSignature = Base64EncodeUrl(btoa(signature));
console.log("Encoded Signature: " + encodedSignature);
var sJWS = byteArray + "." + encodedSignature;
console.log("JWT: " + sJWS);
function Base64EncodeUrl(str){
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
}
var http = require("http");
var requestObject = {
"url": "https://www.googleapis.com/oauth2/v3/token",
"method": "POST",
"headers": {"Content-Type":"application/x-www-form-urlencoded"},
"params": {"grant_type":"urn:ietf:params:oauth:grant-type:jwt-bearer","assertion":sJWS}
}
var response = http.request(requestObject);
var responseBodyStr = response.body;
console.log(responseBodyStr);
var token = JSON.parse(responseBodyStr.access_token);
console.log(token);
When I send the request to the token endpoint with the JWT I get the following response
{
"error": "invalid_grant",
"error_description": "Invalid JWT Signature."
}
Any idea where I'm going wrong? Can someone help me correctly format the JWT so I can get a token?
The function used is doing a hash, not a digital signature
var signature = cryptoJs.SHA256(byteArray, secret);
Digital signature with a RSA private key is not supported . Take a look at the comment in the main repository of CryptoJS
Inactivity
CryptoJS is a project that I enjoy and work on in my spare time, but
unfortunately my 9-to-5 hasn't left me with as much free time as it
used to. I'd still like to continue improving it in the future, but I
can't say when that will be. If you find that CryptoJS doesn't meet
your needs, then I'd recommend you try Forge.
I suggest to move the code to use other Javascript library like recommended. For example forge support RSA signatures (https://github.com/digitalbazaar/forge#rsa)
Google OAuth2 server uses RS256. I have provided an snippet to convert the secret key (I assumed PEM format) to forge and sign data using RSA with SHA256
The only signing algorithm supported by the Google OAuth 2.0 Authorization Server is RSA using SHA-256 hashing algorithm. This is expressed as RS256 in the alg field in the JWT header.
// convert a PEM-formatted private key to a Forge private key
var privateKey = forge.pki.privateKeyFromPem(pem);
// sign data with a private key and output DigestInfo DER-encoded bytes (defaults to RSASSA PKCS#1 v1.5)
var md = forge.md.sha256.create();
md.update(byteArray, 'utf8');
var signature = privateKey.sign(md);
//convert signature to base64
var encodedSignature = Base64EncodeUrl(btoa(signature));

Encrypt string in PHP and decrypt in Node.js

I am sending data through insecure connection between Apache and Node.js servers. I need to encrypt data in PHP and decrypt in Node.js. I've spent 2 days trying to get it to work, however I only managed to get message signing to work, no encryption. I tried passing AES128-CBC, AES256-CBC, DES, AES128, AES256 as algorithms, however nothing worked well..
I tried this in PHP:
$data = json_encode(Array('mk' => $_SESSION['key'], 'algorithm' => 'SHA1', 'username' => $_SESSION['userid'], 'expires' => $expires));
$payload = openssl_encrypt($data, 'des', '716c26ef');
return base64_encode($payload);
And in Node.js:
var enc_json = new Buffer(response[1], 'base64');
var decipher = crypto.createDecipher('des', '716c26ef');
var json = decipher.update(enc_json).toString('ascii');
json += decipher.final('ascii');
And besides wrong decrypted data I get error such as these:
TypeError: error:0606508A:digital envelope routines:EVP_DecryptFinal_ex:data not multiple of block length
TypeError: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length
I need a simple encryption as data is not too sensitive (no password or user data), however data should only be read by the recipient. Key length can be anything, but procedure to encrypt/decrypt has to be as simple as possible, please no IVs.
I was struggling with the same problem this week but in the opposite way (PHP encrypts -> NodeJS decrypts) and had managed to get this snippet working:
aes256cbc.js
var crypto = require('crypto');
var encrypt = function (plain_text, encryptionMethod, secret, iv) {
var encryptor = crypto.createCipheriv(encryptionMethod, secret, iv);
return encryptor.update(plain_text, 'utf8', 'base64') + encryptor.final('base64');
};
var decrypt = function (encryptedMessage, encryptionMethod, secret, iv) {
var decryptor = crypto.createDecipheriv(encryptionMethod, secret, iv);
return decryptor.update(encryptedMessage, 'base64', 'utf8') + decryptor.final('utf8');
};
var textToEncrypt = new Date().toISOString().substr(0,19) + '|My super secret information.';
var encryptionMethod = 'AES-256-CBC';
var secret = "My32charPasswordAndInitVectorStr"; //must be 32 char length
var iv = secret.substr(0,16);
var encryptedMessage = encrypt(textToEncrypt, encryptionMethod, secret, iv);
var decryptedMessage = decrypt(encryptedMessage, encryptionMethod, secret, iv);
console.log(encryptedMessage);
console.log(decryptedMessage);
aes256cbc.php
<?php
date_default_timezone_set('UTC');
$textToEncrypt = substr(date('c'),0,19) . "|My super secret information.";
$encryptionMethod = "AES-256-CBC";
$secret = "My32charPasswordAndInitVectorStr"; //must be 32 char length
$iv = substr($secret, 0, 16);
$encryptedMessage = openssl_encrypt($textToEncrypt, $encryptionMethod, $secret,0,$iv);
$decryptedMessage = openssl_decrypt($encryptedMessage, $encryptionMethod, $secret,0,$iv);
echo "$encryptedMessage\n";
echo "$decryptedMessage\n";
?>
The secret here to avoid falling in key/iv size/decryption problems is to have the secret of exactly 32 characters length and 16 for the IV.
Also, it is VERY important to use 'base64' and 'utf8' in NodeJS since these are the defaults in PHP.
Here are some sample runs:
$ node aes256cbc.js && php aes256cbc.php
zra3FX4iyCc7qPc1dZs+G3ZQ40f5bSw8P9n5OtWl1t86nV5Qfh4zNRPFbsciyyHyU3Qi4Ga1oTiTwzrPIZQXLw==
2015-01-27T18:29:12|My super secret information.
zra3FX4iyCc7qPc1dZs+G3ZQ40f5bSw8P9n5OtWl1t86nV5Qfh4zNRPFbsciyyHyU3Qi4Ga1oTiTwzrPIZQXLw==
2015-01-27T18:29:12|My super secret information.
$ node aes256cbc.js && php aes256cbc.php
zra3FX4iyCc7qPc1dZs+G6B6+8aavHNc/Ymv9L6Omod8Di3tMbvOa2B7O2Yiyoutm9fy9l0G+P5VJT9z2qNESA==
2015-01-27T18:29:15|My super secret information.
zra3FX4iyCc7qPc1dZs+G6B6+8aavHNc/Ymv9L6Omod8Di3tMbvOa2B7O2Yiyoutm9fy9l0G+P5VJT9z2qNESA==
2015-01-27T18:29:15|My super secret information.
$ node aes256cbc.js && php aes256cbc.php
zra3FX4iyCc7qPc1dZs+G4oD1Fr5yLByON6QDE56UOqP6kkfGJzpyH6TbwZYX2oGlh2JGv+aHYUMh0qQnAj/uw==
2015-01-27T18:29:29|My super secret information.
zra3FX4iyCc7qPc1dZs+G4oD1Fr5yLByON6QDE56UOqP6kkfGJzpyH6TbwZYX2oGlh2JGv+aHYUMh0qQnAj/uw==
2015-01-27T18:29:29|My super secret information.
$ node aes256cbc.js && php aes256cbc.php
zra3FX4iyCc7qPc1dZs+G5OVCbCaUy8a0LLF+Bn8UT4X3nYbtynO0Zt2mvXnnli9dRxrxMw43uWnkh8MIwVHXA==
2015-01-27T18:29:31|My super secret information.
zra3FX4iyCc7qPc1dZs+G5OVCbCaUy8a0LLF+Bn8UT4X3nYbtynO0Zt2mvXnnli9dRxrxMw43uWnkh8MIwVHXA==
2015-01-27T18:29:31|My super secret information.
$ node aes256cbc.js && php aes256cbc.php
fdsqSyHBJjlwD0jYfOUZM2FrONG6Fk5d7FOItYEdbnaZIhhmg/apa8/jPwKFkDXD9eNqWC3w0JzY5wjtZADiBA==
2015-01-27T18:30:08|My super secret information.
fdsqSyHBJjlwD0jYfOUZM2FrONG6Fk5d7FOItYEdbnaZIhhmg/apa8/jPwKFkDXD9eNqWC3w0JzY5wjtZADiBA==
2015-01-27T18:30:08|My super secret information.
$ node aes256cbc.js && php aes256cbc.php
fdsqSyHBJjlwD0jYfOUZM4SRfi6jG5EoDFEF6d9xCIyluXSiMaKlhd89ovpeOz/YyEIlPbYR4ly00gf6hWfKHw==
2015-01-27T18:30:45|My super secret information.
fdsqSyHBJjlwD0jYfOUZM4SRfi6jG5EoDFEF6d9xCIyluXSiMaKlhd89ovpeOz/YyEIlPbYR4ly00gf6hWfKHw==
2015-01-27T18:30:45|My super secret information.
NOTE:
I use a "timestamp|message" format to avoid man in the middle attacks. For example, if the encrypted message contains an ID to be authenticated, the MitM could capture the message and re-send it every time he wants to re-authenticate.
Therefore, I could check the timestamp on the encrypted message to be within a little time interval. This way, the same message is encrypted differently each second because of the timestamp, and could not be used out of this fixed time interval.
EDIT:
Here I was misusing the Initialization Vector (IV).
As #ArtjomB. explained, the IV should be the first part of the encrypted message, and also it should be a random value.
It's also recommended to use a hmac value in a HTTP Header (x-hmac: *value*) in order to validate that the message was originated from a valid source (but this does not address the "re-send" message issue previously described).
Here's the improved version, including the hmac for php and node and the IV as a part of the encrypted message:
aes256cbc.js (v2)
var crypto = require('crypto');
var encrypt = function (message, method, secret, hmac) {
//var iv = crypto.randomBytes(16).toString('hex').substr(0,16); //use this in production
var iv = secret.substr(0,16); //using this for testing purposes (to have the same encryption IV in PHP and Node encryptors)
var encryptor = crypto.createCipheriv(method, secret, iv);
var encrypted = new Buffer(iv).toString('base64') + encryptor.update(message, 'utf8', 'base64') + encryptor.final('base64');
hmac.value = crypto.createHmac('md5', secret).update(encrypted).digest('hex');
return encrypted;
};
var decrypt = function (encrypted, method, secret, hmac) {
if (crypto.createHmac('md5', secret).update(encrypted).digest('hex') == hmac.value) {
var iv = new Buffer(encrypted.substr(0, 24), 'base64').toString();
var decryptor = crypto.createDecipheriv(method, secret, iv);
return decryptor.update(encrypted.substr(24), 'base64', 'utf8') + decryptor.final('utf8');
}
};
var encryptWithTSValidation = function (message, method, secret, hmac) {
var messageTS = new Date().toISOString().substr(0,19) + message;
return encrypt(messageTS, method, secret, hmac);
}
var decryptWithTSValidation = function (encrypted, method, secret, hmac, intervalThreshold) {
var decrypted = decrypt(encrypted, method, secret, hmac);
var now = new Date();
var year = parseInt(decrypted.substr(0,4)), month = parseInt(decrypted.substr(5,2)) - 1,
day = parseInt(decrypted.substr(8,2)), hour = parseInt(decrypted.substr(11,2)),
minute = parseInt(decrypted.substr(14,2)), second = parseInt(decrypted.substr(17,2));
var msgDate = new Date(Date.UTC(year, month, day, hour, minute, second))
if (Math.round((now - msgDate) / 1000) <= intervalThreshold) {
return decrypted.substr(19);
}
}
var message = 'My super secret information.';
var method = 'AES-256-CBC';
var secret = "My32charPasswordAndInitVectorStr"; //must be 32 char length
var hmac = {};
//var encrypted = encrypt(message, method, secret, hmac);
//var decrypted = decrypt(encrypted, method, secret, hmac);
var encrypted = encryptWithTSValidation(message, method, secret, hmac);
var decrypted = decryptWithTSValidation(encrypted, method, secret, hmac, 60*60*12); //60*60m*12=12h
console.log("Use HTTP header 'x-hmac: " + hmac.value + "' for validating against MitM-attacks.");
console.log("Encrypted: " + encrypted);
console.log("Decrypted: " + decrypted);
Note that crypto.createHmac(...).digest('hex') is digested with hex. This is the default in PHP for hmac.
aes256cbc.php (v2)
<?php
function encrypt ($message, $method, $secret, &$hmac) {
//$iv = substr(bin2hex(openssl_random_pseudo_bytes(16)),0,16); //use this in production
$iv = substr($secret, 0, 16); //using this for testing purposes (to have the same encryption IV in PHP and Node encryptors)
$encrypted = base64_encode($iv) . openssl_encrypt($message, $method, $secret, 0, $iv);
$hmac = hash_hmac('md5', $encrypted, $secret);
return $encrypted;
}
function decrypt ($encrypted, $method, $secret, $hmac) {
if (hash_hmac('md5', $encrypted, $secret) == $hmac) {
$iv = base64_decode(substr($encrypted, 0, 24));
return openssl_decrypt(substr($encrypted, 24), $method, $secret, 0, $iv);
}
}
function encryptWithTSValidation ($message, $method, $secret, &$hmac) {
date_default_timezone_set('UTC');
$message = substr(date('c'),0,19) . "$message";
return encrypt($message, $method, $secret, $hmac);
}
function decryptWithTSValidation ($encrypted, $method, $secret, $hmac, $intervalThreshold) {
$decrypted = decrypt($encrypted, $method, $secret, $hmac);
$now = new DateTime();
$msgDate = new DateTime(str_replace("T"," ",substr($decrypted,0,19)));
if (($now->getTimestamp() - $msgDate->getTimestamp()) <= $intervalThreshold) {
return substr($decrypted,19);
}
}
$message = "My super secret information.";
$method = "AES-256-CBC";
$secret = "My32charPasswordAndInitVectorStr"; //must be 32 char length
//$encrypted = encrypt($message, $method, $secret, $hmac);
//$decrypted = decrypt($encrypted, $method, $secret, $hmac);
$encrypted = encryptWithTSValidation($message, $method, $secret, $hmac);
$decrypted = decryptWithTSValidation($encrypted, $method, $secret, $hmac, 60*60*12); //60*60m*12=12h
echo "Use HTTP header 'x-hmac: $hmac' for validating against MitM-attacks.\n";
echo "Encrypted: $encrypted\n";
echo "Decrypted: $decrypted\n";
?>
Here are some sample runs:
$ node aes256cbc.js && php aes256cbc.php
Use HTTP header 'x-hmac: 6862972ef0f463bf48523fc9e334bb42' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==I6cAKeoxeSP5TGgtK59PotB/iG2BUSU8Y6NhAhVabN9UB+ZCTn7q2in4JyLwQiGN
Decrypted: My super secret information.
Use HTTP header 'x-hmac: 6862972ef0f463bf48523fc9e334bb42' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==I6cAKeoxeSP5TGgtK59PotB/iG2BUSU8Y6NhAhVabN9UB+ZCTn7q2in4JyLwQiGN
Decrypted: My super secret information.
$ node aes256cbc.js && php aes256cbc.php
Use HTTP header 'x-hmac: b2e63f216acde938a82142220652cf59' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==YsFRdKzCLuCk7Yg+U+S1CSgYBBR8dkZytORm8xwEDmD9WB1mpqC3XnSrB+wR3/KW
Decrypted: My super secret information.
Use HTTP header 'x-hmac: b2e63f216acde938a82142220652cf59' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==YsFRdKzCLuCk7Yg+U+S1CSgYBBR8dkZytORm8xwEDmD9WB1mpqC3XnSrB+wR3/KW
Decrypted: My super secret information.
$ node aes256cbc.js && php aes256cbc.php
Use HTTP header 'x-hmac: 73181744453d55eb6f81896ffd284cd8' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==YsFRdKzCLuCk7Yg+U+S1CTGik4Lv9PnWuEg5SiADJcdKX1to0LrNKmuCiYIweBAZ
Decrypted: My super secret information.
Use HTTP header 'x-hmac: 73181744453d55eb6f81896ffd284cd8' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==YsFRdKzCLuCk7Yg+U+S1CTGik4Lv9PnWuEg5SiADJcdKX1to0LrNKmuCiYIweBAZ
Decrypted: My super secret information.
$ node aes256cbc.js && php aes256cbc.php
Use HTTP header 'x-hmac: 5372ecca442d65f582866cf3b24cb2b6' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==YsFRdKzCLuCk7Yg+U+S1CYEITF6aozBNp7bA54qY0Ugg9v6ktwoH6nqRyatkFqy8
Decrypted: My super secret information.
Use HTTP header 'x-hmac: 5372ecca442d65f582866cf3b24cb2b6' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==YsFRdKzCLuCk7Yg+U+S1CYEITF6aozBNp7bA54qY0Ugg9v6ktwoH6nqRyatkFqy8
Decrypted: My super secret information.
Last but not least, if you don't have openssl mod installed in php, you can use mcrypt instead with rijndael128 and pkcs7 padding (source) like this:
aes256cbc-mcrypt.php (v2)
<?php
function pkcs7pad($message) {
$padding = 16 - (strlen($message) % 16);
return $message . str_repeat(chr($padding), $padding);
}
function pkcs7unpad($message) {
$padding = ord(substr($message, -1)); //get last char and transform it to Int
return substr($message, 0, -$padding); //remove the last 'padding' string
}
function encrypt ($message, $method, $secret, &$hmac) {
//$iv = substr(bin2hex(mcrypt_create_iv(mcrypt_get_iv_size($method, MCRYPT_MODE_CBC), MCRYPT_DEV_URANDOM)),0,16); //use this in production
$iv = substr($secret, 0, 16); //using this for testing purposes (to have the same encryption IV in PHP and Node encryptors)
$message = pkcs7pad($message);
$encrypted = base64_encode($iv) . base64_encode(mcrypt_encrypt($method, $secret, $message, MCRYPT_MODE_CBC, $iv));
$hmac = hash_hmac('md5', $encrypted, $secret);
return $encrypted;
}
function decrypt ($encrypted, $method, $secret, $hmac) {
if (hash_hmac('md5', $encrypted, $secret) == $hmac) {
$iv = base64_decode(substr($encrypted, 0, 24));
return pkcs7unpad(mcrypt_decrypt($method, $secret , base64_decode(substr($encrypted, 24)) , MCRYPT_MODE_CBC, $iv));
}
}
function encryptWithTSValidation ($message, $method, $secret, &$hmac) {
date_default_timezone_set('UTC');
$message = substr(date('c'),0,19) . "$message";
return encrypt($message, $method, $secret, $hmac);
}
function decryptWithTSValidation ($encrypted, $method, $secret, $hmac, $intervalThreshold) {
$decrypted = decrypt($encrypted, $method, $secret, $hmac);
$now = new DateTime();
//echo "Decrypted: $decrypted\n";
$msgDate = new DateTime(str_replace("T"," ",substr($decrypted,0,19)));
if (($now->getTimestamp() - $msgDate->getTimestamp()) <= $intervalThreshold) {
return substr($decrypted,19);
}
}
$message = "My super secret information.";
$method = MCRYPT_RIJNDAEL_128;
$secret = "My32charPasswordAndInitVectorStr"; //must be 32 char length
//$encrypted = encrypt($message, $method, $secret, $hmac);
//$decrypted = decrypt($encrypted, $method, $secret, $hmac);
$encrypted = encryptWithTSValidation($message, $method, $secret, $hmac);
$decrypted = decryptWithTSValidation($encrypted, $method, $secret, $hmac, 60*60*12); //60*60m*12=12h
echo "Use HTTP header 'x-hmac: $hmac' for validating against MitM-attacks.\n";
echo "Encrypted: $encrypted\n";
echo "Decrypted: $decrypted\n";
?>
Ofcourse, some tests next:
$ php aes256cbc-mcrypt.php && node aes256cbc.js
Use HTTP header 'x-hmac: 801282a9ed6b2d5bd2254140d7a17582' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==ipQ+Yah8xoF0C6yjCJr8v9IyatyGeNT2yebrpJZ5xH73H5fFcV1zhqhRGwM0ToGU
Decrypted: My super secret information.
Use HTTP header 'x-hmac: 801282a9ed6b2d5bd2254140d7a17582' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==ipQ+Yah8xoF0C6yjCJr8v9IyatyGeNT2yebrpJZ5xH73H5fFcV1zhqhRGwM0ToGU
Decrypted: My super secret information.
$ php aes256cbc-mcrypt.php && node aes256cbc.js
Use HTTP header 'x-hmac: 0ab2bc83108e1e250f6ecd483cd65329' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==ipQ+Yah8xoF0C6yjCJr8v79P+j4YUl8ln8eu7FDqEdbxMe1Z7BvW8iVUN1qFCiHM
Decrypted: My super secret information.
Use HTTP header 'x-hmac: 0ab2bc83108e1e250f6ecd483cd65329' for validating against MitM-attacks.
Encrypted: YjE0ZzNyMHNwVm50MGswbQ==ipQ+Yah8xoF0C6yjCJr8v79P+j4YUl8ln8eu7FDqEdbxMe1Z7BvW8iVUN1qFCiHM
Decrypted: My super secret information.
When dealing with symmetric encryption like this, the first step is realize that it's probably going to be a huge pain in the rear - I've never, ever had it work right away, even when I was copy pasting my own code. This is mainly because encryption and decryption methods are, by design, utterly unforgiving and rarely give useful error messages. A single null character, carriage return, line feed, or dynamically converted type can silently blow the whole process up.
Knowing this, progress stepwise. I suggest the following:
First, get PHP alone working. Pass in sample text, encrypt it, immediately decrypt it, and compare it with strict equality to the original clear text variable. Are they perfectly the same? Output both, as well - are they the same type and appear perfectly unmolested? Watch out for non-printed characters - check the length and character encoding too!
Now, do the above with one more sample text that is 1 character more or less than the previous one. This debugs block size/zero-padding issues - it matters.
If that's working - and it rarely does right away, for hard to predict reasons, continue to Node.js.
In Node.js, do the same thing as you did in PHP, even if it seems like wasted effort - for extra reasons that will be obvious in a moment. Encrypt and decrypt, all together, in your Node.js. Does it work with all the same provisos given above?
Once that is done, here comes the 'fun' part: using the same encryption methods independently in Node.js and PHP, have them both output to you the 'final' ready-to-transmit cryptext that both produced.
If all is well, they should be perfectly, exactly the same. If they aren't, you have a problem with your encryption implementations and methods not being compatible between systems. Some setting is wrong or conflicting (perhaps with zero padding or a host of other possibilities, or IV, etc), or you need to try a different implementation.
If I had to guess blindly, I'd say there is an issue with the base64 encoding and decoding (it's most commonly what goes wrong). Things tend to get done twice, because it can be tricky to debug binary data types in web applications (through a browser). Sometimes things are being encoded twice but only decoded once, or one implementation will 'helpfully' encode/decode something automatically without being clear that's what it's doing, etc.
It's also possible it's a zero-padding implementation issue between Node and PHP, as suggested here: AES encrypt in Node.js Decrypt in PHP. Fail.
These last two issues are strongly suggested by your error codes. The encryption methods predict block sizes of precise length, and if they are off then that signals corruption of the data being passed to the functions - which happens if a single extra character slipped in, or if encoding is handled differently, etc.
If you step through each of the above one at a time, assuring yourself you can't rush and must check every painstaking tiny little step of the process, it should be much more clear where exactly things are going wrong, and then that can be troubleshooted.
This is codeiginiter framework default decryption equivalent js script (aes128cbc), hope this will help someone.
let crypto = require("crypto");
let secret = 'xxxxxxxxxxxxxxxxxxxx';
// ikm is initial keying material
var hkdf = function (hashAlg, salt, ikm) {
this.hashAlg = hashAlg;
// create the hash alg to see if it exists and get its length
var hash = crypto.createHash(this.hashAlg);
this.hashLength = hash.digest().length;
this.salt = salt || new Buffer(this.hashLength).fill(0).toString();
this.ikm = ikm;
// now we compute the PRK
var hmac = crypto.createHmac(this.hashAlg, this.salt);
hmac.update(this.ikm);
this.prk = hmac.digest();
};
hkdf.prototype = {
derive: function(info, size, cb) {
var prev = new Buffer(0);
var output;
var buffers = [];
var num_blocks = Math.ceil(size / this.hashLength);
info = new Buffer(info);
for (var i=0; i<num_blocks; i++) {
var hmac = crypto.createHmac(this.hashAlg, this.prk);
hmac.update(prev);
hmac.update(info);
hmac.update(new Buffer([i + 1]));
prev = hmac.digest();
buffers.push(prev);
}
output = Buffer.concat(buffers, size);
return output;
}
};
function decrypt(code)
{
if (typeof code !== 'string')
return false;
code = code.substring(128);
var buff = new Buffer(code, 'base64');
var iv = buff.slice(0, 16);
var encyptedText = buff.slice(16).toString('base64');
var _hkdf = new hkdf('sha512', null, secret);
var derive_key = _hkdf.derive('encryption', secret.length);
var key = derive_key.slice(0, 16);
var decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
var result = decipher.update(encyptedText, 'base64');
result += decipher.final();
return result.replace(/[']/g, '');
}
Based on #inieto answer i created two simple classes for encryption and decryption one for php and one for typescript that are easy to use.
https://github.com/5imun/Endecryptor
Just include/import them and you are ready to go.
Example for php:
#Include Endecryptor before using it
$secret = 'hxXxVEVNa3S6OQdgltNoDkbZ10b0MkQV';
$method = 'AES-256-CBC';
$valid_request_TS_interval = 100; # in seconds
$endecryptor = new Endecryptor($secret, $method, $valid_request_TS_interval );
$original_message = '{"test":"Hello, World!"}';
$endecryptor->encryptWithTS($original_message);
echo "Encrypted message: $endecryptor->temp_encrypted\n";
echo "Encrypted message hmac: $endecryptor->temp_hmac\n";
if ( $endecryptor->decryptAndValidateTS( $endecryptor->temp_encrypted, $endecryptor->temp_hmac ) ) {
echo "Original message: $original_message\n";
echo "Decrypted message: $endecryptor->temp_decrypted\n";
} else {
echo 'Description was not successful';
}
Result:
Encrypted message: MjliMmM5NzljYWQ0YjA4Mw==ULxsH1juCOrieEkiRpHY1CMkKtvSvB5X+b8E9cOcQ7yYt+SUKj+I6FjaGvYjEldt
Encrypted message: hmac: 5aa8f1b268dfef0dc2f48f1a25204e82
Original message: {"test":"Hello, World!"}
Decrypted message: {"test":"Hello, World!"}

Categories