Search code examples
javanode.jsencryptionaes

How to port encrypt (AES192) method from nodejs to java correctly?


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:

  1. encrypt method for Java returns a different value than the method for nodejs (for the same data and key).

  2. 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.


Solution

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