Decrypting OpenSSL AES128CTR using Java

Using OpenSSL I can apply a symmetric 128 bit AES block cipher (RFC3686) in “counter” mode using “testpwd” as the password to produce a salted encryption of the word “testing” encoded in Base64 as follows:

echo -n testing | openssl enc -e -base64 -aes-128-ctr -pbkdf2 -salt -k testpwd
U2FsdGVkX19l6/etNkl585d+Y1XgyPc=

I can then reverse the encryption using the same password as follows:

echo "U2FsdGVkX19l6/etNkl585d+Y1XgyPc=" | openssl enc -d -base64 -aes-128-ctr -pbkdf2 -salt -k testpwd 
testing

Automated decryption

If I ever need to store a value in secret I can encrypt it like this, and decrypt whenever I need it so long as I remember the password. If an automated process in possession of the password wants to know the secret value, it could invoke OpenSSL to get it.

For example, Perl can invoke OpenSSL via its library modules:

use Crypt::CBC;
use MIME::Base64;
$cipher = Crypt::CBC->new(
  -cipher => 'Crypt::OpenSSL::AES', -chain_mode => 'ctr', -keysize => 16, -pbkdf => 'pbkdf2',
  -pass => 'tespwd'
);
print $cipher->decrypt(decode_base64('U2FsdGVkX19l6/etNkl585d+Y1XgyPc='));

Recently I wanted to do the decryption via Java (v17) without calling OpenSSL directly, deferring to an OpenSSL library or using something like BouncyCastle. I wanted to do this using off-the-shelf Java. It turned out to be a little more convoluted than I had expected, and rather educational, so I’m sharing the answer here.

The algorithm

Let me first explain a few things about the OpenSSL encryption result (U2FsdGVkX19l6/etNkl585d+Y1XgyPc=) so we can understand how it will be decrypted.

If you repeat the OpenSSL encryption multiple times with the same input and password you will get a different result each time. All of these results will decrypt correctly to the same original plaintext input. The reason each encryption is different is because of the “-salt” parameter, which tells OpenSSL to include some random salt data in the encryption. To decrypt you will need to know both the password and the salt, but as you only have the password it is necessary for the salt to be included in the output. U2FsdGVkX19l6/etNkl585d+Y1XgyPc= is a salted result. The random salt used by OpenSSL is in there somewhere.

Interestingly, while every result will be different, they will all start with these characters: U2FsdGVkX1

To make the output legible as text and not something bizarre like a binary stream it has been encoded in Base64, and if you decode that U2FsdGVkX1 blob you find that it says: Salted__

That means that the rest of the result, ignoring the “=” Base64 padding at the end, 9l6/etNkl585d+Y1XgyPc must include the salt and the encrypted input.

In fact, to make this a little clearer I originally added the parameters “-v -p” to the OpenSSL command so that it would indicate the internal values involved in the encryption. Here is the actual complete output:

echo -n testing | openssl enc -e -v -p -base64 -aes-128-ctr -pbkdf2 -salt -k testpwd
bufsize=8192
salt=65EBF7AD364979F3
key=A8BF4CE7307B05D7721712CF2EB8770A
iv =1A3E7BC156B33970BA08DA10B5A3B238
U2FsdGVkX19l6/etNkl585d+Y1XgyPc=
bytes read : 7
bytes written: 33

The salt, written out in Hex, is 65EBF7AD364979F3 (8 bytes). The OpenSSL aes-128-ctr algorithm requires an encryption key of 16 bytes, and it also has an Initialisation Vector of 16 bytes, which is an unpredictable value but not sensitive (unlike the password!). So where did all these big values come from, and how are these big values squeezed into that small encryption?

The random salt has to be included as-is in the generated encryption output. Let’s examine it in more detail:

Output from OpenSSL in Base64:
U2FsdGVkX19l6/etNkl585d+Y1XgyPc=

As text:
Salted__…unprintable…binary…

As Hex:
53616C7465645F5F65EBF7AD364979F3977E6355E0C8F7

You can see the salt within the Hex of the encryption:
53616C7465645F5F65EBF7AD364979F3977E6355E0C8F7

So that leaves the remaining part of the Hex to represent the original encrypted version of the plaintext:
977E6355E0C8F7

You will notice this is 14 hex digits, representing 7 bytes, and that the original plaintext (“testing”) was 7 characters. Under the hood, the encryption algorithm is applying XOR operations repeatedly and the eventual result will be the same length as the original. 7 characters in, 7 bytes out.

What about that 16 byte key, where did that come from? In fact, it’s produced using a hash of the password via a passphrase-based key derivation function, known as pbkdf2 (and also known as PBKDF2WithHmacSHA256 in Java, but more on that later). Early key derivation functions used iterations of MD5 as the underlying hashing function (or Message Digest), but current OpenSSL assumes by default that “-md sha256” has been specified and therefore uses the Secure Hashing Algorithm with 256 bits (SHA-256). This is important to know because a lot of the reference/sample material available online still assumes MD5 or SHA1 is lurking beneath the covers. Furthermore, OpenSSL assumes the hashing is repeated 10,000 times (by default) though you can change this via the “-iter” parameter.

Thus, in order to successfully decrypt the AES128CTR cipher you need to know the following additional things about how the plaintext was encrypted:

  • What hashing function was used? SHA-256
  • How many iterations were applied? 10,000
  • What key derivation was involved? pbkdf2 (e.g. RFC2898, PKCS#5v2.0)

Taking into account the current OpenSSL defaults, which will likely change in the future as we learn more about cryptographic practices, the complete command to generate the cipher (with random salt) would be:

echo -n testing | openssl enc -e -base64 -aes-128-ctr -pbkdf2 -salt -md sha256 -iter 10000 -k testpwd

Finally, what about the Initialisation Vector? This is an unpredictable 16 byte value associated with the key and it should come as no surprise that OpenSSL uses the key derivation function (KDF) to provide the IV. In fact, it applies the KDF to the password+salt to produce 32 bytes, the first 16 of which it uses for the key, and the remaining 16 it uses for the IV.

With all this in mind, we can now turn our attention to how to unravel an OpenSSL cipher using Java.

Java

The following solution uses Java 17 and the standard off-the-shelf libraries with which it is typically distributed. You should not need to add any additional resources to get this to work.

Here are the standard imports that are used:

import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

The javax.crypto package has been part of Java almost since the beginning, and has evolved along with the major developments in the cryptographic industry. Almost everything you might need is in there. Somewhere. Finding what you need can be a challenge. Finding good documentation more so.

Now some definitions, which you should recognise from the discussion above.

int prefixSize   = "Salted__".length(); // 8
int saltSize     = 8;                   // bytes
int keySize      = 128;                 // bits (16 bytes)
int ivSize       = 128;                 // bits (16 bytes)
int opensslIters = 10000;               // OpenSSL default for "iter"

We assume two String values, encrypted (the Base64 output from OpenSSL) and password (in the examples above this was “testpwd”). Knowing how the data in the cipher is arranged, we can now split it out into the various parts so that we can use it in the decryption algorithm:

byte[] prefixedCipherText = Base64.getDecoder().decode(encrypted);
byte[] saltedCipherText   = Arrays.copyOfRange(prefixedCipherText,prefixSize,prefixedCipherText.length);
byte[] salt               = Arrays.copyOfRange(saltedCipherText,0,saltSize);
byte[] cipherText         = Arrays.copyOfRange(saltedCipherText,salt.length,saltedCipherText.length);
char[] pwdChars           = password.toCharArray();

To use a SHA-256 KDF to generate the bytes of a secret key in Java we need to use an instance of the PBKDF2WithHmacSHA256 algorithm. The 32 generated bytes (256 bits) will actually be the key and the IV needed for the AES decryption. Here’s how we use the above data in the key generator:

byte[] ki = (SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")).generateSecret(
  new PBEKeySpec(pwdChars, salt, opensslIters, keySize+ivSize)
).getEncoded();
byte[] keyBytes = Arrays.copyOfRange(ki,0,keySize/8);
byte[] ivBytes  = Arrays.copyOfRange(ki,keySize/8,(keySize+ivSize)/8);

Now we use Java’s implementation of the AES CTR encryption, without padding.

Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, "AES"), new IvParameterSpec(ivBytes));
String plainText = new String(cipher.doFinal(cipherText),"UTF-8");

That’s it. We now have the original plaintext.

It’s far more involved than the equivalent operation in Perl, or the one-line OpenSSL command. You also can do the decryption in other languages, such as Javascript/Node, though you may need to install some additional libraries/modules and you’ll likely have many to choose from. There’s no “official” solution in many cases.

Obviously the next steps would be to streamline some of the array operations and upgrade to SHA-512 and more KDF iterations, but you need to make sure that the configurations used when generating the cipher match those applied during decryption. Conveying that additional metadata with your cipher is left as an exercise for the reader :)

Categorised as: Coding, Security

Comment Free Zone

Comments are closed.