1

So, we are using AES GCM encryption & decryption in nodejs as follows, We need to use this in Java. So one can encrypt in Java and decrypt in nodeJs and vice versa.

here is encrypt decrypt function in node

const encrypt = (text, masterkey) => {
    // random initialization vector
    const iv = crypto.randomBytes(16);

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

    // derive encryption key: 32 byte key length
    // in assumption the masterkey is a cryptographic and NOT a password there is no need for
    // a large number of iterations. It may can replaced by HKDF
    // the value of 2145 is randomly chosen!
    const key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512');

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

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

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

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

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

    // convert data to buffers
    const salt = bData.slice(0, 64);
    const iv = bData.slice(64, 80);
    const tag = bData.slice(80, 96);
    const text = bData.slice(96);

    // derive key using; 32 byte key length
    const key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512');

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

    // encrypt the given text
    return decipher.update(text, 'binary', 'utf8') + decipher.final('utf8');
};

Here's What I have done in Java, but getting input too short exception. I have also tried to mock above nodeJs encrypt function, but it seems to be not working. I have got initialization vector(IV), salt, key as same as nodejs (in java it is signed 128 bits)

public static String decrypt(String encData, String masterKey)
    {
        var cipherText = Base64.getDecoder().decode(encData.getBytes(StandardCharsets.UTF_8));
        var salt = Arrays.copyOfRange(cipherText, 0, 64);
        var iv = Arrays.copyOfRange(cipherText, 64, 80);
        var tag = Arrays.copyOfRange(cipherText, 80, 96);
        var ciphertext = Arrays.copyOfRange(cipherText, 96, cipherText.length);
        var key = getKeyFromPassword(masterKey, salt);
    //GCM_TAG_LENGTH = 16
        return helper("AES/GCM/NoPadding", ciphertext, tag, key, new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv));
    }

public static SecretKey getKeyFromPassword(String masterKey, byte[] salt) {

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
        KeySpec spec = new PBEKeySpec(masterKey.toCharArray(), salt, 2145, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
                .getEncoded(), "AES");
        return secret;
    }

    public static String helper(String algorithm, byte[] cipherText, SecretKey key,
                                 GCMParameterSpec gcmParameterSpec){
          Cipher cipher = Cipher.getInstance(algorithm);
          cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
          cipher.update(cipherText);
          byte[] plainText = cipher.doFinal(tag);
          return new String(plainText, StandardCharsets.UTF_8);
    }

UPDATE: above code works for decryption in java

But now need to encrypt in java

here's what i am doing, but getting Tag mismatch! exception while decryption.I also change the order of tag & cipherText but still same error occured.

public static String encrypt(String text, String masterKey)
    {
        var iv = generateIv(16);
        var salt = generateIv(64);
        var key = getKeyFromPassword(masterKey, salt);
        var cipher = helper1("AES/GCM/NoPadding", text, key,  new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv));
        var outputStream = new ByteArrayOutputStream();
        var tag = Arrays.copyOfRange(cipher, 0, 16);
        var ciphertext = Arrays.copyOfRange(cipher, 16, cipher.length);
        outputStream.write(salt);
        outputStream.write(iv);
        outputStream.write(tag);
        outputStream.write(ciphertext);
        return Base64.getEncoder().encodeToString(outputStream.toByteArray());
    }

public static byte[] generateIv(int N) {
        byte[] iv = new byte[N];
        new SecureRandom().nextBytes(iv);
        return iv;
    }
    
public static byte[] helper1(String algorithm, String input, SecretKey key,
                                 GCMParameterSpec gcmParameterSpec){

        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
        return cipher.doFinal(input.getBytes());
    }

public static SecretKey getKeyFromPassword(String masterKey, byte[] salt) {

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
        KeySpec spec = new PBEKeySpec(masterKey.toCharArray(), salt, 2145, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
                .getEncoded(), "AES");
        return secret;
    }

Uday Solanki
  • 39
  • 2
  • 7
  • Please post your recent Java code and describe the problem. – Topaco Jul 23 '21 at 11:31
  • Java expects the tag concatenated with the ciphertext: `ciphertext|tag`. What do you do with the tag? The `helper()`-call passes 5 arguments, but the `helper()` function defines only 4 arguments – Topaco Jul 23 '21 at 11:39
  • @Topaco I was just hit and trying by sending tag in helper function, but not working – Uday Solanki Jul 23 '21 at 11:43
  • There may be other problems, but the concatenation of ciphertext and tag (in that order) must be passed to `doFinal()` (which is why the `salt|IV|ciphertext|tag` concatenation order would be more efficient). – Topaco Jul 23 '21 at 11:47
  • Checked this. If you pass the concatenation of ciphertext and tag in `doFinal()`, decryption works. – Topaco Jul 23 '21 at 11:59
  • Note that the recommended size of the IV/nonce for GCM is 12 bytes. – Topaco Jul 23 '21 at 12:02
  • Yes, it worked, thanks @Topaco. Is there any way we can also encrypt as same in nodeJs for Java too – Uday Solanki Jul 23 '21 at 12:22
  • I have answered , thanks again @Topaco, you are a saviour !! _/\_ – Uday Solanki Jul 23 '21 at 12:35
  • I get "Unsupported state or unable to authenticate data" when i encrypt from Android and send it to node js to decrypt. – Henock Bongi Apr 28 '22 at 16:05
  • 1
    It's worked! I put GCM tag on 12 instead of 16. It's worked!! – Henock Bongi Apr 29 '22 at 12:39

1 Answers1

2

So, I am putting Java version of encrypt & decrypt functions here

public static byte[] generateIv(int N) {
        byte[] iv = new byte[N];
        new SecureRandom().nextBytes(iv);
        return iv;
    }

    
    public static SecretKey getKeyFromPassword(String masterKey, byte[] salt) {

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
        KeySpec spec = new PBEKeySpec(masterKey.toCharArray(), salt, 2145, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
                .getEncoded(), "AES");
        return secret;
    }

    
    public static byte[] encryptHelper(String algorithm, String input, SecretKey key,
                                 GCMParameterSpec gcmParameterSpec){

        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
        return cipher.doFinal(input.getBytes());
    }

    @SneakyThrows
    public static String decryptHelper(String algorithm, byte[] cipherText, byte[] tag,SecretKey key,
                                 GCMParameterSpec gcmParameterSpec){
          Cipher cipher = Cipher.getInstance(algorithm);
          cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
          cipher.update(cipherText);
          byte[] plainText = cipher.doFinal(tag);
          return new String(plainText, StandardCharsets.UTF_8);
    }

    
    public static String encrypt(String text, String masterKey)
    {
        var iv = generateIv(16);
        var salt = generateIv(64);
        var key = getKeyFromPassword(masterKey, salt);
        var cipher = encryptHelper("AES/GCM/NoPadding", text, key,  new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv));
        var outputStream = new ByteArrayOutputStream();
        var ciphertext = Arrays.copyOfRange(cipher, 0, text.length());
        var tag = Arrays.copyOfRange(cipher, text.length(), cipher.length);
        outputStream.write(salt);
        outputStream.write(iv);
        outputStream.write(tag);
        outputStream.write(ciphertext);
        return Base64.getEncoder().encodeToString(outputStream.toByteArray());
    }

    
    public static String decrypt(String encData, String masterKey)
    {
        var cipherText = Base64.getDecoder().decode(encData.getBytes(StandardCharsets.UTF_8));
        var salt = Arrays.copyOfRange(cipherText, 0, 64);
        var iv = Arrays.copyOfRange(cipherText, 64, 80);
        var tag = Arrays.copyOfRange(cipherText, 80, 96);
        var ciphertext = Arrays.copyOfRange(cipherText, 96, cipherText.length);
        var key = getKeyFromPassword(masterKey, salt);
        return decryptHelper("AES/GCM/NoPadding", ciphertext, tag, key, new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv));
    }

Uday Solanki
  • 39
  • 2
  • 7
  • `var ciphertext = Arrays.copyOfRange(cipher, 0, 4);` Uh, did you test that properly? Why is your salt 64 bytes? Beware of generic wrapper libraries, I've quit using them altogether. – Maarten Bodewes Jul 24 '21 at 16:27
  • @MaartenBodewes Checked it is working properly. Also this is with respect to above nodeJs encryption/ decryption functions. – Uday Solanki Jul 26 '21 at 07:59
  • When i encrypt from node js and send to Android i get this error "mac check in GCM failed". What can i do for this? – Henock Bongi Apr 28 '22 at 16:07