3

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.

Community
  • 1
  • 1
Sandeep
  • 712
  • 1
  • 10
  • 22
  • "mysecretmysecret" is probably not that secret if its sitting there in the client source code. – Alex K. Apr 25 '16 at 11:36
  • @AlexK. This is just a POC. Not yet into real code. – Sandeep Apr 25 '16 at 11:38
  • Do not use strings as keys, use byte arrays. The conversion from a string to a byte array is ambiguous (BoM at the start? \0 terminator? etc.) Especially when transferring encrypted data between systems do **not** rely on system defaults. Explicitly set mode, IV/Nonce etc. You are doing little of this and are relying on defaults to be the same across systems. Any mismatched defaults will cause problems. – rossum Apr 25 '16 at 12:10
  • @rossum using byte arrays in CryptoJS (and Angular 6, at least) is a nightmare. Do you have a working sample? – afe Nov 16 '18 at 14:57
  • @afe I can't help much since I don't use CryptoJS or Angular. Perhaps ask on a group specifically for them? – rossum Nov 16 '18 at 15:41

2 Answers2

2

So I have finally solved this. Thanks to Artjom for pointing in the right direction. I have changed my Java code to use CBC with PKCS5Padding .

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec myKey = new SecretKeySpec("mysecretmysecret".getBytes(), "AES");
IvParameterSpec IVKey = new IvParameterSpec("mysecretmysecret".getBytes());
cipher.init(Cipher.ENCRYPT_MODE, myKey, IVKey);
byte[] outputBytes = cipher.doFinal(read(sampleFile));
return outputBytes;

And my javascript goes like this:

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

xhr.onload = function(e) {
    var encryptedData = this.response;
    var passwordWords = CryptoJS.enc.Utf8.parse("mysecretmysecret"); //yes I know, not a big secret!
    var wordArray = CryptoJS.lib.WordArray.create(encryptedData);
    var decrypted = CryptoJS.AES.decrypt({
        ciphertext: wordArray
    }, passwordWords, {
        iv: passwordWords, //yes I used password as iv too. Dont mind.
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    console.log(decrypted); //Eureka!!
};

xhr.send();

The decrypted is a WordArray.

Sandeep
  • 712
  • 1
  • 10
  • 22
  • love this as a starting point. "arraybuffer" matching "wordArray" saved the day. decrypted.toString(CryptoJS.enc.Utf8) can show the original text. – Cherrimon Shop Oct 09 '20 at 00:30
1

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.

Community
  • 1
  • 1
Artjom B.
  • 61,146
  • 24
  • 125
  • 222
  • thanks @Artjom . This is just a POC and i need to solve this using least possible code. Of course I know ECB is least secure but again, I will look into the hardening later. – Sandeep Apr 26 '16 at 05:19