AES CBC nodejs encryption and Java decryption - node.js

NodeJs:
I am trying decrypt text using AES CBC PKCS7 in NodeJs and PKCS5 in java. I am getting error: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
UPDATED
encrypt() {
var key = 'ThirtyTwoBytes3$ThirtyTwoBytes3$';
var iv = CryptoJS.enc.Utf8.parse(CryptoJS.lib.WordArray.random(128 / 8));
let utf8Pass = CryptoJS.enc.Utf8.parse("Hello");
let encVal = CryptoJS.AES.encrypt(utf8Pass.toString(), key, {mode:
CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: iv});
return iv.concat(encVal.ciphertext).toString(CryptoJS.enc.Base64);
}
Java:
byte[] keyB = "ThirtyTwoBytes3$ThirtyTwoBytes3$".getBytes(StandardCharsets.UTF_8);
IvParameterSpec ivParameterSpec = new IvParameterSpec(encryptedText.getBytes(), 0, 16);
SecretKeySpec key = new SecretKeySpec(keyB, "AES");
Cipher aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding");
aesCBC.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
byte[] decryptedData = Base64.getDecoder().decode(encryptedText);
decryptedText = new String(Hex.decodeHex(new String(aesCBC.doFinal(decryptedData), StandardCharsets.UTF_8).toCharArray()));
Fixed IV is working fine
NodeJs
var encKey = CryptoJS.enc.Utf8.parse("ThirtyTwoBytes3$ThirtyTwoBytes3$");
var encKeyIv = CryptoJS.enc.Utf8.parse("$1SixteenBytes6$");
let utf8Pass = CryptoJS.enc.Utf8.parse("Hello");
let encVal = CryptoJS.AES.encrypt(utf8Pass.toString(), encKey, {mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: encKeyIv});
encVal.ciphertext.toString();
Java:
SecretKey key = new SecretKeySpec("ThirtyTwoBytes3$ThirtyTwoBytes3$".getBytes(), "AES");
AlgorithmParameterSpec iv = new IvParameterSpec("$1SixteenBytes6$".getBytes());
byte[] decodeBase64 = Base64.decode(encVal);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key, iv);
decString = new String(Hex.decodeHex(new String(cipher.doFinal(decodeBase64), "UTF-8").toCharArray()));

There are a few issues in the CryptoJS part, apply the following fixes:
const iv = CryptoJS.lib.WordArray.random(128 / 8); // no UTF8 encoding, this corrupts the data
...
encrypted = iv.concat(encrypted.ciphertext).toString(CryptoJS.enc.Base64); // typo in ciphertext; use base64 encoding because of the built-in support in Java
return encrypted; // return the data
In the Java code, IV and ciphertext must be separated, e.g.:
import java.nio.ByteBuffer;
import java.util.Base64;
...
String ivCiphertextB64 = "zuXYG1IbUNsiQYSTBUCv+Rx37EpJTw9SWfhRkL3yw2GncOvBDNU+w6UdB+ovfL2LyiCMYF1ptiXvuWynngc76Q=="; // data from CryptoJS code
byte[] ivCiphertext = Base64.getDecoder().decode(ivCiphertextB64);
ByteBuffer bufIvCiphertext = ByteBuffer.wrap(ivCiphertext);
byte[] iv = new byte[16];
bufIvCiphertext.get(iv);
byte[] ciphertext = new byte[bufIvCiphertext.remaining()];
bufIvCiphertext.get(ciphertext);
...
iv is passed in new IvParameterSpec(iv), ciphertext in aesCBC.doFinal(ciphertext). This decrypts the above ciphertext in The quick brown fox jumps over the lazy dog.
Edit:
Regarding your comment: In the modified CryptoJS code of your question the random IV is still Utf8 encoded. This Utf8 encoding is wrong because it corrupts the random data (s. e.g. here) and needs to be changed as already described above. Below you will find the complete, working CryptoJS code. A ciphertext generated with this code can be decrypted with the modified Java code.
function encrypt() {
const _key = CryptoJS.enc.Utf8.parse('ThirtyTwoBytes3$ThirtyTwoBytes3$');
const iv = CryptoJS.lib.WordArray.random(128 / 8);
let encrypted = CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse('The quick brown fox jumps over the lazy dog'),
_key,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
}
);
encrypted = iv
.concat(encrypted.ciphertext)
.toString(CryptoJS.enc.Base64);
return encrypted;
}
document.getElementById("ct").innerHTML = encrypt();
<p style="font-family:'Courier New', monospace;" id="ct"></p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

Related

Java to Node.js AES/ECB/PKCS5Padding Encryption

I have the following encrypt function in JAVA. I am trying to write the same encryption in Node.js using cipher from crypto. But, the output is not the same. It is using the same key and input.
JAVA
public static String encrypt(String input, String key) {
byte[] crypted = null;
try {
SecretKeySpec skey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, skey);
crypted = cipher.doFinal(input.getBytes());
} catch (Exception e) {
System.out.println(e.toString());
}
String result = new String(Base64.encodeBase64(crypted));
return result.replace("+", "-");
}
Sample output: 0HCkcjWj/PoCZ4ZUFJARs/m4kstigMFk8dQnT0uNhog= (44 characters)
Node.js
encrypt = (input, key) => {
const algorithm = 'aes-128-cbc';
key = crypto.scryptSync(key, 'salt', 16);
const iv = Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv(algorithm, key, iv);
cipher.setAutoPadding(true);
let encrypted = cipher.update(input, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted.replace('+','-');
}
Sample output: ZHtEbAhrIo7vWOjdMNgW6Q== (24 characters)
Thanks in advance.
So that the NodeJS code is functionally identical to the Java code, in the NodeJS code:
ECB mode must be used instead of CBC mode:
const algorithm = 'aes-128-ecb';
...
//const iv = Buffer.alloc(16, 0); // remove
const cipher = crypto.createCipheriv(algorithm, key, null);
Note, however, that ECB doesn't use an IV, is generally insecure and should therefore not be used, [1].
Better alternatives are CBC mode (confidentiality) or GCM mode (confidentiality, authenticity/integrity), [2], [3].
No key derivation function may be applied [4], i.e. the following line must be removed:
key = crypto.scryptSync(key, 'salt', 16);

Node decrypt content with private key and padding

So, I have a content which needs to be decrypted with private key and padding AES/ECB/PKCS5Padding.
I tried many libraries, and many examples, but none of them works. Now, this is the one where I managed to finish to the last steps, but im not sure if there is another library that can do this for me.
var absolutePath = path.resolve('./private.txt');
var privateKey = fs.readFileSync(absolutePath, "utf8");
var buffer = new Buffer(toDecrypt, "base64");
var decrypted = crypto.privateDecrypt(privateKey, buffer);
return decrypted.toString("utf8");
This one throws me error:
0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag
The second example, slighlty different than the first one, but uses padding (thats what I need, I just wanted to try without it to see if it works):
var stringKey = 'BEGIN RSA PRIVATE KEY-----....';
var cipherText = 'ENCRYPTEDTEXT';
// we compute the sha256 of the key
var hash = crypto.createHash("sha256");
hash.update(stringKey, "utf8");
var sha256key = hash.digest();
var keyBuffer = new Buffer(sha256key);
var cipherBuffer = new Buffer(cipherText, 'hex');
var aesDec = crypto.createDecipheriv("aes-256-ecb", keyBuffer, ''); // always use createDecipheriv when the key is passed as raw bytes
var output = aesDec.update(cipherBuffer, 'binary', 'binary');
var final = aesDec.final();
return output + final;
It crashes on the line var final = aesDec.final() and throws error:
digital envelope routines:EVP_DecryptFinal_ex:wrong final block length
Does anybody has knowledge or expirience on how to do this?
We had similar issue.
We receieved encrypted base 64 string from api and needed to decrypt aes key, and then, with decrypted aes key, we needed to decrypt the payload.
What we have done:
var bytes = new Buffer(input, 'base64'); this is the encrypted data from server
var aes = bytes.slice(offset, offset + AES_SIZE); slice byte array to get aes
`var aesString = aes.toString('binary');` convert it to binary string
Use forge library:
var forge = require('node-forge');
var pki = require('node-forge').pki;
// Grab private key from file
var absolutePath = path.resolve('../private-key.txt');
var privateKey = fs.readFileSync(absolutePath, "utf8");
// Generate private key object
var private_key = pki.privateKeyFromPem(privateKey);
var result;
// Decrypt aes key with private key
try {
result = private_key.decrypt(api.apiSecret, 'RSA-OAEP', {
md: forge.md.sha1.create(),
mgf1: {
md: forge.md.sha1.create()
}
});
} catch (err) {
console.error(err.message);
return;
}
// Build byte array from aes key
var base = new Buffer(result, 'binary');
// Generate initialization vector
var iv = forge.random.getBytesSync(api.content.length);
// Create decipher object with AES/ECB/PKCS5 padding
var decipher = forge.cipher.createDecipher('AES-ECB', forge.util.createBuffer(base));
decipher.start({ iv: iv });
// Add content for decrypting
decipher.update(forge.util.createBuffer(api.content));
var result = decipher.finish();
// Get json data from decipher object
var data = decipher.output.data;

Is there a vulnerability if the beginning of the plaintext is known before encryption?

Assuming this is my encrypt and decrypt function using native crypto from node.js.
var algo = 'aes-256-cbc';
var algoSecret = 'mySecret';
var encrypt = function(secret){
var cipher = require('crypto').createCipher(algo,algoSecret);
var crypted = cipher.update(secret,'utf8','hex')
crypted += cipher.final('hex');
return crypted;
}
var decrypt = function(text){
var decipher = require('crypto').createDecipher(algo,algoSecret);
var dec = decipher.update(text,'hex','utf8');
dec += decipher.final('utf8');
return dec;
}
I have to encrypt data of length 20. However, the first 8 chars are always the same and known by everyone. Ex: Always starts with api-key=. Does including or removing the 8 first chars affect the security of the system?
Ex: encrypt('api-key=askjdhaskdhaskd') vs 'api-key=' + encrypt('askjdhaskdhaskd')
Does including or removing the 8 first chars affect the security of the system?
Yes, but only slightly.
CBC mode with a static IV is deterministic, which means that an attacker who only observes ciphertexts can determine if the same plaintext prefix was sent before. Since the first 8 bytes are known to be static, the chance for the next 8 bytes of the first block are much more probable to equal another API key, which does not necessarily match in its entirety. Whether this information is useful to the attacker is a completely different question.
It would be better to always generate an unpredictable (read: random) IV for CBC mode instead of relying on the same IV that is derived from a password. An IV is always 16 bytes or 32 hex-encoded characters long for AES regardless of key size.
Some example code:
var crypto = require('crypto');
var algo = 'aes-256-cbc';
var algoSecret = 'mySecret';
var key = crypto.pbkdf2Sync(algoSecret, 'salt', 1000, 256, 'sha256');
// the key can also be stored in Hex in order to prevent PBKDF2 invocation
var encrypt = function(secret){
var iv = crypto.randomBytes(16);
var cipher = crypto.createCipheriv(algo, key, iv);
var crypted = cipher.update(secret,'utf8','hex')
crypted += cipher.final('hex');
return iv.toString('hex') + crypted;
}
var decrypt = function(text){
var iv = new Buffer(text.slice(0, 32), 'hex');
text = text.slice(32);
var decipher = crypto.createDecipheriv(algo, key, iv);
var dec = decipher.update(text,'hex','utf8');
dec += decipher.final('utf8');
return dec;
}
This is still not enough, because this code might be vulnerable to the padding oracle attack depending on your communication architecture. You should authenticate the ciphertexts with a message authentication code (MAC). A popular choice is HMAC-SHA256 for "encrypt-then-MAC".
var crypto = require('crypto');
var algo = 'aes-256-cbc';
var algoSecret = 'mySecret';
var key = crypto.pbkdf2Sync(algoSecret, 'salt', 1000, 512, 'sha512');
var keyMac = key.slice(32);
var keyEnc = key.slice(0, 32);
var encrypt = function(secret){
var iv = crypto.randomBytes(16);
var cipher = crypto.createCipheriv(algo, keyEnc, iv);
var crypted = cipher.update(secret,'utf8','hex')
crypted += cipher.final('hex');
var ct = iv.toString('hex') + crypted;
var hmac = crypto.createHmac('sha256', keyMac);
hmac.update(ct);
return ct + hmac.digest('hex');
}
var decrypt = function(text){
var hmac = crypto.createHmac('sha256', keyMac);
hmac.update(text.slice(0, -64));
if (hmac.digest('hex') !== text.slice(-64)) { // TODO: contant-time comparison
// TODO: do some decoy decryption
return false;
}
var iv = new Buffer(text.slice(0, 32), 'hex');
text = text.slice(32, -64);
var decipher = crypto.createDecipheriv(algo, keyEnc, iv);
var dec = decipher.update(text,'hex','utf8');
dec += decipher.final('utf8');
return dec;
}

Decrypting AES256 with node.js returns wrong final block length

Using this Gist I was able to successfully decrypt AES256 in Node.js 0.8.7. Then when I upgraded to Node.js 0.10.24, I now see this error:
TypeError: error:0606506D:digital envelope
routines:EVP_DecryptFinal_ex:wrong final block length
at Decipheriv.Cipher.final (crypto.js:292:27)
Here is the decrypt code from the Gist (shown here for convenience):
var crypto = require('crypto');
var AESCrypt = {};
AESCrypt.decrypt = function(cryptkey, iv, encryptdata) {
encryptdata = new Buffer(encryptdata, 'base64').toString('binary');
var decipher = crypto.createDecipheriv('aes-256-cbc', cryptkey, iv),
decoded = decipher.update(encryptdata);
decoded += decipher.final();
return decoded;
}
AESCrypt.encrypt = function(cryptkey, iv, cleardata) {
var encipher = crypto.createCipheriv('aes-256-cbc', cryptkey, iv),
encryptdata = encipher.update(cleardata);
encryptdata += encipher.final();
encode_encryptdata = new Buffer(encryptdata, 'binary').toString('base64');
return encode_encryptdata;
}
var cryptkey = crypto.createHash('sha256').update('Nixnogen').digest(),
iv = 'a2xhcgAAAAAAAAAA',
buf = "Here is some data for the encrypt", // 32 chars
enc = AESCrypt.encrypt(cryptkey, iv, buf);
var dec = AESCrypt.decrypt(cryptkey, iv, enc);
console.warn("encrypt length: ", enc.length);
console.warn("encrypt in Base64:", enc);
console.warn("decrypt all: " + dec);
Ok, so there was a change to Crypto in the switch from 0.8 to 0.10 Crypto methods return Buffer objects by default, rather than binary-encoded strings
This means the above code needs to specify encodings.
These four lines:
decoded = decipher.update(encryptdata);
decoded += decipher.final();
encryptdata = encipher.update(cleardata);
encryptdata += encipher.final();
Are changed to:
decoded = decipher.update(encryptdata, 'binary', 'utf8');
decoded += decipher.final('utf8');
encryptdata = encipher.update(cleardata, 'utf8', 'binary');
encryptdata += encipher.final('binary');
This worked for me, but I am open to other suggestions.
As your answer states, those functions work with Buffers now unless you specify an encoding. That said, you'd be better off avoiding binary encoded strings entirely and treat everything as Buffers until you strictly need a string for something. This way you can also use your encryption helpers to process non-text content.
var crypto = require('crypto');
var AESCrypt = {};
AESCrypt.decrypt = function(cryptkey, iv, encryptdata) {
var decipher = crypto.createDecipheriv('aes-256-cbc', cryptkey, iv);
return Buffer.concat([
decipher.update(encryptdata),
decipher.final()
]);
}
AESCrypt.encrypt = function(cryptkey, iv, cleardata) {
var encipher = crypto.createCipheriv('aes-256-cbc', cryptkey, iv);
return Buffer.concat([
encipher.update(cleardata),
encipher.final()
]);
}
var cryptkey = crypto.createHash('sha256').update('Nixnogen').digest(),
iv = new Buffer('a2xhcgAAAAAAAAAA'),
buf = new Buffer("Here is some data for the encrypt"), // 32 chars
enc = AESCrypt.encrypt(cryptkey, iv, buf);
var dec = AESCrypt.decrypt(cryptkey, iv, enc);
console.warn("encrypt length: ", enc.length);
console.warn("encrypt in Base64:", enc.toString('base64'));
console.warn("decrypt all: " + dec.toString('utf8'));
My issue was that the string I was passing to my decrypt function was empty. I built in a check for empty strings and I did not receive the message again.
decrypt: function(text){
if(text.length == 0){
return text;
}
return this.decipher.update(text, 'hex', 'utf8') + this.decipher.final('utf8');
}

Bad decrypt on NodeJS Triple DES implementation?

var cardInfo = "<Card><CVV></CVV><CardNumber></CardNumber><ExpMonth></ExpMonth><ExpYear></ExpYear><Member></Member></Card>"
function genKeyPair(passphrase){
var iv = crypto.createHash('md5').update(passphrase).digest('hex').substring(0, 8)
var key = crypto.createHash('md5').update(passphrase).digest('hex').substring(0, 24)
return {
key: key,
iv: iv
}
}
function encrypt3DES(key, vector, data){
var encryptor = crypto.createCipheriv('des3', key, vector)
var raw = new Buffer(data)
encryptor.update(raw)
var encrypted = encryptor.final()
return encrypted
}
function decrypt3DES(key, vector, data){
var decryptor = crypto.createDecipheriv('des3', key, vector)
decryptor.update(data)
var decrypted = decryptor.final()
return decrypted
}
var key = genKeyPair('test')
var data3DES = encrypt3DES(key.key, key.iv, cardInfo)
var decryptedCard = decrypt3DES(key.key, key.iv, data3DES)
So, I get a "bad decrypt" on decryptor.final() and can't figure out why.
Node expects a buffer when encrypting so you see I provide that in the beginning of the encrypt3DES
I put the raw buffer output from encryption straight into the decrypt method
What am I doing wrong here?
DISCLAIMER
No, this is not going to be used in production. I'm just toying around so please hold with the "you don't know what you're doing so you shouldn't do it" talk
The results of update are thrown away in your code:
Returns the enciphered contents, and can be called many times with new data as it is streamed.
The code also uses 3DES ABC keys that do not contain enough entropy (keys should be binary data, not hexadecimals). At least try to use crypto.createCipher(algorithm, password) or try and find an implementation of PBKDF2 in JavaScript.
here is algorithm related key and iv lengths relations, as
DES-ECB Key: 8; IV: 0
DES-CBC Key: 8; IV: 8
DES-CFB Key: 8; IV: 8
DES-CFB1 Key: 8; IV: 8
DES-CFB8 Key: 8; IV: 8
DES-EDE-CBC Key: 16; IV: 8
DES-EDE-CFB Key: 16; IV: 8
DES-EDE-OFB Key: 16; IV: 8
DES-EDE3-CBC Key: 24; IV: 8
DESX-CBC Key: 24; IV: 8
and, this is a runnable example
const crypto = require('crypto');
const iv = '12345678';
const key = '123456'.padEnd(24,'0');
const ivHex = Buffer.from(iv, 'utf8');
const keyHex = Buffer.from(key, 'utf8');
const decrypt = (text)=>{
const cipher = crypto.createDecipheriv('DES-EDE3-CBC', keyHex,ivHex);
let c = cipher.update(text, 'base64','utf8')
c += cipher.final('utf8');
return c;
}
const encrypt = (text)=>{
const cipher = crypto.createCipheriv('DES-EDE3-CBC', keyHex,ivHex);
let c = cipher.update(text, 'utf8' ,'base64');
c+=cipher.final('base64');
return c;
}
const text = '7LBIMxZKDB0=';
const plaintext = decrypt(text);
console.log(plaintext);//123456
const cipherText = encrypt(plaintext);
console.log(cipherText, text === cipherText);//7LBIMxZKDB0= true
hope it helps.

Resources