Ashish Pandey
Ashish Pandey

Reputation: 495

AES-256-CTR Encryption in node JS and decryption in Java

I am trying to encode in nodejs and decryption for the same in nodejs works well. But when I try to do the decryption in Java using the same IV and secret, it doesn't behave as expected.

Here is the code snippet:

Encryption in nodeJs:

   var crypto = require('crypto'),
   algorithm = 'aes-256-ctr',
   _ = require('lodash'),
   secret = 'd6F3231q7d1942874322a@123nab@392';

  function encrypt(text, secret) {
    var iv = crypto.randomBytes(16);
    console.log(iv);
    var cipher = crypto.createCipheriv(algorithm, new Buffer(secret), 
    iv);
    var encrypted = cipher.update(text);

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

    return iv.toString('hex') + ':' + encrypted.toString('hex');
}
var encrypted = encrypt("8123497494", secret);
console.log(encrypted);

And the output is:

<Buffer 94 fa a4 f4 a1 3c bf f6 d7 90 18 3f 3b db 3f b9>
94faa4f4a13cbff6d790183f3bdb3fb9:fae8b07a135e084eb91e

Code Snippet for decryption in JAVA:

public class Test {
    
    public static void main(String[] args) throws Exception {
        String s = 
   "94faa4f4a13cbff6d790183f3bdb3fb9:fae8b07a135e084eb91e";
        String seed = "d6F3231q7d1942874322a@123nab@392";

        decrypt(s, seed);
    }

    private static void decrypt(String s, String seed)
            throws NoSuchAlgorithmException, NoSuchPaddingException, UnsupportedEncodingException, InvalidKeyException,
            InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
        String parts[] = s.split(":");
        String ivString = parts[0];
        String encodedString = parts[1];
        Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");

        byte[] secretBytes = seed.getBytes("UTF-8");
        
        IvParameterSpec ivSpec = new IvParameterSpec(hexStringToByteArray(ivString));
        
        /*Removed after the accepted answer
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] thedigest = md.digest(secretBytes);*/ 
        
        SecretKeySpec skey = new SecretKeySpec(thedigest, "AES");
        
        cipher.init(Cipher.DECRYPT_MODE, skey, ivSpec);
        byte[] output = cipher.doFinal(hexStringToByteArray(encodedString));

        System.out.println(new String(output));
    }
}

Output: �s˸8ƍ�

I am getting some junk value in the response. Tried a lot of options, but none of them seem to be working. Any lead/help is appreciated.

Upvotes: 10

Views: 7463

Answers (3)

Nigrimmist
Nigrimmist

Reputation: 12338

Faced with the same task (but with 128, it easy to adapt for 256), here is working Java/NodeJs code with comments.

It's additionally wrapped to Base64 to readability, but it's easy to remove if you would like. Java side (encrypt/decrypt) :

import java.lang.Math; // headers MUST be above the first class
import java.util.Base64;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.nio.charset.StandardCharsets;

// one class needs to have a main() method
public class MyClass
{
    private static void log(String s)
    {
        System.out.print("\r\n"+s);
    }

    public static SecureRandom IVGenerator() {
       return new SecureRandom();
    }

  // arguments are passed using the text field below this editor
  public static void  main(String[] args)
  {
    String valueToEncrypt = "hello, stackoverflow!";
    String key = "3e$C!F)H@McQfTjK";

    String encrypted = "";
    String decrypted = "";

    //ENCODE part
    SecureRandom IVGenerator = IVGenerator();
    byte[] encryptionKeyRaw = key.getBytes();
    //aes-128=16bit IV block size
    int ivLength=16;
    byte[] iv = new byte[ivLength];
    //generate random vector
    IVGenerator.nextBytes(iv);

    try {
        Cipher encryptionCipher = Cipher.getInstance("AES/CTR/NoPadding");
        encryptionCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptionKeyRaw, "AES"), new IvParameterSpec(iv));
        //encrypt
        byte[] cipherText = encryptionCipher.doFinal(valueToEncrypt.getBytes());

        ByteBuffer byteBuffer = ByteBuffer.allocate(ivLength + cipherText.length);
        //storing IV in first part of whole message
        byteBuffer.put(iv);
        //store encrypted bytes
        byteBuffer.put(cipherText);
        //concat it to result message
        byte[] cipherMessage = byteBuffer.array();
        //and encrypt to base64 to get readable value
        encrypted = new String(Base64.getEncoder().encode(cipherMessage));
    } catch (Exception e) {
        throw new IllegalStateException(e);
    }
    //END OF ENCODE CODE
    log("encrypted and saved as Base64 : "+encrypted);

    ///DECRYPT CODE : 
    try {
        //decoding from base64
        byte[] cipherMessageArr = Base64.getDecoder().decode(encrypted);
        //retrieving IV from message
        iv = Arrays.copyOfRange(cipherMessageArr, 0, ivLength);
        //retrieving encrypted value from end of message
        byte[] cipherText = Arrays.copyOfRange(cipherMessageArr, ivLength, cipherMessageArr.length);
        Cipher decryptionCipher = Cipher.getInstance("AES/CTR/NoPadding");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        SecretKeySpec secretKeySpec = new SecretKeySpec(encryptionKeyRaw, "AES");
        decryptionCipher.init(Cipher.DECRYPT_MODE,secretKeySpec , ivSpec);
        //decrypt
        byte[] finalCipherText = decryptionCipher.doFinal(cipherText);
        //converting to string
        String finalDecryptedValue = new String(finalCipherText);
        decrypted = finalDecryptedValue;
    } catch (Exception e) {
        throw new IllegalStateException(e);
    }
    log("decrypted from Base64->aes128 : "+decrypted);
    //END OF DECRYPT CODE

  }
}

It could be easy be tested by online java compilers (this example prepared on https://www.jdoodle.com/online-java-compiler).

NodeJs decrypt side :

const crypto = require('crypto');
const ivLength = 16;
const algorithm = 'aes-128-ctr';

const encrypt = (value, key) => {
    //not implemented, but it could be done easy if you will see to decrypt
    return value;
};

function decrypt(value, key) {
    //from base64 to byteArray
    let decodedAsBase64Value = Buffer.from(value, 'base64');        
    let decodedAsBase64Key = Buffer.from(key);
    //get IV from message
    let ivArr = decodedAsBase64Value.slice(0, ivLength);
    //get crypted message from second part of message
    let cipherTextArr = decodedAsBase64Value.slice(ivLength, decodedAsBase64Value.length);
    let cipher = crypto.createDecipheriv(algorithm, decodedAsBase64Key, ivArr);
    //decrypted value
    let decrypted = cipher.update(cipherTextArr, 'binary', 'utf8');
    decrypted += cipher.final('utf8');
    return decrypted;
}

Upvotes: 0

Ilmari Karonen
Ilmari Karonen

Reputation: 50328

In your JS code, you're using the 32-character string d6F3231q7d19428743234@123nab@234 directly as the AES key, with each ASCII character directly mapped to a single key byte.

In the Java code, you're instead first hashing the same string with MD5, and then using the MD5 output as the AES key. It's no wonder that they won't match.

What you probably should be doing, in both cases, is either:

  1. randomly generating a string of 32 bytes (most of which won't be printable ASCII characters) and using it as the key; or
  2. using a key derivation function (KDF) to take an arbitrary input string and turn it into a pseudorandom AES key.

In the latter case, if the input string is likely to have less than 256 bits of entropy (e.g. if it's a user-chosen password, most of which only have a few dozen bits of entropy at best), then you should make sure to use a KDF that implements key stretching to slow down brute force guessing attacks.


Ps. To address the comments below, MD5 outputs a 16-byte digest, which will yield an AES-128 key when used as an AES SecretKeySpec. To use AES-256 in Java, you will need to provide a 32-byte key. If trying to use a 32-byte AES key in Java throws an InvalidKeyException, you are probably using an old version of Java with a limited crypto policy that does not allow encryption keys longer than 128 bits. As described this answer to the linked question, you will either need to upgrade to Java 8 update 161 or later, or obtain and install an unlimited crypto policy file for your Java version.

Upvotes: 4

Luke Joshua Park
Luke Joshua Park

Reputation: 9795

In the Java code you are taking the MD5 hash of secret before using it as a key:

MessageDigest md = MessageDigest.getInstance("MD5");
byte[] thedigest = md.digest(secretBytes);
SecretKeySpec skey = new SecretKeySpec(thedigest, "AES");

Whereas, in your NodeJS code, you don't do this anywhere. So you're using two different keys when encrypting and decrypting.

Don't copy and paste code without understanding it. Especially crypto code.

Upvotes: 2

Related Questions