Search code examples
javaencryptionaescryptojs

Cannot replicate Encryption AES-CBC (Java) with CryptoJS (Javascript)


Goal: Replicate Java-AES-CBC Encryption/Decryption with CryptoJS (Javascript)

Why?: Create an Encrypted String (Base64 or HEX) with CryptoJS and decode with Java (different Server env.)

Problem: I cannot get the CryptoJS part to produce the same Base64 (or HEX) encoded String as the Java. As an effect, with the current state of code, there is no way for the encryption / decryption to be handed off to separate parties.

My Java code outputs following:

SECRET_KEY: 431.396
SALT: d413321c765f563c8288ce281ae6808d
originalString: test123_XXX
encryptedString: suBMmUzhQMtuAboORtvr6g==
encryptedStringHex: 7375424d6d557a68514d747541626f4f5274767236673d3d
decryptedString: test123_XXX

My Javascript code outputs the following

[Log] SECRET_KEY: 431.396
[Log] SALT: d413321c765f563c8288ce281ae6808d
[Log] originalString: test123_XXX
[Log] Encrypted: l+R6aYgrQI2+RAIH+X/iJw==
[Log] encryptedHex: 97E47A69882B408DBE440207F97FE227
[Log] Decrypted: test123_XXX

Here is the complete Java code I am using:

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Base64;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import java.security.*;
import org.apache.commons.codec.binary.Hex;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.spec.KeySpec;


public class AES {
    
    public static String encrypt(String strToEncrypt, String SECRET_KEY, String SALT) {
        try {
            byte[] iv = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
            IvParameterSpec ivspec = new IvParameterSpec(iv);
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            KeySpec spec = new PBEKeySpec(SECRET_KEY.toCharArray(), SALT.getBytes(), 65536, 256);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivspec);
            return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            System.out.println("Error while encrypting: " + e.toString());
        }
        return null;
    }
    
    public static String decrypt(String strToDecrypt, String SECRET_KEY, String SALT) {
        try {
            byte[] iv = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
            IvParameterSpec ivspec = new IvParameterSpec(iv);
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            KeySpec spec = new PBEKeySpec(SECRET_KEY.toCharArray(), SALT.getBytes(), 65536, 256);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivspec);
            return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)));
        } catch (Exception e) {
            System.out.println("Error while decrypting: " + e.toString());
        }
        return null;
    }
    
    ///
    public static void main(String args[]) {
      
        String originalString = "test123_XXX";
        String SECRET_KEY = "431.396";
        String SALT = "d413321c765f563c8288ce281ae6808d";
         
        String encryptedString = AES.encrypt(originalString, SECRET_KEY, SALT);
        String encryptedStringHex = Hex.encodeHexString(encryptedString.getBytes());
        String decryptedString = AES.decrypt(encryptedString, SECRET_KEY, SALT);
    
        System.out.println("SECRET_KEY: " + SECRET_KEY);
        System.out.println("SALT: " + SALT);
        System.out.println("originalString: " + originalString);
        System.out.println("encryptedString: " + encryptedString);
        System.out.println("encryptedStringHex: " + encryptedStringHex);
        System.out.println("decryptedString: " + decryptedString);
    }
}

Here is the complete Javascript code I am using:

var SECRET_KEY = "431.396";
var SALT = "d413321c765f563c8288ce281ae6808d";
var originalString = "test123_XXX";

var iv = '0000000000000000';
iv = CryptoJS.enc.Hex.parse(iv);

var iterations = 65536;
var keySize = 256;

function encrypt (msg) {
  
  var key = CryptoJS.PBKDF2(SECRET_KEY, SALT, {
      keySize: keySize/32,
      iterations: iterations
    });

  var encrypted = CryptoJS.AES.encrypt(msg, key, { 
    iv: iv, 
    padding: CryptoJS.pad.Pkcs7,
    mode: CryptoJS.mode.CBC,
    hasher: CryptoJS.algo.SHA256
  });
  
  return encrypted;
}

function decrypt (encrypted) {
  
  var key = CryptoJS.PBKDF2(SECRET_KEY, SALT, {
      keySize: keySize/32,
      iterations: iterations
    });

  var decrypted = CryptoJS.AES.decrypt(encrypted, key, { 
    iv: iv, 
    padding: CryptoJS.pad.Pkcs7,
    mode: CryptoJS.mode.CBC,
    hasher: CryptoJS.algo.SHA256
  });
  
  return decrypted;
}

var encrypted = encrypt(originalString);
var encryptedHex = encrypted.ciphertext.toString(CryptoJS.enc.Hex).toUpperCase();
var decrypted = decrypt(encrypted);

console.log("SECRET_KEY: "+ SECRET_KEY);
console.log("SALT: "+ SALT);
console.log("originalString: "+ originalString);
console.log("Encrypted: "+ encrypted);
console.log("encryptedHex: "+ encryptedHex);
console.log("Decrypted: "+ decrypted.toString(CryptoJS.enc.Utf8) );
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/aes.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/enc-hex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/md5.min.js"></script>


Solution

  • The hash algorithm is part of the key derivation function. Therefore, in the CryptoJS code, the hash algorithm must be specified in the key derivation function and not within the encrypt()/decrypt() calls:

    var SECRET_KEY = "431.396";
    var SALT = "d413321c765f563c8288ce281ae6808d";
    var originalString = "test123_XXX";
    
    var iv = '00000000000000000000000000000000'; // 16 bytes correspons to 32 hex digits
    iv = CryptoJS.enc.Hex.parse(iv);
    
    var iterations = 65536;
    var keySize = 256;
    
    function encrypt (msg) {
      
      var key = CryptoJS.PBKDF2(SECRET_KEY, SALT, {
          keySize: keySize/32,
          iterations: iterations,
          hasher: CryptoJS.algo.SHA256 // the hash algorithm is part of the key derivation
        });
    
      var encrypted = CryptoJS.AES.encrypt(msg, key, { 
        iv: iv, 
        padding: CryptoJS.pad.Pkcs7, // default
        mode: CryptoJS.mode.CBC // default
      });
      
      return encrypted;
    }
    
    function decrypt (encrypted) {
      
      var key = CryptoJS.PBKDF2(SECRET_KEY, SALT, {
          keySize: keySize/32,
          iterations: iterations,
          hasher: CryptoJS.algo.SHA256 // the hash algorithm is part of the key derivation
        });
    
      var decrypted = CryptoJS.AES.decrypt(encrypted, key, { 
        iv: iv, 
        padding: CryptoJS.pad.Pkcs7, // default
        mode: CryptoJS.mode.CBC // default
      });
      
      return decrypted;
    }
    
    var encrypted = encrypt(originalString);
    var encryptedHex = encrypted.ciphertext.toString(CryptoJS.enc.Hex).toUpperCase();
    var decrypted = decrypt(encrypted);
    
    console.log("SECRET_KEY: "+ SECRET_KEY);
    console.log("SALT: "+ SALT);
    console.log("originalString: "+ originalString);
    console.log("Encrypted: "+ encrypted);
    console.log("encryptedHex: "+ encryptedHex);
    console.log("Decrypted: "+ decrypted.toString(CryptoJS.enc.Utf8) );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

    With this change the CryptoJS code generates the same ciphertext as the Java code (compare for this the Base64 encoded ciphertexts).


    The hex encoded ciphertexts still differ because in the Java code the Base64 encoded ciphertext is hex encoded. This double encoding is unnecessary and should be avoided. E.g. with the following change in the Java code:

    String encryptedStringHex = Hex.encodeHexString(Base64.getDecoder().decode(encryptedString.getBytes(StandardCharsets.US_ASCII)));
    

    these values also match (except for the upper/lower case).
    Consider encoding and decoding outside of encrypt() and decrypt() so that byte arrays are passed or returned instead of strings.


    Note that the IV for AES is 16 bytes in size, which means the hex encoded value consists of 32 hex digits. In the CryptoJS code you use only 16 hex digits, which only works because a zero IV is applied and CryptoJS implicitly pads the too short IV with 0x00 values to the correct length.


    Regarding security: A static salt and IV are vulnerabilities. The correct way is to generate a random IV and salt during encryption and pass both together with the ciphertext to the decrypting side, typically concatenated (IV and salt are not a secret).
    Furthermore, when decoding with new String() and encoding with getBytes(), the encoding should always be specified, otherwise the default encoding is used, which may not correspond to the intended encoding.