Search code examples
javanode.jsaesaes-gcm

Cannot decrypt AES-256 GCM with Java


I have a node module that can both encrypt and decrypt with AES-256 GCM. Now I am also trying to decrypt with Java whatever the node module encrypts, but I keep getting a AEADBadTagException.

I have tested the node module by itself and can confirm that it works as intended. I know that Java assumes the authentication tag is the last part of the message, so I ensured that the tag is the last thing appended in the node module.

Right now I'm just testing with the word, "hello". This is the encrypted message from node: Q10blKuyyYozaRf0RVYW7bave8mT5wrJzSdURQQa3lEqEQtgYM3ss825YpCQ70A7hpq5ECPafAxdLMSIBZCxzGbv/Cj4i6W4JCJXuS107rUy0tAAQVQQA2ZhbrQ0gNV9QA==

The salt is not really being used right now because I am trying to keep things simple for testing purposes

Node module:

var crypto = require('crypto');

var encrypt = function(masterkey, plainText) {
    // random initialization vector
    var iv = crypto.randomBytes(12);

    // random salt
    var salt = crypto.randomBytes(64);

    var key = masterkey;

    // AES 256 GCM Mode
    var cipher = crypto.createCipheriv('aes-256-gcm', key, iv);

    // encrypt the given text
    var encrypted = Buffer.concat([cipher.update(plainText, 'utf8'), cipher.final()]);

    // extract the auth tag
    var tag = cipher.getAuthTag();

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

var decrypt = function(masterkey, encryptedText) {
    // base64 decoding
    var bData = new Buffer(encryptedText, 'base64');

    // convert data to buffers
    var salt = bData.slice(0, 64);
    var iv = bData.slice(64, 76);
    var tag = bData.slice(bData.length - 16, bData.length);
    var text = bData.slice(76, bData.length - 16);

    var key = masterkey;

    // AES 256 GCM Mode
    var decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
    decipher.setAuthTag(tag);

    // encrypt the given text
    var decrypted = decipher.update(text, 'binary', 'utf8') + decipher.final('utf8');

    return decrypted;
};

module.exports = {
    encrypt: encrypt,
    decrypt: decrypt
}

Java Class: The main method is just there for testing right now and will be removed later.

package decryption;

import java.util.Arrays;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class DecryptAES256 {

    private static String salt;
    private static byte[] ivBase64;
    private static String base64EncryptedText;
    private static String key;

    public static void main(String[] args) {
        String key = "123456789aabbccddeefffffffffffff";
        String sourceText = "Q10blKuyyYozaRf0RVYW7bave8mT5wrJzSdURQQa3lEqEQtgYM3ss825YpCQ70A7hpq5ECPafAxdLMSIBZCxzGbv/Cj4i6W4JCJXuS107rUy0tAAQVQQA2ZhbrQ0gNV9QA==";
        System.out.println(decrypt(key, sourceText));
    }

    public static String decrypt(String masterkey, String encryptedText) {
        byte[] parts = encryptedText.getBytes();
        salt = new String(Arrays.copyOfRange(parts, 0, 64));
        ivBase64 = Arrays.copyOfRange(parts, 64, 76);
        ivBase64 = Base64.getDecoder().decode(ivBase64);
        base64EncryptedText = new String(Arrays.copyOfRange(parts, 76, parts.length));
        key = masterkey;
        byte[] decipheredText = decodeAES_256_CBC();
        return new String(decipheredText);
    }

    private static byte[] decodeAES_256_CBC() {
        try {
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            byte[] text = base64EncryptedText.getBytes();
            GCMParameterSpec params = new GCMParameterSpec(128, ivBase64, 0, ivBase64.length);
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, params);
            return cipher.doFinal(Base64.getDecoder().decode(text));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Failed to decrypt");
        }
    }
}

Solution

  • The exception thrown is normal, you have 2 problems in your Java code:

    1- your AES key is not decoded correctly: it is wrapped in hexadecimal representation and you decode it as if it was not. You need to convert it from the hexadecimal representation to bytes, when calling SecretKeySpec().

    Replace the following line:

    SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
    

    By this one:

    SecretKeySpec skeySpec = new SecretKeySpec(Hex.decodeHex(key.toCharArray()), "AES");
    

    Note that to get access to the Hex class, you need to import org.apache.commons.codec.binary.Hex in your class file and include the corresponding Apache commons-codec library in your project.

    2- you split your base64 cipher text before having converted it to base64, this is not the correct order to do things:

    At the start of your decrypt() method, you need to first call Base64.getDecoder().decode() on your cipher text (sourceText) before splitting it into the corresponding fields.

    If you want an example of using AES-256-GCM in Java, you can look at the following example I had written some months ago: https://github.com/AlexandreFenyo/kif-idp-client

    The source code to encrypt and decrypt is in the following file: https://github.com/AlexandreFenyo/kif-idp-client/blob/master/src/kif/libfc/Tools.java

    See the methods named encodeGCM() and decodeGCM().

    Those methods are called by the main class here: https://github.com/AlexandreFenyo/kif-idp-client/blob/master/src/kif/libfc/CommandLine.java