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).