Decrypt AES/CBC/PKCS5Padding with CryptoJS
Asked Answered
V

2

2

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.

Vassili answered 21/5, 2016 at 22:42 Comment(4)
Java code is PKCS5Padding, but JS code is pad.Pkcs7. 5 and 7 are not the same.Paperback
@Paperback Yes, it's the same: crypto.stackexchange.com/q/9043/13022Tuberculate
@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.Paperback
@Paperback 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.Tuberculate
T
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>
Tuberculate answered 21/5, 2016 at 22:59 Comment(11)
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") F6A5230232062D2F0BDC2080021E997C6D07A733004287544C9DDE7708975525Vassili
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?Vassili
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.Tuberculate
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.Vassili
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. It was the fix is was talking about. Everything is fine now, is it not?Tuberculate
Ah, I'm not good at encodings, now I get it. I assume 128 bit enough for a safe key, right?Vassili
Yes, 128 bit is perfectly fine now, but not enough once quantum computers are here.Tuberculate
Thanks @Artjom, you were really helpful!Vassili
@artjom-b can you please provide crypto-js encrypt example!Cletis
@Raja There you go.Tuberculate
@artjom-b Thank you :)Cletis
I
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

Intrench answered 9/7, 2020 at 14:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.