Skip to content
Advertisement

Java AES Decryption with keyFile using BouncyCastle SSL

I am trying to write a Java code decrypt a file encrypted with AES256 using BouncyCastle compatible with OpenSSL decryption.

s_key is the file provided which contains the key that will be used to encrypt and decrypt

Steps to be done: 1 – Read the key file 2 – Use the key provided to decrypt file inputfilename

Below I have use so far but I am getting error:

import java.io.*;
import java.nio.charset.StandardCharsets;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import org.apache.commons.io.FileUtils;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.generators.OpenSSLPBEParametersGenerator;
import org.bouncycastle.crypto.io.CipherOutputStream;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.ParametersWithIV;

import javax.crypto.NoSuchPaddingException;


public class test5_encrypt {

    public static void main(String[] args) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {


        File file = new File("/home/roxane/key");
        String passwordStr = FileUtils.readFileToString(file, "UTF-8");

        String outputPath = "/home/roxane/test1";
        String inputPath = "/home/roxane/test";


        SecureRandom random = new SecureRandom();
        byte salt[] = new byte[8];
        random.nextBytes(salt);

// Derive 32 bytes key (AES_256) and 16 bytes IV
        byte[] password = passwordStr.getBytes(StandardCharsets.UTF_8);
        OpenSSLPBEParametersGenerator pbeGenerator = new OpenSSLPBEParametersGenerator(new MD5Digest()); // SHA256 as of v1.1.0 (if in OpenSSL the default digest is applied)
        pbeGenerator.init(password, salt);
        ParametersWithIV parameters = (ParametersWithIV) pbeGenerator.generateDerivedParameters(256, 128);// keySize, ivSize in bits
        System.out.println(parameters.getIV());


// Decrypt with AES-256
        try (FileOutputStream fos = new FileOutputStream(outputPath)) {

           // Encrypt chunkwise (for large data)
            PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
            cipher.init(false, parameters);
            try (FileInputStream fis = new FileInputStream(inputPath);
                 CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
                int bytesRead = -1;
                byte[] buffer = new byte[64 * 1024 * 1024];
                while ((bytesRead = fis.read(buffer)) != -1) {
                    cos.write(buffer, 0, bytesRead);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);

        }


    }
}

Error:

Exception in thread "main" java.lang.RuntimeException: org.bouncycastle.crypto.io.InvalidCipherTextIOException: Error finalising cipher data
    at decrypt.test5_encrypt.main(test5_encrypt.java:61)
Caused by: org.bouncycastle.crypto.io.InvalidCipherTextIOException: Error finalising cipher data
    at org.bouncycastle.crypto.io.CipherOutputStream.close(Unknown Source)
    at decrypt.test5_encrypt.main(test5_encrypt.java:59)
Caused by: org.bouncycastle.crypto.InvalidCipherTextException: pad block corrupted

Advertisement

Answer

When using a password, OpenSSL stores the ciphertext in a specific format, namely the ASCII encoding of Salted__, followed by the 8 bytes salt, then the actual ciphertext.

During decryption, the salt must not be randomly generated (as it is done in the posted code), otherwise the wrong key and IV will be derived. Instead, the salt must be determined from the metadata of the ciphertext. Also the use of the stream classes must be fixed:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.generators.OpenSSLPBEParametersGenerator;
import org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.ParametersWithIV; 

...

String inputPath = "...";   // path to enc file
String outputPath = "...";  // path to dec file
String passwordStr = "...";

// Decrypt with AES-256, CBC using streams
try (FileInputStream fis = new FileInputStream(inputPath)){

    // Determine salt from OpenSSL format
    fis.readNBytes(8); // Skip prefix Salted__
    byte[] salt = fis.readNBytes(8); // Read salt
    
    // Derive 32 bytes key (AES_256) and 16 bytes IV via EVP_BytesToKey()
    byte[] password = passwordStr.getBytes(StandardCharsets.UTF_8);
    OpenSSLPBEParametersGenerator pbeGenerator = new OpenSSLPBEParametersGenerator(new MD5Digest()); // SHA256 as of v1.1.0 (if in OpenSSL the default digest is applied)
    pbeGenerator.init(password, salt);
    ParametersWithIV parameters = (ParametersWithIV) pbeGenerator.generateDerivedParameters(256, 128); // keySize, ivSize in bits
    
    // Decrypt chunkwise (for large data)
    PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
    cipher.init(false, parameters);
    try (CipherInputStream cis = new CipherInputStream(fis, cipher);
         FileOutputStream fos = new FileOutputStream(outputPath)) {
        int bytesRead = -1;
        byte[] buffer = new byte[64 * 1024 * 1024]; // chunksize, e.g. 64 MiB
        while ((bytesRead = cis.read(buffer)) != -1) {
            fos.write(buffer, 0, bytesRead);
        }               
    }
}

This is functionally equivalent to the OpenSSL statement:

openssl enc -d -aes256 -k <passpharse> -in <enc file> -out <dec file>

Note that OpenSSL applied MD5 as digest by default in earlier versions and SHA256 as of v.1.1.0. Code and OpenSSL statement must use the same digest for compatibility.
In the code the digest is explicitly specified, in the OpenSSL statement it can be explicitly set via the -md option so that matching is possible on both sides.
Keep in mind that EVP_BytesToKey(), which is used by default by OpenSSL for key derivation, is deemed insecure nowadays.


Addition regarding Java 8: For Java 8, e.g. the following implementation can be applied for the determination of the salt:

int i = 0;
byte[] firstBlock = new byte[16]; 
while (i < firstBlock.length) {
    i += fis.read(firstBlock, i, firstBlock.length - i);
}
byte[] salt = Arrays.copyOfRange(firstBlock, 8, 16);

The loop is necessary because read(byte[],int,int), unlike readNBytes(int), does not guarantee that the buffer is completely filled (considering here the non-EOF and non-error case).

If you omit the loop (which means using the equivalent read(byte[])), the code will still run for those JVMs which also fill the buffer completely. Since this applies to the most common JVMs for small buffer sizes the code will mostly work, see the comment by dave_thompson_085. However, this is not guaranteed for any JVM and is therefore less robust (though probably not by much).

User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement