Search code examples
javaencryptionencryption-symmetric

ChaCha20-Poly1305 fails with ShortBufferException Output buffer too small


I'm working on a file encryption benchmark for large files and tested ChaCha20-Poly1305, but received an error on decryption part:

java.lang.RuntimeException: javax.crypto.ShortBufferException: Output buffer too small
    at java.base/com.sun.crypto.provider.ChaCha20Cipher.engineDoFinal(ChaCha20Cipher.java:703)
    at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2085)
    at ChaCha20.ChaCha20Poly1305Jre.main(ChaCha20Poly1305Jre.java:73)
Caused by: javax.crypto.ShortBufferException: Output buffer too small
    at java.base/com.sun.crypto.provider.ChaCha20Cipher$EngineAEADDec.doFinal(ChaCha20Cipher.java:1360)
    at java.base/com.sun.crypto.provider.ChaCha20Cipher.engineDoFinal(ChaCha20Cipher.java:701)

This is not an error in my program but in OpenJava 11 I'm using and should get fixed (known since 2019, see https://bugs.openjdk.java.net/browse/JDK-8224997). Even with the newest "Early adopter"-version (OpenJDK11U-jdk_x64_windows_11.0.7_9_ea) the error still occurs. This test is run with java version: 11.0.6+8-b520.43.

My question is: is there any other way to perform file encryption with ChaCha20-Poly1305 with native JCE for large files ?

I do not want to use BouncyCastle (as I'm using BC allready for the counterpart benchmark) or reading the plainfile completly into memory (in my source the testfile is only 1.024 bytes large but the benchmark will test up to 1 GB files). As well I do not want to use ChaCha20 as it does not provide any authentication. You can find the sources for ChaCha20Poly1305Jce.java, ChaCha20Poly1305JceNoStream.java and ChaCha20Jce.java in my Github-Repo https://github.com/java-crypto/Stackoverflow/tree/master/ChaCha20Poly1305.

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.*;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;

public class ChaCha20Poly1305Jce {
    public static void main(String[] args) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
        System.out.println("File En-/Decryption with ChaCha20-Poly1305 JCE");
        System.out.println("see: https://stackoverflow.com/questions/61520639/chacha20-poly1305-fails-with-shortbufferexception-output-buffer-too-small");
        System.out.println("\njava version: " + Runtime.version());
        String filenamePlain = "test1024.txt";
        String filenameEnc = "test1024enc.txt";
        String filenameDec = "test1024dec.txt";
        Files.deleteIfExists(new File(filenamePlain).toPath());
        generateRandomFile(filenamePlain, 1024);
        // setup chacha20-poly1305-cipher
        SecureRandom sr = SecureRandom.getInstanceStrong();
        byte[] key = new byte[32]; // 32 for 256 bit key or 16 for 128 bit
        byte[] nonce = new byte[12]; // nonce = 96 bit
        sr.nextBytes(key);
        sr.nextBytes(nonce);
        // Get Cipher Instance
        Cipher cipherE = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding");
        // Create parameterSpec
        AlgorithmParameterSpec algorithmParameterSpec = new IvParameterSpec(nonce);
        // Create SecretKeySpec
        SecretKeySpec keySpec = new SecretKeySpec(key, "ChaCha20");
        System.out.println("keySpec: " + keySpec.getAlgorithm() + " " + keySpec.getFormat());
        System.out.println("cipher algorithm: " + cipherE.getAlgorithm());
        // initialize the cipher for encryption
        cipherE.init(Cipher.ENCRYPT_MODE, keySpec, algorithmParameterSpec);
        // encryption
        System.out.println("start encryption");
        byte inE[] = new byte[8192];
        byte outE[] = new byte[8192];
        try (InputStream is = new FileInputStream(new File(filenamePlain));
             OutputStream os = new FileOutputStream(new File(filenameEnc))) {
            int len = 0;
            while (-1 != (len = is.read(inE))) {
                cipherE.update(inE, 0, len, outE, 0);
                os.write(outE, 0, len);
            }
            byte[] outEf = cipherE.doFinal();
            os.write(outEf, 0, outEf.length);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // decryption
        System.out.println("start decryption");
        Cipher cipherD = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding");
        // initialize the cipher for decryption
        cipherD.init(Cipher.DECRYPT_MODE, keySpec, algorithmParameterSpec);
        byte inD[] = new byte[8192];
        byte outD[] = new byte[8192];
        try (InputStream is = new FileInputStream(new File(filenameEnc));
             OutputStream os = new FileOutputStream(new File(filenameDec))) {
            int len = 0;
            while (-1 != (len = is.read(inD))) {
                cipherD.update(inD, 0, len, outD, 0);
                os.write(outD, 0, len);
            }
            byte[] outDf = cipherD.doFinal();
            os.write(outDf, 0, outDf.length);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // file compare
        System.out.println("compare plain <-> dec: " + Arrays.equals(sha256(filenamePlain), sha256(filenameDec)));
    }

    public static void generateRandomFile(String filename, int size) throws IOException, NoSuchAlgorithmException {
        SecureRandom sr = SecureRandom.getInstanceStrong();
        byte[] data = new byte[size];
        sr.nextBytes(data);
        Files.write(Paths.get(filename), data, StandardOpenOption.CREATE);
    }

    public static byte[] sha256(String filenameString) throws IOException, NoSuchAlgorithmException {
        byte[] buffer = new byte[8192];
        int count;
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filenameString));
        while ((count = bis.read(buffer)) > 0) {
            md.update(buffer, 0, count);
        }
        bis.close();
        return md.digest();
    }
}

Solution

  • Using the Early Access version of OpenJDK 11.0.8 (get it here: https://adoptopenjdk.net/upstream.html?variant=openjdk11&ga=ea) I was able to run the (edited) testprogram (now I'm using CipherInput/OutputStreams).

    File En-/Decryption with ChaCha20-Poly1305 JCE
    see: https://stackoverflow.com/questions/61520639/chacha20-poly1305-fails-with-shortbufferexception-output-buffer-too-small
    
    java version: 11.0.8-ea+8
    start encryption
    keySpec: ChaCha20 RAW
    cipher algorithm: ChaCha20-Poly1305/None/NoPadding
    start decryption
    compare plain <-> dec: true
    

    Edit: I missed the final version (GA) for one hour but it's working as well:

    java version: 11.0.8+10
    start encryption
    keySpec: ChaCha20 RAW
    cipher algorithm: ChaCha20-Poly1305/None/NoPadding
    start decryption
    compare plain <-> dec: true
    

    new code:

    import javax.crypto.Cipher;
    import javax.crypto.CipherInputStream;
    import javax.crypto.CipherOutputStream;
    import javax.crypto.NoSuchPaddingException;
    import javax.crypto.spec.IvParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    import java.io.*;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.nio.file.StandardOpenOption;
    import java.security.*;
    import java.util.Arrays;
    
    public class ChaCha20Poly1305JceCis {
        public static void main(String[] args) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
            System.out.println("File En-/Decryption with ChaCha20-Poly1305 JCE");
            System.out.println("see: https://stackoverflow.com/questions/61520639/chacha20-poly1305-fails-with-shortbufferexception-output-buffer-too-small");
            System.out.println("\njava version: " + Runtime.version());
            String filenamePlain = "test1024.txt";
            String filenameEnc = "test1024enc.txt";
            String filenameDec = "test1024dec.txt";
            Files.deleteIfExists(new File(filenamePlain).toPath());
            generateRandomFile(filenamePlain, 1024);
            // setup chacha20-poly1305-cipher
            SecureRandom sr = SecureRandom.getInstanceStrong();
            byte[] key = new byte[32]; // 32 for 256 bit key or 16 for 128 bit
            byte[] nonce = new byte[12]; // nonce = 96 bit
            sr.nextBytes(key);
            sr.nextBytes(nonce);
    
            System.out.println("start encryption");
            Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding");
            try (FileInputStream in = new FileInputStream(filenamePlain);
                 FileOutputStream out = new FileOutputStream(filenameEnc);
                 CipherOutputStream encryptedOutputStream = new CipherOutputStream(out, cipher);) {
                SecretKeySpec secretKeySpec = new SecretKeySpec(key, "ChaCha20");
                System.out.println("keySpec: " + secretKeySpec.getAlgorithm() + " " + secretKeySpec.getFormat());
                System.out.println("cipher algorithm: " + cipher.getAlgorithm());
                //AlgorithmParameterSpec algorithmParameterSpec = new IvParameterSpec(nonce);
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(nonce));
                byte[] buffer = new byte[8096];
                int nread;
                while ((nread = in.read(buffer)) > 0) {
                    encryptedOutputStream.write(buffer, 0, nread);
                }
                encryptedOutputStream.flush();
            }
    
            // decryption
            System.out.println("start decryption");
            Cipher cipherD = Cipher.getInstance("ChaCha20-Poly1305/None/NoPadding");
            try (FileInputStream in = new FileInputStream(filenameEnc); // i don't care about the path as all is lokal
                 CipherInputStream cipherInputStream = new CipherInputStream(in, cipherD);
                 FileOutputStream out = new FileOutputStream(filenameDec)) // i don't care about the path as all is lokal
            {
                byte[] buffer = new byte[8192];
                SecretKeySpec secretKeySpec = new SecretKeySpec(key, "ChaCha20");
                //AlgorithmParameterSpec algorithmParameterSpec = new IvParameterSpec(nonce);
                cipherD.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(nonce));
                int nread;
                while ((nread = cipherInputStream.read(buffer)) > 0) {
                    out.write(buffer, 0, nread);
                }
                out.flush();
            }
    
            // file compare
            System.out.println("compare plain <-> dec: " + Arrays.equals(sha256(filenamePlain), sha256(filenameDec)));
        }
    
        public static void generateRandomFile(String filename, int size) throws IOException, NoSuchAlgorithmException {
            SecureRandom sr = SecureRandom.getInstanceStrong();
            byte[] data = new byte[size];
            sr.nextBytes(data);
            Files.write(Paths.get(filename), data, StandardOpenOption.CREATE);
        }
    
        public static byte[] sha256(String filenameString) throws IOException, NoSuchAlgorithmException {
            byte[] buffer = new byte[8192];
            int count;
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filenameString));
            while ((count = bis.read(buffer)) > 0) {
                md.update(buffer, 0, count);
            }
            bis.close();
            return md.digest();
        }
    }
    

    Edit July 23 2020: Seems to be that not all Java versions with version nr 11.0.8 got fixed. I tested the "original" Oracle Java 1.0.8 for Windows x64 and the error still persists. I reported this to Oracle Bug tracker and it was assigned as a Bug ID: JDK-8249844 (https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8249844).