Search code examples
javascriptjavaencryptioncryptographyaes-gcm

AES/GCM/NoPadding encryption in JAVA & decryption in JavaScript


I am using AES/GCM/NoPadding 128 bit. I want to encrypt in Java & decrypt it in Javascript.

When I encrypt in JS & try to decrypt in Java I get the error as Tag mismatch!null

When I encrypt in Java & try to decrypt in JS I get the error as

internal/crypto/cipher.js:164
  const ret = this._handle.final();
                           ^

Error: Unsupported state or unable to authenticate data
    at Decipheriv.final (internal/crypto/cipher.js:164:28)
    at decrypt (/tmp/HoErdq6TQ2.js:51:58)

What am I missing in my JS Please suggest fixes in JS code, Java code can't be changed as it is in use on production

Java code:


import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import java.util.logging.Logger;
import java.util.Base64;

public class HelloWorld {

    private final static Logger LOGGER =  Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);

    public static void main(String []args) {
        String masterKey = "2f12cb0f1d2e3d12345f1af2b123dce4";
        String encrypted = aesEncryptStringV2("Hello, World!", masterKey);
        System.out.println(encrypted);
        

        String decrypted = aesDecryptStringV2(encrypted, masterKey);
        System.out.println(decrypted);
    }


    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_IV_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 16;

    private static SecretKeySpec setKeyV2(final String myKey) {
        try {
            byte[] newKey = myKey.getBytes(StandardCharsets.UTF_8);
            MessageDigest sha = MessageDigest.getInstance("SHA-512");

            newKey = sha.digest(newKey);
            newKey = Arrays.copyOf(newKey, 16);

            return new SecretKeySpec(newKey, "AES");
        } catch (Exception e) {
            System.out.println("Error in setKeyV2: ");
            System.out.println(e.getMessage());
        }
        return null;
    }

    public static synchronized String aesEncryptStringV2(
        final String strToEncrypt, final String secret) {
        try {
            SecretKeySpec newSecretKey = setKeyV2(secret);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            GCMParameterSpec gcmParameterSpec = new
            GCMParameterSpec(GCM_TAG_LENGTH * 8,
                             new byte[GCM_IV_LENGTH]);
            cipher.init(Cipher.ENCRYPT_MODE, newSecretKey, gcmParameterSpec);
            return Base64.getEncoder()
                   .encodeToString(cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8
                                                                       )));
        } catch (Exception e) {
            System.out.println("Error in aesEncryptStringV2: ");
            System.out.println(e.getMessage());
        }
        return null;
    }

    public static synchronized String aesDecryptStringV2(
        final String strToDecrypt, final String secret) {
        try {
            SecretKeySpec newSecretKey = setKeyV2(secret);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            GCMParameterSpec gcmParameterSpec = new
            GCMParameterSpec(GCM_TAG_LENGTH * 8,
                             new byte[GCM_IV_LENGTH]);
            cipher.init(Cipher.DECRYPT_MODE, newSecretKey, gcmParameterSpec);

            return new
                   String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)));
        } catch (Exception e) {
            System.out.println("Error in aesDecryptStringV2: ");
            System.out.println(e.getMessage());
        }
        return null;
    }
}

Javascript Code:


const crypto = require('crypto');


const cryptoConfig = {
    cipherAlgorithm: 'aes-128-gcm',
    masterKey: '2f12cb0f1d2e3d12345f1af2b123dce4',
    ivLength: 12,
    tagLength: 16,
    digest: 'sha512'
}

const generateKey = () => {
    var h = crypto.createHash(cryptoConfig.digest);
    h.update(cryptoConfig.masterKey, 'utf8');
    
    var k = h.digest().slice(0, 16);
    return k;
  };

function encrypt(content) {
    const iv = crypto.randomBytes(cryptoConfig.ivLength);
    const key = generateKey();

    const cipher = crypto.createCipheriv(cryptoConfig.cipherAlgorithm, key, iv, {authTagLength: cryptoConfig.tagLength});

    const encrypted = Buffer.concat([cipher.update(content, 'utf8'), cipher.final()]);

    const tag = cipher.getAuthTag();

    return Buffer.concat([iv, encrypted, tag]).toString('base64');
}


const decrypt = (encdata, masterkey) => {
    const bData = Buffer.from(encdata, 'base64');

    const iv = bData.slice(0, 12);
    const tag = bData.slice(bData.length - 16, bData.length);
    const text = bData.slice(12, bData.length - 16);

    const key = generateKey(masterkey);

    const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv);
    decipher.setAuthTag(tag);


    const decrypted =
      decipher.update(text, 'binary', 'utf8') + decipher.final('utf8');


    return decrypted;
  };

const encryptedData = encrypt('hello world'); 
console.log('encrypt data -> ', encryptedData);

const decryptedData = decrypt(encryptedData); 
console.log('decryptedData -> ', decryptedData);


Solution

  • So the Java code doesn't attach IV/nonce to the output. It uses a buffer of 0's of specified length.

    Updated JS decryption code:

    const decrypt = (encdata, masterkey) => {
    const bData = Buffer.from(encdata, 'base64');
    
    const iv = Buffer.alloc(12);
    const tag = bData.slice(bData.length - 16, bData.length);
    const text = bData.slice(0, bData.length - 16);
    
    const key = generateKey(masterkey);
    
    const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv);
    decipher.setAuthTag(tag);
    
    
    const decrypted =
      decipher.update(text, 'binary', 'utf8') + decipher.final('utf8');
    
    
    return decrypted;
    };
    

    Serious security warning: Do NOT use the above code in production as it is UNSECURE. You should consider to change your production code as the encryption in AES GCM mode gets total insecure when using a nonce more than one time with the same key (the Java code is using a static IV/nonce)

    Credits - Michael Fehr's comment