0

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.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • There are many things wrong here. First, the output of the cipher (i.e. after *encryption*) should be a multiple of the block size, so the input is incorrect. I don't know how PHP manages to decrypt that. OpenSSL uses PKCS#7 padding by default (incorrectly called `"PKCS5Padding"` by Java). I don't see any of the hash calculations over the IV. I don't know why you are base 64 encoding the result either. – Maarten Bodewes Mar 22 '23 at 07:04
  • The PHP code is not code I wrote. It was an example given to me so that I can implement decryption is Java. Just to clarify, you are saying that the input (i.e. 'bnY0UEc2NFcySHgwRTIyNFU1NU5pUT09') is incorrect? That input is 32 characters long so it should be fine. The output is what is confusing me, 12 characters does not seem like correct length there, unless it was truncated somewhere. Also, I think no padding is used in the PHP example. The 0 passed in the fourth parameter of the openssl_decrypt function signifies no padding if I am not mistaken. – Mohamed E. Mar 22 '23 at 07:15
  • 1
    The PHP code Base64 decodes twice (implicit by default and explicit): bnY0UEc2NFcySHgwRTIyNFU1NU5pUT09 -> nv4PG64W2Hx0E224U55NiQ== which corresponds to a 16 bytes ciphertext (the double Base64 encoding is unnecessary and inefficient). – Topaco Mar 22 '23 at 07:18
  • Not sure about the double encoding. First time it is encoded implicitly then it is decoded not encoded again. I would like to retract my statement in the last comment. After the original cipher (bnY0UEc2NFcySHgwRTIyNFU1NU5pUT09) is base64 decoded it gives an incorrect length of 24 bytes - which is exactly my problem. That won't work with no padding. – Mohamed E. Mar 22 '23 at 07:24
  • *...it gives an incorrect length of 24 bytes - which is exactly my problem...*: The Base64 decoding gives nv4PG64W2Hx0E224U55NiQ==, repeated Base64 decoding results in a 16 bytes value, hex encoded: 9efe0f1bae16d87c74136db8539e4d89. This is the ciphertext to decrypt. – Topaco Mar 22 '23 at 07:29
  • Further differences to the Java code: Besides the IV, the PHP code also derives the key via SHA256. Furthermore, the hex encoded values are used as key and IV. – Topaco Mar 22 '23 at 07:32
  • Also, don't do `keyString.substring(0,32).getBytes();`, you should always explicitly specify the character set you want to use to encode the string to bytes. – Mark Rotteveel Mar 22 '23 at 08:01
  • 1
    If the problem persists: Post test data: `$secret_key`, `$secret_iv`, `$encrypted_txt` plus expected plaintext. Also post the `generateSHA256()` implementation. – Topaco Mar 22 '23 at 08:22
  • Working on it now, if it persists I'll post test data. I got the hex encoded value you mentioned above to use as ciphertext (9efe0f1bae16d87c74136db8539e4d89). But struggling to understand what you meant by hex encoding the key and IV? Technically since they are generated using SHA-256, they are already hex encoded I believe. I just convert them to bytes (using StandardCharsets.US_ASCII) and use them that way. Note that the key MUST be 32 bytes in length for me to use AES-256 bit decryption here. Also, what about the output? Should that be a base64 encoded string? – Mohamed E. Mar 22 '23 at 08:32
  • Not understanding the difference between binary (bits / bytes) and hexadecimals seem to be an issue here. Hexadecimals are the "human readable" / "textual" representation of the bytes. If you have them in a string in your application you should **hex** decode before using them as bytes instead of using character decoding with UTF-8 or whatever. – Maarten Bodewes Mar 22 '23 at 08:44
  • From the notes under the underspecified `openssl_decrypt` function: "$data can be as the description says raw or base64. If no $option is set (this is, if value of 0 is passed in this parameter), data will be assumed to be base64 encoded. If parameter OPENSSL_RAW_DATA is set, it will be understood as row data." So undoubtedly the same base64 default is used instead of raw in the encrypt function as well. Yes, PHP OpenSSL library doesn't adhere to the principle of least surprise. But so does the entire PHP runtime, all the more reason to switch to e.g. Java. – Maarten Bodewes Mar 22 '23 at 08:52

1 Answers1

0

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.
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • It worked! Minor comment on your implementation. For some reason it refused to work when the key and IV were generated using your hash function, giving me the below error: javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. When I used a different SHA-256 hash function it worked fine. The only difference between the two was that you used capital letters and the other function didn't. Not sure how that affects anything - it shouldn't, but it did. Either way, happy it worked! I appreciate your help a lot! – Mohamed E. Mar 22 '23 at 10:59
  • @MohamedE. - My `generateSHA256()` implementation uses *lowercase* letters (s. [here](https://www.jdoodle.com/ia/FAn)). Maybe you have used another hex encoding for compatibility reasons, one with uppercase letters. That would cause such an issue, s. *cross-platform compatibility issues can occur (case sensitivity of the hex digits)* in the *vulnerabilty* section. I have added a remark to this effect when introducing `generateSHA256()` to make this clearer. – Topaco Mar 22 '23 at 16:30