1

I referred this and trying to do file decryption with base64 decoding

My requirement is to encode data with base64 during encryption and decode data with base64 during decryption.

But im facing below error:

javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
    at java.base/com.sun.crypto.provider.CipherCore.doFinal(Unknown Source)
    at java.base/com.sun.crypto.provider.CipherCore.doFinal(Unknown Source)
    at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(Unknown Source)
    at java.base/javax.crypto.Cipher.doFinal(Unknown Source)
    at aes.DecryptNew.decryptNew(DecryptNew.java:124)
    at aes.DecryptNew.main(DecryptNew.java:32)

Also im confused on how to perform decryption in chunks. Please suggest me issues in this code.

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.Arrays;
import java.util.Base64;

public class DecryptNew {
    public static void main(String[] args) {
        String plaintextFilename = "D:\\\\plaintext.txt";
        String ciphertextFilename = "D:\\\\plaintext.txt.crypt";
        String decryptedtextFilename = "D:\\\\plaintextDecrypted.txt";
        String password = "testpass";

        writeToFile();
        String ciphertext = encryptfile(plaintextFilename, password);
        System.out.println("ciphertext: " + ciphertext);
        decryptNew(ciphertextFilename, password, decryptedtextFilename);
    }

    static void writeToFile() {
        BufferedWriter writer = null;
        try
        {
            writer = new BufferedWriter( new FileWriter("D:\\\\plaintext.txt"));
            byte[] data = Base64.getEncoder().encode("hello\r\nhello".getBytes(StandardCharsets.UTF_8));
            writer.write(new String(data)); 
        }
        catch ( IOException e)
        {
        }
        finally
        {
            try
            {
                if ( writer != null)
                writer.close( );
            }
            catch ( IOException e)
            {
            }
        }
    }
    
    public static String encryptfile(String path, String password) {
        try {
            FileInputStream fis = new FileInputStream(path);
            FileOutputStream fos = new FileOutputStream(path.concat(".crypt"));
            final byte[] pass = Base64.getEncoder().encode(password.getBytes());
            final byte[] salt = (new SecureRandom()).generateSeed(8);
            fos.write(Base64.getEncoder().encode("Salted__".getBytes()));
            fos.write(salt);
            final byte[] passAndSalt = concatenateByteArrays(pass, salt);
            byte[] hash = new byte[0];
            byte[] keyAndIv = new byte[0];
            for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
                final byte[] hashData = concatenateByteArrays(hash, passAndSalt);
                final MessageDigest md = MessageDigest.getInstance("SHA-1");
                hash = md.digest(hashData);
                keyAndIv = concatenateByteArrays(keyAndIv, hash);
            }
            final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
            final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);
            final SecretKeySpec key = new SecretKeySpec(keyValue, "AES");
            final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
            CipherOutputStream cos = new CipherOutputStream(fos, cipher);
            int b;
            byte[] d = new byte[8];
            while ((b = fis.read(d)) != -1) {
                cos.write(d, 0, b);
            }
            cos.flush();
            cos.close();
            fis.close();
            System.out.println("encrypt done " + path);
        } catch (IOException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        }
        return path;
    }

    static void decryptNew(String path,String password, String outPath) {
        byte[] SALTED_MAGIC = Base64.getEncoder().encode("Salted__".getBytes());
        try{
            FileInputStream fis = new FileInputStream(path);
            FileOutputStream fos = new FileOutputStream(outPath);
            final byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
            final byte[] inBytes = Files.readAllBytes(Paths.get(path));
            final byte[] shouldBeMagic = Arrays.copyOfRange(inBytes, 0, SALTED_MAGIC.length);
            if (!Arrays.equals(shouldBeMagic, SALTED_MAGIC)) {
                throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.");
            }
            final byte[] salt = Arrays.copyOfRange(inBytes, SALTED_MAGIC.length, SALTED_MAGIC.length + 8);
            final byte[] passAndSalt = concatenateByteArrays(pass, salt);
            byte[] hash = new byte[0];
            byte[] keyAndIv = new byte[0];
            for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
                final byte[] hashData = concatenateByteArrays(hash, passAndSalt);
                MessageDigest md = null;
                md = MessageDigest.getInstance("SHA-1");
                hash = md.digest(hashData);
                keyAndIv = concatenateByteArrays(keyAndIv, hash);
            }
            final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
            final SecretKeySpec key = new SecretKeySpec(keyValue, "AES");
            final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);
            final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
            final byte[] clear = cipher.doFinal(inBytes, 16, inBytes.length - 16);
            String contentDecoded = new String(Base64.getDecoder().decode(clear));
            fos.write(contentDecoded.getBytes());
            fos.close();
            System.out.println("Decrypt is completed");
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    

    public static byte[] concatenateByteArrays(byte[] a, byte[] b) {
        return ByteBuffer
                .allocate(a.length + b.length)
                .put(a).put(b)
                .array();
    }
}
Anees U
  • 1,077
  • 1
  • 12
  • 20
  • 2
    For the password different encodings are used for encryption and decryption (Base64 vs ASCII). Both must be identical. In both encryption and decryption a Base64 encoded prefix `Salted__` is applied, so prefix plus salt are larger than one block (16 bytes), which is inconsistent with the length determination of the ciphertext during decryption. A solution would be e.g. an ASCII encoded prefix. – Topaco Oct 21 '20 at 09:03
  • It' s not clear to me what you actually want to achieve. The encryption only performs a Base64 encoding of the prefix. A Base64 encoded plaintext does not mean that the encryption itself performs a Base64 encoding, instead only a Base64 encoded plaintext is encrypted. Shouldn't everything be Base64 encoded (i.e. prefix, salt and ciphertext together) as OpenSSL does with the -base64 option? – Topaco Oct 21 '20 at 11:40
  • Two things from my side - encrypting with chunks is best done with CipherInput-/Outputstream. This is necessary only if you are working with large file (more than some Megabyte) to avoid to run into an "Out of memory error". Second you should consider to use Base64Input-/Outputstream from Apache commons codec library (put it between FileInput/FileOutputStream). – Michael Fehr Oct 21 '20 at 12:54

1 Answers1

1

As mentioned in my first comment: Encryption and decryption use different encodings for the password (Base64 in encryption, ASCII in decryption).
In addition the prefix is Base64 encoded in both encryption and decryption, so prefix plus salt are larger than one block (16 bytes), and therefore the subsequent length determination of the ciphertext during decryption fails, as it is assumed that the ciphertext starts with the 2nd block.
Both issues can be solved if the same encoding is used for the password in encryption and decryption (e.g. ASCII) and the prefix is ASCII encoded, e.g. for encryption:

...
byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
byte[] SALTED_MAGIC = "Salted__".getBytes(StandardCharsets.US_ASCII);
byte[] salt = (new SecureRandom()).generateSeed(8);
fos.write(SALTED_MAGIC);
fos.write(salt);
...

and for decryption:

...
byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
byte[] SALTED_MAGIC = "Salted__".getBytes(StandardCharsets.US_ASCII);
byte[] prefix = fis.readNBytes(8);
byte[] salt = fis.readNBytes(8);
...

However the current encryption method does not Base64 encode (only the prefix is Base64 encoded which is rather counterproductive, s. above).
Even a Base64 encoded plaintext does not change this, as the ciphertext itself is not Base64 encoded.
Considering your statement My requirement is to encode data with base64 during encryption and decode data with base64 during decryption and the OpenSSL format used, I assume that you want to decrypt a ciphertext that has been encrypted with the -base64 option, analogous to OpenSSL, i.e. with

openssl enc -aes-256-cbc -base64 -pass pass:testpass -p -in sample.txt -out sample.crypt

Here, prefix, salt and ciphertext are concatenated on byte level and the result is then Base64 encoded. To achieve this, your implementation posted in the question could be changed as follows:

static void decrypt(String path, String password, String outPath) {

    try (FileInputStream fis = new FileInputStream(path);
         Base64InputStream bis = new Base64InputStream(fis, false, 64, "\r\n".getBytes(StandardCharsets.US_ASCII))) { // from Apache Commons Codec
        
        // Read prefix and salt
        byte[] SALTED_MAGIC = "Salted__".getBytes(StandardCharsets.US_ASCII);
        byte[] prefix = bis.readNBytes(8);
        if (!Arrays.equals(prefix, SALTED_MAGIC)) {
            throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.");
        }
        byte[] salt = bis.readNBytes(8);

        // Derive key and IV
        byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
        byte[] passAndSalt = concatenateByteArrays(pass, salt);
        byte[] hash = new byte[0];
        byte[] keyAndIv = new byte[0];
        for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
            byte[] hashData = concatenateByteArrays(hash, passAndSalt);
            MessageDigest md = null;
            md = MessageDigest.getInstance("SHA-1");   // Use digest from encryption
            hash = md.digest(hashData);
            keyAndIv = concatenateByteArrays(keyAndIv, hash);
        }
        byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
        SecretKeySpec key = new SecretKeySpec(keyValue, "AES");
        byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);

        // Decrypt
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
        try (CipherInputStream cis = new CipherInputStream(bis, cipher);
             FileOutputStream fos = new FileOutputStream(outPath)) {

            int length;
            byte[] buffer = new byte[1024];
            while ((length = cis.read(buffer)) != -1) {
                fos.write(buffer, 0, length);
            }
        }
        System.out.println("Decrypt is completed");
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}

As I already mentioned in the comments to the linked question, the processing in chunks can easily be implemented with the class CipherInputStream. The Base64 decoding can be achieved with the class Base64InputStream of the Apache Commons Codec, as also addressed by Michael Fehr. This is a convenient way to perform Base64 decoding and decryption together. If the data does not need to be Base64 decoded (e.g. if the -base64 option was not used during encryption) the Base64InputStream class can simply be omitted.

As previously stated in the comments, small files do not need to be processed in chunks. This is only necessary when the files become large relative to the memory. However, since your encryption method processes the data in chunks, it is consistent that the decryption does the same.

Note that the encryption posted in the question is not compatible with the result of the above OpenSSL statement, i.e. the encryption would have to be adapted if necessary (analogous to the decryption posted above).

Edit: The encryptfile() method posted in the question creates a file containing the Base64 encoded prefix, the raw salt and the raw ciphertext. For key derivation the Base64 encoded password is applied. The method is used to encrypt a Base64 encoded plaintext. The following method is the counterpart of encryptfile() and allows the decryption and Base64 decoding of the plaintext:

static void decryptfile(String path, String password, String outPath) {

    try (FileInputStream fis = new FileInputStream(path)) {

        // Read prefix and salt
        byte[] SALTED_MAGIC = Base64.getEncoder().encode("Salted__".getBytes());
        byte[] prefix = new byte[SALTED_MAGIC.length];
        fis.readNBytes(prefix, 0, prefix.length);
        if (!Arrays.equals(prefix, SALTED_MAGIC)) {
            throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.");
        }
        byte[] salt = new byte[8];
        fis.readNBytes(salt, 0, salt.length);

        // Derive key and IV
        final byte[] pass = Base64.getEncoder().encode(password.getBytes());
        byte[] passAndSalt = concatenateByteArrays(pass, salt);
        byte[] hash = new byte[0];
        byte[] keyAndIv = new byte[0];
        for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
            byte[] hashData = concatenateByteArrays(hash, passAndSalt);
            MessageDigest md = null;
            md = MessageDigest.getInstance("SHA-1");   // Use digest from encryption
            hash = md.digest(hashData);
            keyAndIv = concatenateByteArrays(keyAndIv, hash);
        }
        byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
        SecretKeySpec key = new SecretKeySpec(keyValue, "AES");
        byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);

        // Decrypt
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
        try (CipherInputStream cis = new CipherInputStream(fis, cipher);
             Base64InputStream bis = new Base64InputStream(cis, false, -1, null); // from Apache Commons Codec  
             FileOutputStream fos = new FileOutputStream(outPath)) {

            int length;
            byte[] buffer = new byte[1024];
            while ((length = bis.read(buffer)) != -1) {
                fos.write(buffer, 0, length);
            }
        }
        System.out.println("Decrypt is completed");
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}

The decryptfile()-method writes the Base64 decoded plaintext to the file in outPath. To get the Base64 encoded plaintext simply remove the Base64InputStream instance from the stream hierarchy.

As already explained in the comment, this method is incompatible to OpenSSL, i.e. it can't decrypt ciphertexts generated with the OpenSSL statement posted above (with or without -base64 option). To decrypt a ciphertext generated with OpenSSL with -base64 option use the decrypt() method.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Thanks for the answer! The byte[] salt = bis.readNBytes(8); takes 3 parameter. when i used apache codec1.15 jar. please suggest on how to use those 3 param method. – Anees U Oct 22 '20 at 13:10
  • @Anees U: The Apache codec's Base64InputStream is taking 1 or 2 or 4 parameter but never 3. Kindly see the documentation https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/binary/Base64InputStream.html. Giving 1 parameter as in Topaco's answer it is only the InputStream that it takes. – Michael Fehr Oct 22 '20 at 13:45
  • @Anees U: `ReadNBytes()` is inherited from `java.io.InputStream`, so the version of Java you are using determines which overloads are available. `ReadNBytes()` with one parameter exists since version 11, the one with three parameters since version 9. The latter can be used as follows (e.g. for the salt): `byte[] salt = new byte[8]; bis.readNBytes(salt, 0, salt.length);` And analogous for the prefix. – Topaco Oct 22 '20 at 13:51
  • @MichaelFehr, Thanks, If i use like this it goes to exception block: byte[] prefix = new byte[8]; bis.readNBytes(prefix, 0, prefix.length); if (!Arrays.equals(prefix, SALTED_MAGIC)) { throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value."); } – Anees U Oct 22 '20 at 14:02
  • @AneesU - How did you create the ciphertext you want to decrypt? – Topaco Oct 22 '20 at 14:11
  • writer = new BufferedWriter( new FileWriter("D:\\\\plaintext.txt")); byte[] data = Base64.getEncoder().encode("hello\r\nhello".getBytes(StandardCharsets.US_ASCII)); writer.write(new String(data)); } – Anees U Oct 22 '20 at 14:21
  • This is basically the logic of your `writeToFile()` method that creates a file with the Base64 encoded plaintext. My question is, how do you create from this the _ciphertext_ you are trying to decrypt? If you use `encryptfile()` the decryption will fail because the posted `decrypt()` method decrypts a ciphertext that was generated with the posted OpenSSL statement (and **not** with `encryptfile()`). – Topaco Oct 22 '20 at 14:39
  • So my actual requirement is encryptfile() api used at android side and decryption done by a java tool. For testing purpose i need to make same decryption with OpenSSL also. – Anees U Oct 22 '20 at 14:54
  • The results delivered by `encryptfile()` and OpenSSL (with -base64 option) are incompatible. Thus, you have to make changes to `encryptfile()` (so that the generated ciphertext can be decrypted with the posted `decrypt()` method) or waive compatibility with OpenSSL/base64. Principally it's easy to modify the posted `decrypt()` method to be the counterpart of the `encryptfile()` logic if that is really what you want. I have added such an implementation in the edit section of my answer. – Topaco Oct 22 '20 at 17:41