How to properly encode strings so to decrypt with CryptoJs in NodeJS? - node.js

I am working out a custom hybrid encryption system. I've got symmetric encryption & asymmetric encryption & decryption all handled server-side. All I need to work out now is symmetric decryption.
I got some trouble because my client is sending symmetric key, iv & data all in string format (after asymmetric decryption), but CryptoJS is very touchy with it's encoding. It's also very confusing and vague as far as documentation goes- at least for a relatively new developer. I just can't figure out what encoding CryptoJS wants for each argument. I figure I should have guessed right by now, but no.
Docs
Some help I've gotten previously
I'm requesting help getting the encoding right so that I can decrypt with the following. And thanks a lot for any assistance.
Example of data after asymmetric decryption as per below (throw away keys):
symmetricKey: bDKJVr5wFtQZaPrs4ZoMkP2RjtaYpXo5HHKbzrNELs8=,
symmetricNonce: Z8q66bFkbEqQiVbrUrts+A==,
dataToReceive: "hX/BFO7b+6eYV1zt3+hu3o5g61PFB4V3myyU8tI3W7I="
exports.transportSecurityDecryption = async function mmTransportSecurityDecryption(dataToReceive, keys) {
const JSEncrypt = require('node-jsencrypt');
const CryptoJS = require("crypto-js");
// Asymmetrically decrypt symmetric cypher data with server private key
const privateKeyQuery = new Parse.Query("ServerPrivateKey");
const keyQueryResult = await privateKeyQuery.find({useMasterKey: true});
const object = keyQueryResult[0];
const serverPrivateKey = object.get("key");
const crypt = new JSEncrypt();
crypt.setPrivateKey(serverPrivateKey);
let decryptedDataString = crypt.decrypt(keys);
let decryptedData = JSON.parse(decryptedDataString);
// Symmetrically decrypt transit data
let symmetricKey = decryptedData.symmetricKey;
let symmetricNonce = decryptedData.symmetricNonce;
// Works perfectly till here <---
var decrypted = CryptoJS.AES.decrypt(
CryptoJS.enc.Hex.parse(dataToReceive),
CryptoJS.enc.Utf8.parse(symmetricKey),
{iv: CryptoJS.enc.Hex.parse(symmetricNonce)}
);
return decrypted.toString(CryptoJS.enc.Utf8);
}

You are using the wrong encoders for data, key and IV. All three are Base64 encoded (and not hex or Utf8). So apply the Base64 encoder.
The ciphertext must be passed to CryptoJS.AES.decrypt() as a CipherParams object or alternatively Base64 encoded, which is implicitly converted to a CipherParams object.
When both are fixed, the plain text is: "[\"001\",\"001\"]".
var symmetricKey = "bDKJVr5wFtQZaPrs4ZoMkP2RjtaYpXo5HHKbzrNELs8="
var symmetricNonce = "Z8q66bFkbEqQiVbrUrts+A=="
var dataToReceive = "hX/BFO7b+6eYV1zt3+hu3o5g61PFB4V3myyU8tI3W7I="
var decrypted = CryptoJS.AES.decrypt(
dataToReceive, // pass Base64 encoded
//{ciphertext: CryptoJS.enc.Base64.parse(dataToReceive)}, // pass as CipherParams object, works also
CryptoJS.enc.Base64.parse(symmetricKey),
{iv: CryptoJS.enc.Base64.parse(symmetricNonce)}
);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

Related

nodejs recover createCipher data with createCipheriv

I have some encrypted data in my database
I did it few years ago using crypto.createCipher
const cipher = crypto.createCipher('aes192', password);
As createCipher and createDecipher is deprecated, I would like to change to createCipheriv and createDecipheriv. The problem is that the data I have in my database are encoded without iv.
Is it possible to decode with createDecipheriv data encoded with createDecipher and to generate the same secret with createCipher and createCipheriv.
I tried setting the iv to null but not working
Thanks, because the database migration is an heavy work !
I tried setting the iv to null but not working
This is because this method didn’t allow for passing an initialization vector (IV), and instead derived the IV from the key using the OpenSSL EVP_BytesToKey derivation function, using a null salt meaning that the IV would be deterministic for a given key which is an issue for ciphers with counter mode like CTR, GCM and CCM.
Looking at your code:
const cipher = crypto.createCipher('aes192', password);
If you want to make this code backwards compatible, you need to call OpenSSL’s EVP_BytesToKey function yourself, typically through evp_bytestokey module which makes it available in JS userland.
Is it possible to decode with createDecipheriv data encoded with createDecipher and to generate the same secret with createCipher and createCipheriv.
Yes, you can. check out my example code here:
const crypto = require('crypto');
const EVP_BytesToKey = require('evp_bytestokey')
const ALGO = 'aes192';
const password = 'Your_Password_Here';
const KEY_SIZE = 24;
function decrypt_legacy_using_IV(text) {
const result = EVP_BytesToKey(
password,
null,
KEY_SIZE * 8, // byte to bit size
16
)
let decipher = crypto.createDecipheriv(ALGO, result.key, result.iv);
let decrypted = decipher.update(text, 'hex','utf8') + decipher.final('utf8');
return decrypted.toString();
}
function encrypt_legacy_using_IV(text) {
const result = EVP_BytesToKey(
password,
null,
KEY_SIZE * 8, // byte to bit size
16
)
var cipher = crypto.createCipheriv(ALGO, result.key, result.iv);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
return encrypted.toString();
}
For complete running example, clone node-snippets and run node apogee-legacy-crypto-cipheriv.js.
However the reason this function is deprecated in the first place is because you shouldn’t use it, and instead use a random unpredictable IV, which requires you to change your code to something like this:
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes192', password, iv)
Here, for AES-192 in CBC mode (aes192 being aliased to AES-192-CBC by OpenSSL), the IV size is expected to be the same as the block size, which is always 16 bytes.
In order to decrypt the message, you will need the IV as well. Typically you’d store the IV together with the message, as the important part is for the IV to not be predictable ahead of time.

Can the value from node crypto.createCipheriv('aes-256-gcm', ...).getAuthKey() be public?

I'm having trouble finding some information. Does anyone know if the value returned from cipher.getAuthTag() (--> returns MAC) can be publicly visible?
TL;DR
Can a message authentication code be publicly visible, or does this need to be kept secret like a password?
Some background, I am trying to encrypt a file. I found this stackoverflow question and answer that helped me get started. https://stackoverflow.com/a/27345933/11070228
After doing some research in the nodejs documentation, I found that the answer uses a deprecated function. createCipher. The new function to use should be createCipheriv.
So, to use the new createCipheriv, I used the documentation to write a new encryption and decryption function, similar to the one in the post using the new createCipheriv function. After writing the decryption function, I got an error that was
Error: Unsupported state or unable to authenticate data
After googling that issue, it led me here to this github post. In a nutshell, it said that the authTag generated with the cipher is needed to decrypt the file.
I did not know what this authTag was, and neither did anyone I knew. So I started googling that and it let me to this blogpost. It states
The authTag is the message authentication code (MAC) calculated during the encryption.
And here is a wikipedia article on what a message authentication code is.
So. Here is my question. Can a message authentication code be publicly visible, or does this need to be kept secret like a password?
My code, not as relevant, but might help someone create the encryption and decryption using createCipheriv and createDecipheriv.
Encryption
const crypto = require('crypto');
const fs = require('fs');
// const iv = crypto.randomBytes(32).toString('hex');
// EDIT - based on #President James K. Polk. The initialization vector should be 12 bytes long
// const iv = crypto.randomBytes(6).toString('hex');
// EDIT - based on #dsprenkels. I misunderstood #President James K. Polk
const iv = crypto.randomBytes(12).toString('hex');
const privateKey = 'private key that is 32 byte long';
const cipher = crypto.createCipheriv('aes-256-gcm', privateKey, iv);
const filename = 'somefile.txt';
const encFilename = 'somefile.txt.enc';
const unencryptedInput = fs.createReadStream(filename);
const encryptedOutput = fs.createWriteStream(encFilename);
unencryptedInput.pipe(cipher).pipe(encryptedOutput);
encryptedOutput.on('finish', () => {
const authTagAsHex = cipher.getAuthTag().toString('hex'); // <-- can this be public
console.log(authTagAsHex);
});
Decryption
const crypto = require('crypto');
const fs = require('fs');
// const publicIV = 'same iv generated during encryption crypto.randomBytes(32).toString("hex")';
// EDIT - based on #President James K. Polk. The initialization vector should be 12 bytes long
// const publicIV = 'same iv generated during encryption crypto.randomBytes(6).toString("hex")';
// EDIT - based on #dsprenkels. I misunderstood #President James K. Polk
const publicIV = 'same iv generated during encryption crypto.randomBytes(12).toString("hex")';
const authTag = 'same authKey generated from cipher.getAuthTag().toString("hex")';
const privateKey = 'private key that is 32 byte long';
const decipher = crypto.createDecipheriv('aes-256-gcm', privateKey, publicIV);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
const filename = 'somefile.txt';
const encFilename = 'somefile.txt.enc';
const readStream = fs.createReadStream(encFilename);
const writeStream = fs.createWriteStream(filename);
readStream.pipe(decipher).pipe(writeStream);
Yes. The MAC is considered public.
In general, message authentication codes are considered public. A message authentication code authenticates the (encrypted) message under the key that you provided. On other words, it is used by the receiver to check if the ciphertext did not change during transmission. In your situation, as long as the key remains secret, the attacker does not have any use for the MAC.
The MAC is normally put next to the ciphertext when the ciphertext is stored (just as the IV).
By the way, in your case you are were randomly generating the IV. That is fine, but beware that the amount of messages that can be safely encrypted under the same key is quite small. If an IV is used for multiple message (even once!) the complete security of this scheme breaks down. Really, you probably want this:
const iv = crypto.randomBytes(12);

How to decrypt a value in frontend which is encrypted in the backend (nodejs)?

Backend developers have encrypted a value in nodejs using crypto module. The code is shown below:
const _encrypt = async function(text){
var cipher = crypto.createCipher('aes-256-cbc','123|a123123123123123#&12')
var crypted = cipher.update(text,'utf8','hex')
crypted += cipher.final('hex');
console.log("in generic function....encrpted val", crypted)
return crypted;
}
I need to decrypt this value in the front end (Angular). So I tried decrypting like below:
let bytes = CryptoJS.AES.decrypt("e0912c26238f29604f5998fa1fbc78f6",'123|a123123123123123#&12');
if(bytes.toString()){
let m = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
console.log("data ",m);
}
using hardcoded value. But Im getting Error: Malformed UTF-8 data error. Can anybody please tell me how to decrypt this in angular side?
This is a tricky enough one.. the crypto.createCipher function creates a key and IV from the password you provide (See the createCipher documentation for details).
This is implemented using the OpenSSL function EVP_BytesToKey.
A JavaScript implementation is available here: openssl-file.. we'll use this to get a key and IV from the password.
So there are two steps here:
Get a key and IV from your password.
Use these with Crypto.js to decode your encoded string.
Step 1: Get key and IV (Run in Node.js )
const EVP_BytesToKey = require('openssl-file').EVP_BytesToKey;
const result = EVP_BytesToKey(
'123|a123123123123123#&12',
null,
32,
'MD5',
16
);
console.log('key:', result.key.toString('hex'));
console.log('iv:', result.iv.toString('hex'));
Step 2: Decrypt string:
const encryptedValues = ['e0912c26238f29604f5998fa1fbc78f6', '0888e0558c3bce328cd7cda17e045769'];
// The results of putting the password '123|a123123123123123#&12' through EVP_BytesToKey
const key = '18bcd0b950de300fb873788958fde988fec9b478a936a3061575b16f79977d5b';
const IV = '2e11075e7b38fa20e192bc7089ccf32b';
for(let encrypted of encryptedValues) {
const decrypted = CryptoJS.AES.decrypt({ ciphertext: CryptoJS.enc.Hex.parse(encrypted) }, CryptoJS.enc.Hex.parse(key), {
iv: CryptoJS.enc.Hex.parse(IV),
mode: CryptoJS.mode.CBC
});
console.log('Ciphertext:', encrypted);
console.log('Plain text:', decrypted.toString(CryptoJS.enc.Utf8));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js"></script>
Note that if you change the password you need to generate a new key and iv using EVP_BytesToKey.
I should note that createCipher is now deprecated, so use with caution. The same applies to EVP_BytesToKey.

NodeJS | Crypto Encryption is not producing correct results

I have a tricky problem to resolve. Not sure how to explain it correctly but will try my best. So here is what I am trying to do: I am trying to use a 3rd Party API, which wants me to encrypt a value and submits it. I successfully achieved it through C# code using the following block:
public string Encrypt(byte[] dataToEncrypt, byte[] keyBytes)
{
AesManaged tdes = new AesManaged();
tdes.KeySize = 256;
tdes.BlockSize = 128;
tdes.Key = keyBytes;
tdes.Mode = CipherMode.ECB;
tdes.Padding = PaddingMode.PKCS7;
ICryptoTransform crypt = tdes.CreateEncryptor();
byte[] cipher = crypt.TransformFinalBlock(dataToEncrypt, 0, dataToEncrypt.Length);
tdes.Clear();
return Convert.ToBase64String(cipher, 0, cipher.Length);
}
Now, I am trying to achieve the same in Node. I wrote the following function.
encrypt(buffer){
var buffbytes = new Buffer('my app key goes here to be used as password','utf8'); //converts the app key into buffer stream
return this.encrypt_key(new Buffer(buffer,'utf8'), buffbytes);
},
encrypt_key(buffer, keybytes){
var cipher = crypto.createCipher('aes-128-ecb',keybytes);
var crypted = cipher.update(buffer,'utf8','base64');
crypted = crypted+ cipher.final('base64');
return crypted;
},
This encryption code works fine. It encrypts it fine, but it doesn't encrypt it similar to what c# code does. When I take the encrypted text from C# code, and inject the encrypted result into the API call, it passes through fine, but when I use my encrypted result into the API call, it fails mentioning that the format of my key is incorrect.
I would like to know if these code blocks are same or not. I assume it is same, because both code using 128 bit AES, ECB Cipher and default padding for Crypto Node module is PKCS5 which is same as PKCS7 for 128 bit encryption. Please Help!
Edit: 9/19/2017
Fixed as per #smarx solution:
encrypt(buffer){
var buffbytes = new Buffer(helper.Constants.AppKey,'utf8'); //converts the app key into buffer stream
return this.encrypt_key(new Buffer(buffer,'utf8'), helper.Constants.AppKey);
},
encrypt_key(buffer, key){
var cipher = crypto.createCipheriv('aes-256-ecb',key,new Buffer(0));
var crypted = cipher.update(buffer,'utf8','base64');
crypted = crypted+ cipher.final('base64');
console.log('printed: ', crypted);
return crypted;
},
In your Node.js code, you're using the wrong cipher algorithm. Use aes-256-ecb, since you're using a 256-bit key. Also, be sure to use createCipheriv, since createCipher expects a password from which it derives an encryption key.
One-line fix:
const cipher = crypto.createCipheriv('aes-256-ecb', key, new Buffer(0));
The below two programs produce identical output (Q9VZ73VKhW8ZvdcBzm05mw==).
C#:
var key = System.Text.Encoding.UTF8.GetBytes("abcdefghijklmnopqrstuvwxyz123456");
var data = System.Text.Encoding.UTF8.GetBytes("Hello, World!");
var aes = new AesManaged {
Key = key,
Mode = CipherMode.ECB,
};
Console.WriteLine(Convert.ToBase64String(
aes.CreateEncryptor().TransformFinalBlock(data, 0, data.Length)));
Node.js:
const crypto = require('crypto');
const key = 'abcdefghijklmnopqrstuvwxyz123456';
const data = 'Hello, World!';
const cipher = crypto.createCipheriv('aes-256-ecb', key, new Buffer(0));
console.log(cipher.update(data, 'utf-8', 'base64') + cipher.final('base64'));

Decryption returns empty result - nodejs crypto

function encrypt() {
const iv = '3af545da025d5b07319cd9b2571670ca'
, payload = '01000000000000000000000000000000'
, key = 'c1602e4b57602e48d9a3ffc1b578d9a3';
const cipher = crypto.createCipheriv('aes128', new Buffer(key, 'hex'), new Buffer(iv, 'hex'));
const encryptedPayload = cipher.update(new Buffer(payload, 'hex'));
let encryptedPayloadHex = encryptedPayload.toString('hex');
console.log(encryptedPayloadHex); // returns 'ae47475617f38b4731e8096afa5a59b0'
};
function decrypt() {
const iv = '3af545da025d5b07319cd9b2571670ca'
, key = 'c1602e4b57602e48d9a3ffc1b578d9a3'
, payload = 'ae47475617f38b4731e8096afa5a59b0';
const decipher = crypto.createDecipheriv('aes128', new Buffer(key, 'hex'), new Buffer(iv, 'hex'));
const decryptedPayload = decipher.update(new Buffer(payload, 'hex'), 'hex', 'hex');
console.log(decryptedPayload); // returns empty string
// decipher.update(new Buffer(payload, 'hex')) // returns empty buffer
const decryptedPayloadHex = decipher.final('hex'); // returns 'EVP_DecryptFinal_ex:bad decrypt' error
// console.log(decryptedPayloadHex);
};
The decryption result, though, is always empty.
The nodejs docs state that update returns the value as string in given encoding, if provided, otherwise as Buffer. Nevertheless I tried using final as well, but no success.
P.S. In fact, I receive the encryptedPayload value and the iv from external source (they're not encrypted and generated by me), but I decided to test out the encryption (I have the plain payload value) and my encryption returns the same result as the one that I'm receiving externally.
Ok, so the problem turned out to be the padding. I got inspiration from here. I simply added
decipher.setAutoPadding(false);
right after I crete the decipher object.
That is weird though, because padding problems could occur when encryption is done in one language and decryption in another, but should not happen when encryption and decryption are done in the same language (as I did my testing here)... If anyone has comments on the padding issue - please add them, so that future viewers can gain knowledge (as well as me).

Resources