Java's AES/GCM/NoPadding equivalent in JS

Viewed 606

We are using the services of a third party in our organization in which we have to send some data to them in an encrypted manner. Recently, they updated the encryption algorithm to AES/GCM/NoPadding.

They have their code in java whereas we use javascript. they have shared with us their implementation of the algorithm in Java which we have to replicate and implement in JS because that is what we use.

I am facing challenges in converting this code. Attaching both Java implementation which works like a charm and the JS code which is not working as expected. Although I have tried multiple things but none of them worked for me. So, I am sharing only the latest code that I tried.

I have no knowledge of Java or cryptography so any help in that direction will be highly appreciated.

JAVA Code -

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * Encryption class for managing all types of AES encryptions
 */
public class EncryptionUtil {

    private final Builder mBuilder;
    private final static String HEX = "0123456789ABCDEF";

    private EncryptionUtil(Builder builder) {
        mBuilder = builder;
    }

    public static EncryptionUtil getDefault(String key, String salt, byte[] iv) {
        try {
            return Builder.getDefaultBuilder(key, salt, iv).build();
        } catch (NoSuchAlgorithmException e) {
        
            return null;
        }
    }

    public String encryptOrNull(String data) {
        try {
            return encrypt(data);
        } catch (Exception e) {
        
            return "";
        }
    }

    private String encrypt(String data) throws Exception {
        if (data == null) return null;
        SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey()));
        return doEncryptAES(data, secretKey, mBuilder.getAlgorithm(), mBuilder.getCharsetName());
    }

    private String decrypt(String data) throws Exception {
        if (data == null) return null;
        SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey()));
        return doDecryptAES(data, secretKey, mBuilder.getAlgorithm());
    }

    private String doEncryptAES(String inputString,
                                SecretKey key, String xForm, String charset) throws Exception {
        byte inpBytes[] = inputString.getBytes(charset);
        Cipher cipher = Cipher.getInstance(xForm);
        switch (xForm) {
            case "AES/ECB/PKCS5Padding":
            case "AES/ECB/NoPadding":
                cipher.init(Cipher.ENCRYPT_MODE, key);
                break;
            case "AES/CBC/PKCS5Padding":
            case "AES/CBC/NoPadding":
                cipher.init(Cipher.ENCRYPT_MODE, key, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom());
                break;
            case "AES/GCM/NoPadding":
            
                    cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, mBuilder.getIv()));
                    byte[] encryptedData = cipher.doFinal(inpBytes);

                    ByteBuffer byteBuffer = ByteBuffer.allocate(4 + mBuilder.getIv().length + encryptedData.length);
                    byteBuffer.putInt(mBuilder.getIv().length);
                    byteBuffer.put(mBuilder.getIv());
                    byteBuffer.put(encryptedData);
                    return toHex(byteBuffer.array());
        
        }
        return toHex(cipher.doFinal(inpBytes));
    }

    /**
     * for AES in GCM mode kitkat version is required
     *
     * @param inputString is String we want to decrypt
     * @param key         is symmetric key use for decryption and it similar to key used for encryption (128,192,256)
     * @param xForm       is the transformation form in which form we want to transform
     *                    (AES/ECB/PKCS5Padding,AES/ECB/NoPadding,AES/CBC/PKCS5Padding,AES/CBC/NoPadding,AES/GCM/NoPadding)
     * @return it reurn decrypted string
     * @throws Exception NOSuchAlgorithmEXception,NoSuchPaddingEXception
     */
    private String doDecryptAES(String inputString,
                                SecretKey key, String xForm) throws Exception {
        byte[] inpBytes = toByte(inputString);
        Cipher cipher = Cipher.getInstance(xForm);
        switch (xForm) {
            case "AES/ECB/PKCS5Padding":
            case "AES/ECB/NoPadding":
                cipher.init(Cipher.DECRYPT_MODE, key);
                break;
            case "AES/CBC/PKCS5Padding":
            case "AES/CBC/NoPadding":
                cipher.init(Cipher.DECRYPT_MODE, key, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom());
                break;
            case "AES/GCM/NoPadding":
            
                    ByteBuffer byteBuffer = ByteBuffer.wrap(inpBytes);
                    int noonceSize = byteBuffer.getInt();

                    if (noonceSize < 12 || noonceSize >= 16)
                        throw new IllegalArgumentException("Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file.");

                    byte[] iv = new byte[noonceSize];
                    byteBuffer.get(iv);

                    byte[] cipherBytes = new byte[byteBuffer.remaining()];
                    byteBuffer.get(cipherBytes);
                    cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
                    return new String(cipher.doFinal(cipherBytes), mBuilder.getCharsetName());
               
        }
        return new String(cipher.doFinal(inpBytes), mBuilder.getCharsetName());
    }

    public String decryptOrNull(String data) {
        try {
            return decrypt(data);
        } catch (Exception e) {
            return "";
        }
    }

    private SecretKey getSecretKey(char[] key) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException {
        SecretKeyFactory factory = SecretKeyFactory.getInstance(mBuilder.getSecretKeyType());
        KeySpec spec = new PBEKeySpec(key, mBuilder.getSalt().getBytes(mBuilder.getCharsetName()), mBuilder.getIterationCount(), mBuilder.getKeyLength());
        SecretKey tmp = factory.generateSecret(spec);
        return new SecretKeySpec(tmp.getEncoded(), mBuilder.getKeyAlgorithm());
    }

    private char[] hashTheKey(String key) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        MessageDigest messageDigest = MessageDigest.getInstance(mBuilder.getDigestAlgorithm());
        messageDigest.update(key.getBytes(mBuilder.getCharsetName()));
        return toHex(messageDigest.digest()).toCharArray();
    }

    private byte[] toByte(String hexString) {
        int len = hexString.length() / 2;

        byte[] result = new byte[len];

        for (int i = 0; i < len; i++)
            result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue();
        return result;
    }

    public String toHex(byte[] stringBytes) {
        StringBuffer result = new StringBuffer(2 * stringBytes.length);

        for (int i = 0; i < stringBytes.length; i++) {
            result.append(HEX.charAt((stringBytes[i] >> 4) & 0x0f)).append(HEX.charAt(stringBytes[i] & 0x0f));
        }

        return result.toString();
    }

    private static class Builder {

        private byte[] mIv;
        private int mKeyLength;
        private int mIterationCount;
        private String mSalt;
        private String mKey;
        private String mAlgorithm;
        private String mKeyAlgorithm;
        private String mCharsetName;
        private String mSecretKeyType;
        private String mDigestAlgorithm;
        private String mSecureRandomAlgorithm;
        private SecureRandom mSecureRandom;
        private IvParameterSpec mIvParameterSpec;

        static Builder getDefaultBuilder(String key, String salt, byte[] iv) {
            return new Builder()
                    .setIv(iv)
                    .setKey(key)
                    .setSalt(salt)
                    .setKeyLength(128)
                    .setKeyAlgorithm("AES")
                    .setCharsetName("UTF8")
                    .setIterationCount(1)
                    .setDigestAlgorithm("SHA-256")
                    .setAlgorithm("AES/GCM/NoPadding")
                    .setSecureRandomAlgorithm("SHA1PRNG")
                    .setSecretKeyType("PBKDF2WithHmacSHA1");
        }

        private EncryptionUtil build() throws NoSuchAlgorithmException {
            setSecureRandom(SecureRandom.getInstance(getSecureRandomAlgorithm()));
            SecureRandom secureRandom = new SecureRandom();
            byte[] iv = getIv();
            secureRandom.nextBytes(iv);
            setIvParameterSpec(new IvParameterSpec(iv));
            return new EncryptionUtil(this);
        }

        private String getCharsetName() {
            return mCharsetName;
        }

        private Builder setCharsetName(String charsetName) {
            mCharsetName = charsetName;
            return this;
        }

        private String getAlgorithm() {
            return mAlgorithm;
        }

        private Builder setAlgorithm(String algorithm) {
            mAlgorithm = algorithm;
            return this;
        }

        private String getKeyAlgorithm() {
            return mKeyAlgorithm;
        }

        private Builder setKeyAlgorithm(String keyAlgorithm) {
            mKeyAlgorithm = keyAlgorithm;
            return this;
        }

        private String getSecretKeyType() {
            return mSecretKeyType;
        }

        private Builder setSecretKeyType(String secretKeyType) {
            mSecretKeyType = secretKeyType;
            return this;
        }

        private String getSalt() {
            return mSalt;
        }

        private Builder setSalt(String salt) {
            mSalt = salt;
            return this;
        }

        private String getKey() {
            return mKey;
        }

        private Builder setKey(String key) {
            mKey = key;
            return this;
        }

        private int getKeyLength() {
            return mKeyLength;
        }

        Builder setKeyLength(int keyLength) {
            mKeyLength = keyLength;
            return this;
        }

        private int getIterationCount() {
            return mIterationCount;
        }

        Builder setIterationCount(int iterationCount) {
            mIterationCount = iterationCount;
            return this;
        }

        private String getSecureRandomAlgorithm() {
            return mSecureRandomAlgorithm;
        }

        Builder setSecureRandomAlgorithm(String secureRandomAlgorithm) {
            mSecureRandomAlgorithm = secureRandomAlgorithm;
            return this;
        }

        private byte[] getIv() {
            return mIv;
        }

        Builder setIv(byte[] iv) {
            mIv = iv;
            return this;
        }

        private SecureRandom getSecureRandom() {
            return mSecureRandom;
        }

        Builder setSecureRandom(SecureRandom secureRandom) {
            mSecureRandom = secureRandom;
            return this;
        }

        private IvParameterSpec getIvParameterSpec() {
            return mIvParameterSpec;
        }

        Builder setIvParameterSpec(IvParameterSpec ivParameterSpec) {
            mIvParameterSpec = ivParameterSpec;
            return this;
        }

        private String getDigestAlgorithm() {
            return mDigestAlgorithm;
        }

        Builder setDigestAlgorithm(String digestAlgorithm) {
            mDigestAlgorithm = digestAlgorithm;
            return this;
        }

    }

    public static void main(String[] args) {
        String secretKey = "some_secret_key";
        String salt = "some_secret_salt";
        EncryptionUtil encryptionUtil = EncryptionUtil.getDefault(secretKey, salt, new byte[12]);
        String data = "Data to encrypt";
        System.out.println("Encrypted:");
        String encrypted = encryptionUtil.encryptOrNull(data);
        System.out.println(encrypted);
        System.out.println("Decrypted:");
        System.out.println(encryptionUtil.decryptOrNull(encrypted));
    }
}

Please note I need help only to encrypt the data

JS Code -

import * as crypto from 'crypto';

export const encData = () => {
    const data = 'Data to encrypt';
    const secretKey = 'some_secret_key';
    const salt = 'some_secret_salt';
    let key = '';

    const keyHash = key => {
        const hash = crypto.createHash('sha256');
        const hashedKey = hash.update(key, 'utf-8');
        return hashedKey.digest('hex').toUpperCase();
    };

    const getSecretKey = key => {
        return crypto.pbkdf2Sync(key, salt, 1, 16, 'sha1');
    };

    key = getSecretKey(keyHash(secretKey));

    const iv = crypto.randomBytes(12);
    const cipher = crypto.createCipheriv('aes-128-gcm', key, iv);
    const buffer = Buffer.from(_.isPlainObject(data) ? JSON.stringify(data) : data);

    // Updating text
    let encrypted = cipher.update(buffer);

    // Using concatenation
    encrypted = Buffer.concat([encrypted, cipher.final()]);

    return encrypted.toString('base64');
};

console.log(encData());

To make sure my code is working fine I am decrypting my encoded string generated with JS function by passing it to Java decrypt function.

1 Answers

In the Java code, the result of the encryption is composed as follows:

iv-length (4 bytes, BE) | IV | ciphertext | authentication tag 

In contrast, in the NodeJS code the result consists only of the ciphertext, i.e. IV length, IV and tag are missing and must be added.
Here it must be taken into account that Java's SunJCE provider automatically concatenates ciphertext and tag, while this must happen explicitly in the NodeJS code.

Also, the ciphertext is returned hex encoded in the Java code, while it is Base64 encoded in the NodeJS code. This also needs to be changed in the NodeJS code.

The fix is to replace in the NodeJS code the lines:

// Using concatenation
encrypted = Buffer.concat([encrypted, cipher.final()]);

return encrypted.toString('base64');

with:

const length = Buffer.allocUnsafe(4);
length.writeUInt32BE(iv.length);

// Using concatenation
encrypted = Buffer.concat([length, iv, encrypted, cipher.final(), cipher.getAuthTag()]);

return encrypted.toString('hex');

With this, the NodeJS code returns a result that can be decrypted by the Java code.


Note that a static salt is insecure. Instead, the salt should be randomly generated like the IV for each encryption and passed along with the ciphertext.
Also, an iteration count of 1 is not secure, the value should be as high as possible with acceptable performance.
Hashing the key with SHA256 before the PBKDF2 derivation is actually not necessary (at least if PBKDF2 is applied correctly).

Related