javaencryptionaesaes-gcm

Getting ShortBufferException when using CipherOutputStream with AES GCM mode


I'm trying to write an encrypter/decrypter class that encrypts and decrypts files. My server receives the file as an InputStream, and I want to store it at a particular path, so those are the inputs to my encrypt() method. For testing purposes, encrypt() generates a new key for each file and stores it in a map with the file's path. My decrypt() method just takes the file name and attempts to print the file contents as a string. encrypt() seems to work fine. The problem I'm running into is that when I call decrypt() with the file name, I get a ShortBufferException. I've tried changing the buffer size in my BufferedReader, but to no avail. I think I'm missing something fundamental about how CipherInputStream or InputStreams in general work. Any thoughts would be much appreciated! Here's my FileEncrypterDecrypter class:

public class FileEncrypterDecrypter {
    private static final int ALGORITHM_NONCE_SIZE = 12;
    private static final String ALGORITHM_NAME = "AES/GCM/NoPadding";
    private final static int ALGORITHM_TAG_SIZE = 128;
    private final static HashMap<String, SecretKey> fileToKey = new HashMap<>();

    FileEncrypterDecrypter() {}

    public static void encrypt(InputStream plaintext, String filePath) throws IOException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
        // Generate a new SecretKey for this file
        SecretKey secretKey = KeyGenerator.getInstance("AES").generateKey();
        fileToKey.put(filePath, secretKey);

        // Generate a 96-bit nonce using a CSPRNG.
        SecureRandom rand = new SecureRandom();
        byte[] nonce = new byte[ALGORITHM_NONCE_SIZE];
        rand.nextBytes(nonce);
        System.out.println(Arrays.toString(nonce));

        // Create the cipher instance and initialize.
        Cipher cipher = Cipher.getInstance(ALGORITHM_NAME);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(ALGORITHM_TAG_SIZE, nonce));

        // Write to the file using a CipherOutputStream
        FileOutputStream fileOut = new FileOutputStream(filePath);
        CipherOutputStream cipherOut = new CipherOutputStream(fileOut, cipher);
        fileOut.write(nonce);
        plaintext.transferTo(cipherOut);
    }

    public static String decrypt(String filePath) throws InvalidKeyException, IOException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
        // Get the SecretKey for this file
        SecretKey secretKey = fileToKey.get(filePath);

        // Read nonce from the file
        FileInputStream fileIn = new FileInputStream(filePath);
        byte[] nonce = new byte[ALGORITHM_NONCE_SIZE];
        fileIn.read(nonce);
        System.out.println(Arrays.toString(nonce));
        fileIn = new FileInputStream(filePath);
        // Create the cipher instance and initialize
        Cipher cipher = Cipher.getInstance(ALGORITHM_NAME);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, nonce));

        CipherInputStream cipherIn = new CipherInputStream(fileIn, cipher);
        InputStreamReader inputReader = new InputStreamReader(cipherIn);
        BufferedReader reader = new BufferedReader(inputReader);

        // Decrypt and return result.
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        return sb.toString();
    }
}

And my error output:

Exception in thread "main" java.io.IOException: javax.crypto.ShortBufferException: Output buffer invalid
    at java.base/javax.crypto.CipherInputStream.getMoreData(CipherInputStream.java:148)
    at java.base/javax.crypto.CipherInputStream.read(CipherInputStream.java:261)
    at java.base/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:270)
    at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:313)
    at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:188)
    at java.base/java.io.InputStreamReader.read(InputStreamReader.java:177)
    at java.base/java.io.BufferedReader.fill(BufferedReader.java:162)
    at java.base/java.io.BufferedReader.readLine(BufferedReader.java:329)
    at java.base/java.io.BufferedReader.readLine(BufferedReader.java:396)
    at FileEncrypterDecrypter.decrypt(FileEncrypterDecrypter.java:64)
    at Main.main(Main.java:22)
Caused by: javax.crypto.ShortBufferException: Output buffer invalid
    at java.base/com.sun.crypto.provider.GaloisCounterMode$GCMDecrypt.doFinal(GaloisCounterMode.java:1366)
    at java.base/com.sun.crypto.provider.GaloisCounterMode.engineDoFinal(GaloisCounterMode.java:432)
    at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2152)
    at java.base/javax.crypto.CipherInputStream.getMoreData(CipherInputStream.java:145)
    ... 10 more

Here's my main method:

public static void main (String[] args) throws BadPaddingException, IllegalBlockSizeException, IOException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
        InputStream is = new ByteArrayInputStream(Charset.forName("UTF-8").encode("HELLO WORLD").array());
        FileEncrypterDecrypter encrypterDecrypter = new FileEncrypterDecrypter();
        encrypterDecrypter.encrypt(is, "./message");
        encrypterDecrypter.decrypt("./message");
    }

I've found similar problems on Stack Overflow, but none which applies exactly to my situation.


Solution

  • You are not closing the OutputStream in your encryption code, which prevents the encryption finalization code from running. Your encrypt function should end with

    plaintext.close();
    cipherOut.close();
    

    Java's GCM implementation won't return any cipher until the underlying Cipher object's doFinal method is called, at which point it returns all the cipher and the tag. That only happens when the CipherOutputStream.close() method is called.

    Your decryption code is strange in that it opens the cipher twice, without closing it. It correctly reads the nonce in, though even that is not done reliably, and then it reopens the file a second time and treats the nonce as if it is ciphertext. This will cause decryption to fail when the tag verification is attempted. The GCM tag comes to the rescue again!

    The decryption code runs successfully for me if I simply remove the second fileIn = new FileInputStream(filePath);.

    Here some other improvements that I'll suggest:

    1. Replace
    byte[] nonce = new byte[ALGORITHM_NONCE_SIZE];
    fileIn.read(nonce);
    

    with

    byte [] nonce = fileIn.readNBytes(ALGORITHM_NONCE_SIZE);
    
    1. For clarity, the encrypt and decrypt methods should exhibit symmetry. In this case, that is challenging since encrypt takes an already opened InputStream as a parameter. An InputStream is a source of uninterpreted bytes, not a source of characters. Thus, maintaining symmetry as best one can, the result from decrypt should be the same bytes that were read in during encrypt rather than a String.

    Something like the following:

    public static byte[] decrypt2(String filePath) throws Exception {
        SecretKey secretKey = fileToKey.get(filePath);
    
        // Read nonce from the file
        FileInputStream fileIn = new FileInputStream(filePath);
        byte[] nonce = fileIn.readNBytes(ALGORITHM_NONCE_SIZE);
        System.out.println(Arrays.toString(nonce));
        // Create the cipher instance and initialize
        Cipher cipher = Cipher.getInstance(ALGORITHM_NAME);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, nonce));
    
        CipherInputStream cipherIn = new CipherInputStream(fileIn, cipher);
        return cipherIn.readAllBytes();
    }
    

    Conversions of bytes to strings can take place outside of the decrypt function.