Search code examples
javaencryptioncryptographyaescryptojs

CryptoJS decrypting (AES) a file bytearray coming from Java


I am encrypting a file in Java and need to decrypt it at client side. This is the server side code:

Key secretKey = new SecretKeySpec("mysecretmysecret".getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] outputBytes = cipher.doFinal(read(sampleFile));
return outputBytes;

At client side I use Ajax request to fetch the file and use CryptoJS AES:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'file', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function (e) {
        var encryptedData = this.response;
        var decrypted = CryptoJS.AES.decrypt(encryptedData, "mysecretmysecret");
        console.log(decrypted);
};
xhr.send();

But this does not decrypt the file. I get this printed as value of decrypted in the console:

W…y.init {words: Array[0], sigBytes: 0}

I have also tried converting arraybuffer to WordArray suggested here but still the same result. I would be more than glad if someone could point me in the right direction and tell me what I did wrong.

Edit 1: I have solved the issue. The code I used is posted as an answer.


Solution

  • Let's recap, in Java you're using

    • AES,
    • ECB (not specified but most often the default; it's insecure!),
    • PKCS#7 padding (not specified but most often the default; it's the same as PKCS#5 padding), and
    • a password of 16 characters as a key of 16 bytes (depending on the default system encoding).

    If the key is passed as a string to CryptoJS, it will have to derive the key from the assumed password using OpenSSL's EVP_BytesToKey with a single round of MD5. Since your ciphertext is not encoded in an OpenSSL-compatible format, it will fail. The thing is, you don't need that.

    The following code would decrypt the ciphertext that is coming from Java correctly, but it's not very secure:

    var passwordWords = CryptoJS.enc.Utf8.parse("mysecretmysecret");
    var decrypted = CryptoJS.AES.decrypt({
        ciphertext: CryptoJS.lib.WordArray.create(encryptedData) // or use some encoding
    }, passwordWords, {
        mode: CryptoJS.mode.ECB
    });
    console.log(decrypted.toString(CryptoJS.enc.Utf8)); // in case the plaintext is text
    

    The ECB mode is not included in the basic rollup, so you will have to include that JavaScript file in your page after the main CryptoJS file.
    Also, CryptoJS doesn't handle an ArrayBuffer by default. You need to include the shim for that (source: this answer).


    The problem with this is its insecurity. ECB mode is very insecure.

    • Never use ECB mode. It's deterministic and therefore not semantically secure. You should at the very least use a randomized mode like CBC or CTR. The IV/nonce is not secret, so you can send it along with the ciphertext. A common way is to put it in front of the ciphertext.

    • It is better to authenticate your ciphertexts so that attacks like a padding oracle attack are not possible. This can be done with authenticated modes like GCM or EAX, or with an encrypt-then-MAC scheme.

    • Keys can be derived from passwords, but a proper scheme such as PBKDF2 should be used. Java and CryptoJS both support these.