Java to Node.js AES/ECB/PKCS5Padding Encryption - node.js

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);

Related

AES CBC nodejs encryption and Java decryption

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>

Encrypt a string in nodejs and decrypt in java

I am encrypting a text in NODEJS and trying decrypt in Java but getting error.
my nodejs code:
var crypto = require('crypto')
, key = 'mykey#91'
, plaintext = 'SS18617710213463'
, cipher = crypto.createCipher('aes-128-ecb', key)
, decipher = crypto.createDecipher('aes-128-ecb', key);
var encryptedPassword = cipher.update(plaintext, 'utf8', 'base64');
encryptedPassword += cipher.final('base64')
var decryptedPassword = decipher.update(encryptedPassword, 'base64', 'utf8');
decryptedPassword += decipher.final('utf8');
console.log('original :', plaintext);
console.log('encrypted :', encryptedPassword);
console.log('decrypted :', decryptedPassword);
but when I am trying to decrypt it, it always throws an error.
public static String decrypt(String encryptedText) {
try {
final String key = "mykey#91";
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] cipherText = Base64.getDecoder().decode(encryptedText.getBytes("UTF8"));
String decryptedString = new String(cipher.doFinal(cipherText),"UTF8");
return decryptedString;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
The error I am getting as below:
java.security.InvalidKeyException: Invalid AES key length: 8 bytes
The reason you are getting Invalid AES key length: 8 bytes invalid AES is related to length of your key and text. You need to make sure that its length in bits is a power of two. If you want to use a String as your encryption key, check its length in bytes and multiply by 8 to find the length in bits. Also most String implementation will require 2 bytes for every character (Java 64bit). Detailed information here: How to solve InvalidKeyException
In this case, the mentioned error will disappear just using a padded or longer key, for example:
static String PLAIN_TEXT = "SS18617710213463";
static String ENCRYPTION_KEY = "mykey#91mykey#91";
However there is another important thing to consider. The Java Implementation has to match the exact algorithms provided by node.js. This is not as easy as it sounds (at least based on my experience). In your case, I would suggest you to use node-forge on node.js side which is easier to match Java implementations:
var forge = require('node-forge');
var plaintext = 'SS18617710213463';
var key = 'mykey#91mykey#91';
var iv = 'AODVNUASDNVVAOVF';
console.log('Plain Text: ' + plaintext);
var cipher = forge.cipher.createCipher('AES-CBC', key);
cipher.start({iv: iv});
cipher.update(forge.util.createBuffer(plaintext));
cipher.finish();
var encrypted = cipher.output;
var encodedB64 = forge.util.encode64(encrypted.data);
console.log("Encoded: " + encodedB64);
var decodedB64 = forge.util.decode64(encodedB64);
encrypted.data = decodedB64;
var decipher = forge.cipher.createDecipher('AES-CBC', key);
decipher.start({iv: iv});
decipher.update(encrypted);
var result = decipher.finish();
console.log("Decoded: " + decipher.output.data);
Running the code above, the output should be:
Plain Text: SS18617710213463
Encoded: HCzZD7uc13fqfM6odWcXf/mdR4aNJfkMDhEbnU+asjE=
Decoded: SS18617710213463
And the compatible Java code that will work on the same way looks like the code below:
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Main {
static String PLAIN_TEXT = "SS18617710213463";
static String ENCRYPTION_KEY = "mykey#91mykey#91";
static String INITIALIZATIO_VECTOR = "AODVNUASDNVVAOVF";
public static void main(String [] args) {
try {
System.out.println("Plain text: " + PLAIN_TEXT);
byte[] encryptedMsg = encrypt(PLAIN_TEXT, ENCRYPTION_KEY);
String base64Encrypted = Base64.getEncoder().encodeToString(encryptedMsg);
System.out.println("Encrypted: "+ base64Encrypted);
byte[] base64Decrypted = Base64.getDecoder().decode(base64Encrypted);
String decryptedMsg = decrypt(base64Decrypted, ENCRYPTION_KEY);
System.out.println("Decrypted: " + decryptedMsg);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] encrypt(String plainText, String encryptionKey) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/pkcs5padding", "SunJCE");
SecretKeySpec key = new SecretKeySpec(encryptionKey.getBytes("UTF-8"), "AES");
cipher.init(Cipher.ENCRYPT_MODE, key,new IvParameterSpec(INITIALIZATIO_VECTOR.getBytes("UTF-8")));
return cipher.doFinal(plainText.getBytes("UTF-8"));
}
public static String decrypt(byte[] cipherText, String encryptionKey) throws Exception{
Cipher cipher = Cipher.getInstance("AES/CBC/pkcs5padding", "SunJCE");
SecretKeySpec key = new SecretKeySpec(encryptionKey.getBytes("UTF-8"), "AES");
cipher.init(Cipher.DECRYPT_MODE, key,new IvParameterSpec(INITIALIZATIO_VECTOR.getBytes("UTF-8")));
return new String(cipher.doFinal(cipherText),"UTF-8");
}
}
Which produces:
Plain text: SS18617710213463
Encrypted: HCzZD7uc13fqfM6odWcXf/mdR4aNJfkMDhEbnU+asjE=
Decrypted: SS18617710213463

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;
}

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