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 InputStream
s 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.
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:
byte[] nonce = new byte[ALGORITHM_NONCE_SIZE];
fileIn.read(nonce);
with
byte [] nonce = fileIn.readNBytes(ALGORITHM_NONCE_SIZE);
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.