1

I generate 128bit AES/CBC/PKCS5Padding key using Java javax.crypto API. Here is the algorithm that I use:

public static String encryptAES(String data, String secretKey) {
    try {
        byte[] secretKeys = Hashing.sha1().hashString(secretKey, Charsets.UTF_8)
                .toString().substring(0, 16)
                .getBytes(Charsets.UTF_8);

        final SecretKey secret = new SecretKeySpec(secretKeys, "AES");

        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secret);

        final AlgorithmParameters params = cipher.getParameters();

        final byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
        final byte[] cipherText = cipher.doFinal(data.getBytes(Charsets.UTF_8));

        return DatatypeConverter.printHexBinary(iv) + DatatypeConverter.printHexBinary(cipherText);
    } catch (Exception e) {
        throw Throwables.propagate(e);
    }
}


public static String decryptAES(String data, String secretKey) {
    try {
        byte[] secretKeys = Hashing.sha1().hashString(secretKey, Charsets.UTF_8)
                .toString().substring(0, 16)
                .getBytes(Charsets.UTF_8);

        // grab first 16 bytes - that's the IV
        String hexedIv = data.substring(0, 32);

        // grab everything else - that's the cipher-text (encrypted message)
        String hexedCipherText = data.substring(32);

        byte[] iv = DatatypeConverter.parseHexBinary(hexedIv);
        byte[] cipherText = DatatypeConverter.parseHexBinary(hexedCipherText);

        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secretKeys, "AES"), new IvParameterSpec(iv));

        return new String(cipher.doFinal(cipherText), Charsets.UTF_8);
    } catch (BadPaddingException e) {
        throw new IllegalArgumentException("Secret key is invalid");
    }catch (Exception e) {
        throw Throwables.propagate(e);
    }
}

I can easily encrypt and decrypt messages using secretKey with these methods. Since Java has 128bit AES encryption by default, it generates a hash of the original secret key with SHA1 and takes the first 16-bytes of the hash to use it as secret key in AES. Then it dumps the IV and cipherText in HEX format.

For example encryptAES("test", "test") generates CB5E759CE5FEAFEFCC9BABBFD84DC80C0291ED4917CF1402FF03B8E12716E44C and I want to decrypt this key with CryptoJS.

Here is my attempt:

var str = 'CB5E759CE5FEAFEFCC9BABBFD84DC80C0291ED4917CF1402FF03B8E12716E44C';

CryptJS.AES.decrypt( 
CryptJS.enc.Hex.parse(str.substring(32)),
CryptJS.SHA1("test").toString().substring(0,16),  
{
  iv: CryptJS.enc.Hex.parse(str.substring(0,32)),
  mode: CryptJS.mode.CBC,
  formatter: CryptJS.enc.Hex, 
  blockSize: 16,  
  padding: CryptJS.pad.Pkcs7 
}).toString()

However it returns an empty string.

burak emre
  • 1,501
  • 4
  • 22
  • 46
  • 1
    Java code is `PKCS5Padding`, but JS code is `pad.Pkcs7`. 5 and 7 are not the same. – Andreas May 21 '16 at 22:53
  • 1
    @Andreas Yes, it's the same: http://crypto.stackexchange.com/q/9043/13022 – Artjom B. May 21 '16 at 22:54
  • @ArtjomB. Always fun to get a link to an answer saying *"PKCS#5 padding can not be used for AES"* as proof that 5 and 7 are interchangeable for AES. – Andreas May 21 '16 at 23:00
  • 1
    @Andreas It's a relic of the naming convention for Java. It just stayed from the DES days. If you look further you will see that `RSA/ECB/PKCS1Padding` also doesn't make sense, because ECB is not applicable to RSA. – Artjom B. May 21 '16 at 23:01

2 Answers2

9

The problem is that you're using a 64 bit key as a 128 bit. Hashing.sha1().hashString(secretKey, Charsets.UTF_8) is an instance of HashCode and its toString method is described as such:

Returns a string containing each byte of asBytes(), in order, as a two-digit unsigned hexadecimal number in lower case.

It is a Hex-encoded string. If you take only 16 characters of that string and use it as a key, you only have 64 bits of entropy and not 128 bits. You really should be using HashCode#asBytes() directly.


Anyway, the problem with the CryptoJS code is manyfold:

  • The ciphertext must be a CipherParams object, but it is enough if it contains the ciphertext bytes as a WordArray in the ciphertext property.
  • The key must be passed in as a WordArray instead of a string. Otherwise, an OpenSSL-compatible (EVP_BytesToKey) key derivation function is used to derive the key and IV from the string (assumed to be a password).
  • Additional options are either unnecessary, because they are defaults, or they are wrong, because the blockSize is calculated in words and not bytes.

Here is CryptoJS code that is compatible with your broken Java code:

var str = 'CB5E759CE5FEAFEFCC9BABBFD84DC80C0291ED4917CF1402FF03B8E12716E44C';

console.log("Result: " + CryptoJS.AES.decrypt({
    ciphertext: CryptoJS.enc.Hex.parse(str.substring(32))
}, CryptoJS.enc.Utf8.parse(CryptoJS.SHA1("test").toString().substring(0,16)),  
{
  iv: CryptoJS.enc.Hex.parse(str.substring(0,32)),
}).toString(CryptoJS.enc.Utf8))
<script src="https://cdn.rawgit.com/CryptoStore/crypto-js/3.1.2/build/rollups/sha1.js"></script>
<script src="https://cdn.rawgit.com/CryptoStore/crypto-js/3.1.2/build/rollups/aes.js"></script>

Here is CryptoJS code that is compatible with the fixed Java code:

var str = 'F6A5230232062D2F0BDC2080021E997C6D07A733004287544C9DDE7708975525';

console.log("Result: " + CryptoJS.AES.decrypt({
    ciphertext: CryptoJS.enc.Hex.parse(str.substring(32))
}, CryptoJS.enc.Hex.parse(CryptoJS.SHA1("test").toString().substring(0,32)),  
{
  iv: CryptoJS.enc.Hex.parse(str.substring(0,32)),
}).toString(CryptoJS.enc.Utf8))
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>

The equivalent encryption code in CryptoJS would look like this:

function encrypt(plaintext, password){
    var iv = CryptoJS.lib.WordArray.random(128/8);
    var key = CryptoJS.enc.Hex.parse(CryptoJS.SHA1(password).toString().substring(0,32));
    var ct = CryptoJS.AES.encrypt(plaintext, key, { iv: iv });
    return iv.concat(ct.ciphertext).toString();
}

console.log("ct: " + encrypt("plaintext", "test"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
Artjom B.
  • 61,146
  • 24
  • 125
  • 222
  • I modified the code as follows: `Arrays.copyOfRange(Hashing.sha1().hashString(secretKey, Charsets.UTF_8).asBytes(), 0, 16)`, as you said, it now generates different hash but in CryptoJS I still get an empty string. Here is an example output of `encryptAES("test", "test")` `F6A5230232062D2F0BDC2080021E997C6D07A733004287544C9DDE7708975525` – burak emre May 21 '16 at 23:13
  • Thanks, now it works as expected! The reason that I used HashCode.toString() is string representation of SHA1 is same in both Java and Javascript. Why do you think that Java code is broken right now? – burak emre May 21 '16 at 23:23
  • The Java code is broken, because it uses only a 64 bit key (16 hexits). Nowadays, such a key is in the realm of brute-forcing even for private individuals. – Artjom B. May 21 '16 at 23:26
  • What would be your suggestion that would work with CryptoJS? I limit the secret key to 128bit because Java enforces that limit. In order to use 256bit I need to install JCE Unlimited Strength Jurisdiction Policy Files to JRE folder but it's not an option for me because I can't force the clients to install those files manually into their JRE folder which probably requires root permission. – burak emre May 21 '16 at 23:32
  • You're limiting the key to 16 hexits, which are not really 128 bit, but rather 64 bit in disguise. As I suggest in the first part of my answer. You should use the hash bytes directly as you did in [your first comment](http://stackoverflow.com/questions/37368710/decrypt-aes-cbc-pkcs5padding-with-cryptojs/37368832#comment62250447_37368832). It was the fix is was talking about. Everything is fine now, is it not? – Artjom B. May 22 '16 at 10:35
  • Ah, I'm not good at encodings, now I get it. I assume 128 bit enough for a safe key, right? – burak emre May 22 '16 at 12:03
  • Yes, 128 bit is perfectly fine now, but not enough once quantum computers are here. – Artjom B. May 22 '16 at 12:04
  • Thanks @Artjom, you were really helpful! – burak emre May 22 '16 at 12:05
  • @artjom-b can you please provide crypto-js encrypt example! – Raja Rama Mohan Thavalam Nov 21 '18 at 06:41
  • 1
    @Raja There you go. – Artjom B. Nov 21 '18 at 18:32
  • @artjom-b Thank you :) – Raja Rama Mohan Thavalam Nov 22 '18 at 02:29
0

This one perfectly worked for me

import * as CryptoJS from 'crypto-js';    
const SECRET_CREDIT_CARD_KEY = '1231231231231231' // 16 digits key
    
    decrypt(cipherText) {
        const iv = CryptoJS.enc.Hex.parse(this.SECRET_CREDIT_CARD_KEY);
        const key = CryptoJS.enc.Utf8.parse(this.SECRET_CREDIT_CARD_KEY);
        const result = CryptoJS.AES.decrypt(cipherText, key,
          {
            iv,
            mode: CryptoJS.mode.ECB,
          }
          )
          const final  = result.toString(CryptoJS.enc.Utf8)
          return final
      }
    
    console.log(decrypt('your encrypted text'))

using this library in Angular 8 https://www.npmjs.com/package/crypto-js