javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes - AES_256/CBC/NoPadding

510 Views Asked by At

I am trying to decrypt an AES cipher in Java. The cipher was encrypted/decrypted in PHP using the openssl_encrypt/openssl_decrypt function.

An example of decryption in PHP looks like this:

function decryptSerial($encrypted_txt){
  $encrypt_method = 'AES-256-CBC';                
  $key = hash('sha256', $secret_key);        

  //iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning          
  $iv = substr(hash('sha256', $secret_iv), 0, 16);        

  return openssl_decrypt(base64_decode($encrypted_txt), $encrypt_method, $key, 0, $iv);        
}

echo decryptSerial('bnY0UEc2NFcySHgwRTIyNFU1NU5pUT09');  //output is MXeaSFSUj4az

The PHP code uses AES-256-CBC with no padding to decrypt so I do the same in Java:

public static String decryptAES256CBC(String cipherText, String keyString, String ivString){
    try {
        // Truncate the key at the first 32 bytes
        byte [] keyBytes = keyString.substring(0,32).getBytes();
        byte [] ivBytes = ivString.getBytes();

        SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
        IvParameterSpec iv = new IvParameterSpec(ivBytes);

        Cipher cipher = Cipher.getInstance("AES_256/CBC/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key, iv);

        byte [] decodedCipher = java.util.Base64.getDecoder().decode(cipherText);
        byte[] plainText = cipher.doFinal(decodedCipher);

        return java.util.Base64.getEncoder().encodeToString(plainText);

    } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException |
             InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
        throw new RuntimeException(e);
    }
}

public static void main(String [] args){
    String key = generateSHA256("*****");
    String iv = generateSHA256("******").substring(0,16);

    System.out.println(decryptAES256CBC("bnY0UEc2NFcySHgwRTIyNFU1NU5pUT09", key, iv));
}

This however does not work. When I run it with the example input, I get the error mentioned in the title. It seems like by input cipher is not of the correct length - which is true, when I base64 decode the cipher I get a byte array of length 24 a.k.a not a multiple of 16. This would require padding to get things to work I believe. But then how does the PHP code do it without padding?

I tried recreating the PHP code in Kava. I researched the openssl_decrypt function and ported its functionality. However, when I ran it in Java, it seems like I need padding. The PHP code used no padding if I am not mistaken.

1

There are 1 best solutions below

2
Topaco On BEST ANSWER

In the PHP code, key and IV are derived using SHA256, hex encoded, and these values are applied UTF-8 encoded as key and IV. The key is implicitly truncated to 32 bytes (by openssl_decrypt()) and the IV explicitly to 16 bytes. In Java, both truncations must be performed explicitly, e.g.:

private static String generateSHA256(String passphrase, int size) throws Exception {
    byte[] digest = MessageDigest.getInstance("SHA256").digest(passphrase.getBytes(StandardCharsets.UTF_8));
    return HexFormat.of().formatHex(digest).substring(0,size); // see regarding the hex encoding for older Java versions e.g. here: https://stackoverflow.com/a/9855338/9014097. Note that for compatibility with the PHP code, the implementation must be changed to apply lowercase hex digits.
}

Remark: The PHP code and the generateSHA256() implementation above apply lowercase hex digits. For older Java versions that did not support HexFormat, care must be taken that the hex encoding also applies lowercase hex digits (this is in particular true for the hex encoding variants from here).


Furthermore, in the PHP code the ciphertext is Base64 decoded twice: implicit by default and explicit. As padding PKCS#7 is applied by default. For a detailed description of the default values related to Base64 and padding, see the options parameter and constants.

A possible Java implementation is as follows:

public static String decryptAES256CBC(String cipherText, String keyString, String ivString) throws Exception {
        
    byte [] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
    byte [] ivBytes = ivString.getBytes(StandardCharsets.UTF_8);

    SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
    IvParameterSpec iv = new IvParameterSpec(ivBytes);

    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // PKCS#7 padding is called PKCS5 padding in the Java world
    cipher.init(Cipher.DECRYPT_MODE, key, iv);

    byte [] decodedCipher = java.util.Base64.getDecoder().decode(cipherText);
    byte [] decodedCipher2nd = java.util.Base64.getDecoder().decode(decodedCipher); // 2nd Base64 decoding
    byte[] plainText = cipher.doFinal(decodedCipher2nd);

    return new String(plainText, StandardCharsets.UTF_8);
}

Test:

String key = generateSHA256("secretkey", 32).substring(0,32);
String iv = generateSHA256("secretiv", 16).substring(0,16);
System.out.println(decryptAES256CBC("UTY5b1pGUHYvTW5tL0pJcTVVZktuUT09", key, iv)); // MXeaSFSUj4az

In accordance with the result of the PHP code.


The PHP code has some vulnerabilities and inefficiencies (even if you can't change this, it should be noted for future readers):

  • For deriving the key, not a fast digest but a key derivation function like Argon2 or at least PBKDF2 should be used.
  • The IV should be randomly generated during encryption and passed to the decrypting side (usually concatenated).
  • Using the hex encoded value as key reduces security because each byte of 256 possible values are reduced to only 16 values.
    Also, cross-platform compatibility issues can occur (case sensitivity of the hex digits).
  • The double Base64 encoding of the ciphertext is unnecessary and inefficient.
  • It looks like the plaintext is Base64 encoded as well. This is not necessary because the algorithms operate on bytes. A base64 encoding only increases the amount of data.