How to decrypt an encrypted AES-256 string from CryptoJS using Java? [closed]

I have an encrypted AES-256 string from CryptoJS with a passphrase. I need to decrypt it in Java but can't figure out how to do it. It seems that you need IV, key and salt to decrypt, and as in CryptoJS mainpage, the encrypted data already contains all of them, and CryptoJS can somehow parse them out of encrypted input.

Anyone know how to do that? I've seen so much example about CryptoJS - Java encrypt/decrypt but most of them use hardcoded IV/key, or just send IV/key from cryptoJS side to Java side. All I have is a passphrase, just like what this site do!


When a message is encrypted in this way:

CryptoJS.AES.encrypt("message", "passphrase")

a password-based approach is used. For that CryptoJS generates a new salt and uses this salt in conjunction with the passphrase to derive the key and IV (I've recreated a derivation function for this question).
After the ciphertext is produced a special OpenSSL formatter is used to encode the ciphertext including the used salt:

var wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext);

You need to decode the Base64 encoded string to get the byte array out if it. Then you can check if the header matches:

byte[] ctBytes = Base64.getDecoder().decode(ciphertext.getBytes("UTF-8"));
System.out.println("Is salted: " + new String(Arrays.copyOf(ctBytes, 8)).equals("Salted__"));

After that you can recover the salt and ciphertext from it:

byte[] saltBytes = Arrays.copyOfRange(ctBytes, 8, 16);
byte[] ciphertextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.length);

Derive the key+IV:

byte[] key = new byte[keySize/8];
byte[] iv = new byte[ivSize/8];
EvpKDF(password.getBytes("UTF-8"), keySize, ivSize, saltBytes, key, iv);

And decrypt the ciphertext:

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
        new IvParameterSpec(iv));
byte[] recoveredPlaintextBytes = cipher.doFinal(ciphertextBytes);
String recoveredPlaintext = new String(recoveredPlaintextBytes);

Complete code:

public static void main(String[] args) throws UnsupportedEncodingException, GeneralSecurityException {
    String ciphertext = "U2FsdGVkX1+0m/gle/XQX1shjnpveUrl1fO3oOlurPMlTks6+oQlEPfOrucihzEz";
    String plaintext = "This is some example plaintext";
    String password = "This is a very strong password";
    int keySize = 256;
    int ivSize = 128;

    // var wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext);
    byte[] ctBytes = Base64.getDecoder().decode(ciphertext.getBytes("UTF-8"));
    System.out.println("Is salted: " + Arrays.equals(Arrays.copyOf(ctBytes, 8), new byte[]{0x53, 0x61, 0x6c, 0x74, 0x65, 0x64, 0x5f, 0x5f}));
    System.out.println("Is salted: " + new String(Arrays.copyOf(ctBytes, 8)).equals("Salted__"));

    byte[] saltBytes = Arrays.copyOfRange(ctBytes, 8, 16);
    System.out.println("Salt matches: " + Arrays.equals(saltBytes, hexStringToByteArray("b49bf8257bf5d05f")));

    byte[] ciphertextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.length);
    System.out.println("CT matches: " + Arrays.equals(ciphertextBytes, hexStringToByteArray("5b218e7a6f794ae5d5f3b7a0e96eacf3254e4b3afa842510f7ceaee722873133")));

    byte[] key = new byte[keySize/8];
    byte[] iv = new byte[ivSize/8];
    EvpKDF(password.getBytes("UTF-8"), keySize, ivSize, saltBytes, key, iv);

    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
    byte[] recoveredPlaintextBytes = cipher.doFinal(ciphertextBytes);
    String recoveredPlaintext = new String(recoveredPlaintextBytes);

    System.out.println("Recovered Plaintext: " + recoveredPlaintext);
    System.out.println("Expected Plaintext: " + plaintext);
}

public static byte[] EvpKDF(byte[] password, int keySize, int ivSize, byte[] salt, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
    return EvpKDF(password, keySize, ivSize, salt, 1, "MD5", resultKey, resultIv);
}

public static byte[] EvpKDF(byte[] password, int keySize, int ivSize, byte[] salt, int iterations, String hashAlgorithm, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
    keySize = keySize / 32;
    ivSize = ivSize / 32;
    int targetKeySize = keySize + ivSize;
    byte[] derivedBytes = new byte[targetKeySize * 4];
    int numberOfDerivedWords = 0;
    byte[] block = null;
    MessageDigest hasher = MessageDigest.getInstance(hashAlgorithm);
    while (numberOfDerivedWords < targetKeySize) {
        if (block != null) {
            hasher.update(block);
        }
        hasher.update(password);
        block = hasher.digest(salt);
        hasher.reset();

        // Iterations
        for (int i = 1; i < iterations; i++) {
            block = hasher.digest(block);
            hasher.reset();
        }

        System.arraycopy(block, 0, derivedBytes, numberOfDerivedWords * 4,
                Math.min(block.length, (targetKeySize - numberOfDerivedWords) * 4));

        numberOfDerivedWords += block.length/4;
    }

    System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4);
    System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4);

    return derivedBytes; // key + iv
}

/**
 * Copied from https://stackoverflow.com/a/140861
 * */
public static byte[] hexStringToByteArray(String s) {
    int len = s.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                + Character.digit(s.charAt(i+1), 16));
    }
    return data;
}

JavaScript code:

var pw = "This is a very strong password";
var pt = "This is some example plaintext";
var encrypted = CryptoJS.AES.encrypt(pt, pw);
encrypted.toString(); // U2FsdGVkX1+0m/gle/XQX1shjnpveUrl1fO3oOlurPMlTks6+oQlEPfOrucihzEz
encrypted.salt.toString(); // b49bf8257bf5d05f
encrypted.ciphertext.toString(); // 5b218e7a6f794ae5d5f3b7a0e96eacf3254e4b3afa842510f7ceaee722873133