I'm trying to port this nodejs code to java
const crypto = require("crypto");
const encrypt = (data, key) => {
const cipher = crypto.createCipher('aes192', key)
let crypted = cipher.update(data, 'utf8', 'hex')
crypted += cipher.final('hex')
return crypted;
}
I've tried to use this solution:
import org.springframework.security.crypto.codec.Hex;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public String encrypt(String data, String key) {
try {
var cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes(), "AES"));
var cipherText = cipher.update(data.getBytes());
cipherText = ArrayUtils.addAll(cipherText, cipher.doFinal());
return new String(Hex.encode(cipherText));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Cons:
encrypt
method for Java returns a different value than the method for nodejs (for the same data and key).
In nodejs, I can put a short key (5 characters long), at the same time in java I'm catching an exception e.g. "java.security.InvalidKeyException: Invalid AES key length: 5 bytes"
Could you suggest the right solution or point on a mistake in the existing one? Thank you in advance!
Note: I'm unable to change encrypt/decrypt method in nodejs, so I need to port this to java correctly.
Your encrypt
java version doesn't use the same logic as the javascript version.
The encrypt
javascript function takes a password (the argument name key
is misleading) and then passes it to createCipher
. createCipher
doesn't use the password directly, but derives a key from it. It's the derived key that is used to encrypt the message. From NodeJs documentation :
The password is used to derive the cipher key and initialization vector (IV). The value must be either a 'latin1' encoded string, a Buffer, a TypedArray, or a DataView.
The encrypt
java function, on the other hand, expects a ready to use key, so you have to do the key derivation from the password yourself.
AES keys have a fixed size. They can only be 128, 192 or 256 bits long. (in bytes 8, 16, 32 bytes long). If you use a key with a different size, you'll get the exception InvalidKeyException
. NodeJS didn't complain about an "invalid key length" because in actuality you were using a password, not a key. NodeJS derives a key from the password before encrypting the data.
(as mentioned in the docs) NodeJs uses OpenSSL to encrypt the data and derives the key using a function specific to OpenSSL : EVP_BytesToKey
.
Luckily this SO answer has an implementation of EVP_BytesToKey
in Java. (The code is originally from this blog entry)
I adapted your code to use it. I added the final result to the end of the answer. I rarely write security code, and in this case I just adapted an existing solution, so you should review the code (or have someone else review it if you have a security team in your company) if you decide to use it.
One final comment : createCipher
is deprecated. But you said in your question that you can't change the implementation of the javascript encrypt
version. (If you're not the maintainer of encrypt
) You should discuss the deprecation issue with the maintainer to understand their reasons for still using createCipher
(which uses EVP_BytesToKey). EVP_BytesToKey
is considered a weak key derivation function and OpenSSL recommends using more modern functions for newer applications.
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.security.crypto.codec.Hex;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class Main {
public static void main(String[] args){
System.out.println("Result : " + encrypt("my secret message","pass"));
}
public static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md, byte[] salt, byte[] data, int count) {
byte[][] both = new byte[2][];
byte[] key = new byte[key_len];
int key_ix = 0;
byte[] iv = new byte[iv_len];
int iv_ix = 0;
both[0] = key;
both[1] = iv;
byte[] md_buf = null;
int nkey = key_len;
int niv = iv_len;
int i = 0;
if(data == null) {
return both;
}
int addmd = 0;
for(;;) {
md.reset();
if(addmd++ > 0) {
md.update(md_buf);
}
md.update(data);
if(null != salt) {
md.update(salt,0,8);
}
md_buf = md.digest();
for(i=1;i<count;i++) {
md.reset();
md.update(md_buf);
md_buf = md.digest();
}
i=0;
if(nkey > 0) {
for(;;) {
if(nkey == 0) break;
if(i == md_buf.length) break;
key[key_ix++] = md_buf[i];
nkey--;
i++;
}
}
if(niv > 0 && i != md_buf.length) {
for(;;) {
if(niv == 0) break;
if(i == md_buf.length) break;
iv[iv_ix++] = md_buf[i];
niv--;
i++;
}
}
if(nkey == 0 && niv == 0) {
break;
}
}
for(i=0;i<md_buf.length;i++) {
md_buf[i] = 0;
}
return both;
}
public static String encrypt(String data, String password) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
var keySizeBits = 192 / Byte.SIZE; //AES with 192 bits key = 16 bytes
var ivSize = cipher.getBlockSize();
final byte[][] keyAndIV = EVP_BytesToKey(keySizeBits, ivSize, md5, null, password.getBytes(StandardCharsets.US_ASCII), 1);
SecretKeySpec key = new SecretKeySpec(keyAndIV[0], "AES");
IvParameterSpec iv = new IvParameterSpec(keyAndIV[1]);
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
var cipherText = cipher.update(data.getBytes());
cipherText = ArrayUtils.addAll(cipherText, cipher.doFinal());
return new String(Hex.encode(cipherText));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}