使用 Cipher 解密大文件时出现内存不足异常

Kai*_*Kai 4 java encryption exception heap-memory out-of-memory

我试图使用 javax.crypto 下的类和用于输入/输出的文件流来实现加密/解密程序。为了限制内存使用,我使用-Xmx256m参数运行。

它适用于较小文件的加密和解密。但是当解密一个大文件(1G大小)时,出现内存不足的异常:

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3236)
    at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:118)
    at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
    at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:153)
    at com.sun.crypto.provider.GaloisCounterMode.decrypt(GaloisCounterMode.java:505)
    at com.sun.crypto.provider.CipherCore.update(CipherCore.java:782)
    at com.sun.crypto.provider.CipherCore.update(CipherCore.java:667)
    at com.sun.crypto.provider.AESCipher.engineUpdate(AESCipher.java:380)
    at javax.crypto.Cipher.update(Cipher.java:1831)
    at javax.crypto.CipherOutputStream.write(CipherOutputStream.java:166)
Run Code Online (Sandbox Code Playgroud)

这是解密代码:

private final int _readSize = 0x10000;//64k

...

GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(gcmTagSize, iv);
Key keySpec = new SecretKeySpec(key, keyParts[0]);
Cipher decCipher = Cipher.getInstance("AES/GCM/PKCS5Padding");

decCipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);

try (InputStream fileInStream = Files.newInputStream(inputEncryptedFile);
    OutputStream fileOutStream = Files.newOutputStream(outputDecryptedFile)) {
    try (CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutStream, decCipher)) {
        long count = 0L;
        byte[] buffer = new byte[_readSize];

        int n;
        for (; (n = fileInStream.read(buffer)) != -1; count += (long) n) {
            cipherOutputStream.write(buffer, 0, n);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

gcmTagSize 和 iv 等关键参数是从关键文件中读取的,对于较小的文件(例如大小约为 50M 的文件)来说它可以正常工作。

据我了解,每次只有64k数据传递给解密,为什么它会耗尽堆内存?我怎样才能避免这种情况?

编辑:

实际上我尝试过使用 4k 作为缓冲区大小,但由于同样的异常而失败。

编辑2:

经过更多测试,它可以处理的最大文件大小约为堆大小的 1/4。例如,如果设置-Xmx256m,大于64M的文件将无法解密。

Par*_*fal 5

这似乎是 GCM 模式实施的问题。我不确定你是否可以解决这个问题。

如果您查看堆栈跟踪:

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3236)
    at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:118)
    at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
    at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:153)
    at com.sun.crypto.provider.GaloisCounterMode.decrypt(GaloisCounterMode.java:505)
Run Code Online (Sandbox Code Playgroud)

ByteArrayOutputStream从 内部写入时会发生内存不足错误GaloisCounterMode。您使用 a FileOutputStream,因此要么您没有显示正确的代码,要么这ByteArrayStream是在内部使用的。

如果您查看GaloisCounterMode 的源代码,您会发现它定义了一个内部ByteArrayOutputStream(它实际上定义了两个,但我认为这就是问题所在):

    // buffer for storing input in decryption, not used for encryption
    private ByteArrayOutputStream ibuffer = null;
Run Code Online (Sandbox Code Playgroud)

然后,稍后,它将字节写入该流。注意代码注释。

    int decrypt(byte[] in, int inOfs, int len, byte[] out, int outOfs) {
        processAAD();

        if (len > 0) {
            // store internally until decryptFinal is called because
            // spec mentioned that only return recovered data after tag
            // is successfully verified
            ibuffer.write(in, inOfs, len);
        }
        return 0;
    }
Run Code Online (Sandbox Code Playgroud)

该缓冲区直到 才重置decryptFinal()


编辑:看看这个 CSx 答案,看起来 GCM 需要缓冲整个流。如果您有大文件且内存不足,那么这将是一个非常糟糕的选择。

我认为最好的解决方案是切换到 CBC 模式。