0

Im trying to write a program to encrypt any type of file. I had my encryption classes already done, when I noticed (at first it worked) that I am getting an AEADBadTagException whenever I try to decrypt any of my files.

Here is my encryption/decryption class:

class Encryptor {

    private static final String algorithm = "AES/GCM/NoPadding";

    private final int tagLengthBit = 128; // must be one of {128, 120, 112, 104, 96}
    private final int ivLengthByte = 12;
    private final int saltLengthByte = 64;
    protected final Charset UTF_8 = StandardCharsets.UTF_8;
    private CryptoUtils crypto = new CryptoUtils();

    // return a base64 encoded AES encrypted text
    /**
     * 
     * @param pText    to encrypt
     * @param password password for encryption
     * @return encoded pText
     * @throws Exception
     */
    protected byte[] encrypt(byte[] pText, char[] password) throws Exception {

        // 64 bytes salt
        byte[] salt = crypto.getRandomNonce(saltLengthByte);

        // GCM recommended 12 bytes iv?
        byte[] iv = crypto.getRandomNonce(ivLengthByte);

        // secret key from password
        SecretKey aesKeyFromPassword = crypto.getAESKeyFromPassword(password, salt);

        Cipher cipher = Cipher.getInstance(algorithm);

        // ASE-GCM needs GCMParameterSpec
        cipher.init(Cipher.ENCRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(tagLengthBit, iv));

        byte[] cipherText = cipher.doFinal(pText);

        // prefix IV and Salt to cipher text
        byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length).put(iv).put(salt)
                .put(cipherText).array();
        Main.clearArray(password, null);
        Main.clearArray(null, salt);
        Main.clearArray(null, iv);
        Main.clearArray(null, cipherText);
        aesKeyFromPassword = null;
        cipher = null;
        try {
            return cipherTextWithIvSalt;

        } finally {
            Main.clearArray(null, cipherTextWithIvSalt);
        }
    }



// für Files
    protected byte[] decrypt(byte[] encryptedText, char[] password)
            throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            NoSuchPaddingException, InvalidKeySpecException, IllegalBlockSizeException, BadPaddingException {

        // get back the iv and salt from the cipher text
        ByteBuffer bb = ByteBuffer.wrap(encryptedText);

        byte[] iv = new byte[ivLengthByte];
        bb.get(iv);

        byte[] salt = new byte[saltLengthByte];
        bb.get(salt);

        byte[] cipherText = new byte[bb.remaining()];
        bb.get(cipherText);

        // get back the aes key from the same password and salt
        SecretKey aesKeyFromPassword;
        aesKeyFromPassword = crypto.getAESKeyFromPassword(password, salt);

        Cipher cipher;
        cipher = Cipher.getInstance(algorithm);

        cipher.init(Cipher.DECRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(tagLengthBit, iv));

        byte[] plainText = cipher.doFinal(cipherText);
        
        Main.clearArray(password, null);
        Main.clearArray(null, iv);
        Main.clearArray(null, salt);
        Main.clearArray(null, cipherText);
        aesKeyFromPassword = null;
        cipher = null;
        bb = null;
        try {
            return plainText;
        } finally {
            Main.clearArray(null, plainText);
        }

    }

    protected void encryptFile(String file, char[] pw) throws Exception {
        Path pathToFile = Paths.get(file);

        byte[] fileCont = Files.readAllBytes(pathToFile);

        byte[] encrypted = encrypt(fileCont, pw);

        Files.write(pathToFile, encrypted);

        Main.clearArray(pw, null);
        Main.clearArray(null, fileCont);
        Main.clearArray(null, encrypted);
    }

    protected void decryptFile(String file, char[] pw)
            throws IOException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            NoSuchPaddingException, InvalidKeySpecException, IllegalBlockSizeException, BadPaddingException {
        Path pathToFile = Paths.get(file);
        
        byte[] fileCont = Files.readAllBytes(pathToFile);
        
        byte[] decrypted = decrypt(fileCont, pw);

        Files.write(pathToFile, decrypted);

        Main.clearArray(pw, null);
        Main.clearArray(null, fileCont);
        Main.clearArray(null, decrypted);

    }

}

The corresponding CryptoUtils class:

class CryptoUtils {

    protected byte[] getRandomNonce(int numBytes) {
        byte[] nonce = new byte[numBytes];
        new SecureRandom().nextBytes(nonce);
        try {
            return nonce;

        } finally {
            Main.clearArray(null, nonce);
        }
    }


    // Password derived AES 256 bits secret key
    protected SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
            throws NoSuchAlgorithmException, InvalidKeySpecException {

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
        // iterationCount = 65536
        // keyLength = 256
        KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
        try {
            return secret;

        } finally {
            secret = null;
        }
    }

    // hex representation
    protected String hex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }

        try {
            return result.toString();

        } finally {
            result.delete(0, result.length() - 1);
        }
    }

    // print hex with block size split
    protected String hexWithBlockSize(byte[] bytes, int blockSize) {

        String hex = hex(bytes);

        // one hex = 2 chars
        blockSize = blockSize * 2;

        // better idea how to print this?
        List<String> result = new ArrayList<>();
        int index = 0;
        while (index < hex.length()) {
            result.add(hex.substring(index, Math.min(index + blockSize, hex.length())));
            index += blockSize;
        }

        try {
            return result.toString();

        } finally {
            result.clear();
        }
    }

}

The Exception occurs at byte[] plainText = cipher.doFinal(cipherText); in the decrypt method.

Im unsure if the tagLenthBit must be the ivLengthByte * 8, I did try it though and it didnt make any difference.

Dahlin
  • 157
  • 1
  • 11
  • I can't reproduce this. Are you possibly mixing up files, since you save everything in the same file? – Topaco Feb 08 '21 at 12:34
  • @Topaco that cant be, since for every file the content is read, and then written right back. And this also happens for only choosing one file – Dahlin Feb 08 '21 at 12:36
  • As I said, on my machine the code works (using a 14 MB pdf test file). I would try to isolate the issue, e.g. you could first test if the problem also occurs for `encrypt()` and `decrypt()` _without_ files. – Topaco Feb 08 '21 at 12:43
  • You seem to clear the results in your `finally` statements (`finally` is executed before `return`), s. `encrypt()`, `decrypt()` and `getRandomNonce()`. I had commented out these lines (since you didn't post `Main.clearArray()`), so it worked on my machine from the beginning. – Topaco Feb 08 '21 at 13:09
  • @Topaco that explains a lot, I have to remove that then. but how would I clear an array that is returned first? – Dahlin Feb 08 '21 at 13:17
  • In Java, only references to objects are passed between functions ([call-by-value](https://www.tutorialspoint.com/Call-by-value-and-Call-by-reference-in-Java) where the value is a reference), so the objects are the same. In the case of the ciphertext this means: `cipherTextWithIvSalt` in `encrypt()` and `encrypted` in `encyrptFile()` reference the same object. If you want to delete it explicitly e.g. for security reasons, this must happen after it has been used, i.e. in `encyrptFile()` after the ciphertext has been saved. – Topaco Feb 08 '21 at 14:20
  • okay, but how would I do that, since the value is returned and saved into another array. would clearing the array result in clearing the returned value? – Dahlin Feb 08 '21 at 17:03
  • As already said: `encrypted` in `encyrptFile()` and `cipherTextWithIvSalt` in `encrypt()` reference _one and the same_ object, i.e. if you fill the `byte[]` referenced with `encrypted` with 0-values, then this also applies to the (identical) object referenced with `cipherTextWithIvSalt`. – Topaco Feb 08 '21 at 17:15
  • oh yes sure, thats what I already have anyway, thanks! And removing the try finally block fixed the actual problem. – Dahlin Feb 08 '21 at 17:29
  • 1
    For completeness, please note that even if you set all references of an object to null, so that it is cleared by the GC _on the next run_, you have no control over when this happens (and whether the data is completely cleared from physical memory, which is usually not the case for performance reasons). Removing critical data from memory is generally not a trivial issue, s. e.g. [here](https://crypto.stackexchange.com/q/9998), [here](https://security.stackexchange.com/q/6753) and [here](https://stackoverflow.com/questions/28907297/how-to-zero-out-from-memory-an-aes-secretkeyspec-key-in-java). – Topaco Feb 08 '21 at 18:04

1 Answers1

0

I'm providing my own example code for AES 256 GCM file encryption with PBKDF2 key derivation because I'm too lazy to check all parts of your code :-)

The encryption is done with CipherInput-/Outputstreams because that avoids "out of memory errors" when encrypting larger files (your code is reading the complete plaintext / ciphertext in a byte array).

Please note that the code has no exception handling, no clearing of sensitive data/variables and the encryption/decryption result is a simple "file exist" routine but I'm sure you can use it as a good basis for your program.

That's a sample output:

AES 256 GCM-mode PBKDF2 with SHA512 key derivation file encryption
result encryption: true
result decryption: true

code:

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

public class AesGcmEncryptionInlineIvPbkdf2BufferedCipherInputStreamSoExample {
    public static void main(String[] args) throws NoSuchPaddingException, NoSuchAlgorithmException, IOException,
            InvalidKeyException, InvalidKeySpecException, InvalidAlgorithmParameterException {
        System.out.println("AES 256 GCM-mode PBKDF2 with SHA512 key derivation file encryption");

        char[] password = "123456".toCharArray();
        int iterations = 65536;
        String uncryptedFilename = "uncrypted.txt";
        String encryptedFilename = "encrypted.enc";
        String decryptedFilename = "decrypted.txt";
        boolean result;
        result = encryptGcmFileBufferedCipherOutputStream(uncryptedFilename, encryptedFilename, password, iterations);
        System.out.println("result encryption: " + result);
        result = decryptGcmFileBufferedCipherInputStream(encryptedFilename, decryptedFilename, password, iterations);
        System.out.println("result decryption: " + result);
    }

    public static boolean encryptGcmFileBufferedCipherOutputStream(String inputFilename, String outputFilename, char[] password, int iterations) throws
            IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, InvalidAlgorithmParameterException {
        SecureRandom secureRandom = new SecureRandom();
        byte[] salt = new byte[32];
        secureRandom.nextBytes(salt);
        byte[] nonce = new byte[12];
        secureRandom.nextBytes(nonce);
        Cipher cipher = Cipher.getInstance("AES/GCM/NOPadding");
        try (FileInputStream in = new FileInputStream(inputFilename);
             FileOutputStream out = new FileOutputStream(outputFilename);
             CipherOutputStream encryptedOutputStream = new CipherOutputStream(out, cipher);) {
            out.write(nonce);
            out.write(salt);
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
            KeySpec keySpec = new PBEKeySpec(password, salt, iterations, 32 * 8); // 128 - 192 - 256
            byte[] key = secretKeyFactory.generateSecret(keySpec).getEncoded();
            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
            GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8, nonce);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
            byte[] buffer = new byte[8096];
            int nread;
            while ((nread = in.read(buffer)) > 0) {
                encryptedOutputStream.write(buffer, 0, nread);
            }
            encryptedOutputStream.flush();
        }
        if (new File(outputFilename).exists()) {
            return true;
        } else {
            return false;
        }
    }

    public static boolean decryptGcmFileBufferedCipherInputStream(String inputFilename, String outputFilename, char[] password, int iterations) throws
            IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, InvalidAlgorithmParameterException {
        byte[] salt = new byte[32];
        byte[] nonce = new byte[12];
        Cipher cipher = Cipher.getInstance("AES/GCM/NOPadding");
        try (FileInputStream in = new FileInputStream(inputFilename); // i don't care about the path as all is lokal
             CipherInputStream cipherInputStream = new CipherInputStream(in, cipher);
             FileOutputStream out = new FileOutputStream(outputFilename)) // i don't care about the path as all is lokal
        {
            byte[] buffer = new byte[8192];
            in.read(nonce);
            in.read(salt);
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
            KeySpec keySpec = new PBEKeySpec(password, salt, iterations, 32 * 8); // 128 - 192 - 256
            byte[] key = secretKeyFactory.generateSecret(keySpec).getEncoded();
            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
            GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8, nonce);
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
            int nread;
            while ((nread = cipherInputStream.read(buffer)) > 0) {
                out.write(buffer, 0, nread);
            }
            out.flush();
        }
        if (new File(outputFilename).exists()) {
            return true;
        } else {
            return false;
        }
    }
}
Michael Fehr
  • 5,827
  • 2
  • 19
  • 40
  • thanks! the only problem with this cipher output stream is that it want a cipher, however I got a method that already returns the result of the encryption via cipher :/ – Dahlin Feb 08 '21 at 17:30