ChaCha20-Poly1305 因 ShortBufferException 输出缓冲区太小而失败

ChaCha20-Poly1305 fails with ShortBufferException Output buffer too small

我正在为大文件进行文件加密基准测试并测试了 ChaCha20-Poly1305,但在解密部分收到错误:

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)

这不是我的程序中的错误,但在我正在使用的 OpenJava 11 中应该得到修复(自 2019 年以来已知,请参阅 https://bugs.openjdk.java.net/browse/JDK-8224997)。 即使使用最新的“早期采用者”版本 (OpenJDK11U-jdk_x64_windows_11.0.7_9_ea),错误仍然存​​在。本次测试 运行 版本 java:11.0.6+8-b520.43.

我的问题是:有没有其他方法可以使用带有原生 JCE 的 ChaCha20-Poly1305 对大文件执行文件加密?

我不想使用 BouncyCastle(因为我正在使用 BC allready 作为对应的基准测试)或将 plainfile 完全读入内存(在我的源代码中 测试文件只有 1.024 字节大,但基准测试最多可测试 1 GB 的文件)。我也不想使用 ChaCha20,因为它不提供任何身份验证。 您可以在我的 Github-Repo https://github.com/java-crypto/Whosebug/tree/master/ChaCha20Poly1305.

中找到 ChaCha20Poly1305Jce.java、ChaCha20Poly1305JceNoStream.java 和 ChaCha20Jce.java 的来源
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: 
        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();
    }
}

使用 OpenJDK 11.0.8 的抢先体验版本(在此处获取:https://adoptopenjdk.net/upstream.html?variant=openjdk11&ga=ea) 我能够 运行(编辑过的)测试程序(现在我正在使用 CipherInput/OutputStreams)。

File En-/Decryption with ChaCha20-Poly1305 JCE
see: 

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

编辑: 我错过了最终版本 (GA) 一个小时,但它仍然有效:

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

新代码:

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: ");
        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();
    }
}

2020 年 7 月 23 日编辑: 似乎并非所有 Java 版本 nr 11.0.8 的版本都已修复。我针对 Windows x64 测试了“原始”Oracle Java 1.0.8,但错误仍然存​​在。我将此报告给 Oracle Bug tracker,它被指定为 Bug ID:JDK-8249844 (https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8249844).