java.lang.OutOfMemoryError 当 encrypting/decrypting 大文件时

java.lang.OutOfMemoryError when encrypting/decrypting large files

问题

我正在尝试为 Android 制作一个文件加密器(加密算法是 AES)。一切正常,直到我尝试 encrypt/decrypt 一个大文件。例如,当我尝试加密一个 771 MB 的文件时,出现以下错误:

E/AndroidRuntime: FATAL EXCEPTION: Thread-6
Process: com.suslanium.encryptor, PID: 27638
java.lang.OutOfMemoryError: Failed to allocate a 536869904 byte allocation with 25165824 free bytes and 254MB until OOM, target footprint 295637424, growth limit 536870912
    at com.android.org.conscrypt.OpenSSLCipher$EVP_AEAD.expand(OpenSSLCipher.java:1219)
    at com.android.org.conscrypt.OpenSSLCipher$EVP_AEAD.updateInternal(OpenSSLCipher.java:1336)
    at com.android.org.conscrypt.OpenSSLCipher.engineUpdate(OpenSSLCipher.java:323)
    at javax.crypto.Cipher.update(Cipher.java:1722)
    at javax.crypto.CipherOutputStream.write(CipherOutputStream.java:158)
    at com.suslanium.encryptor.MainActivity.encryptFileAES_GCM(MainActivity.java:912)
    at com.suslanium.encryptor.MainActivity.run(MainActivity.java:794)
    at java.lang.Thread.run(Thread.java:919)

我的代码片段

public void encryptFileAES_GCM(File file , File fileToSave) throws Exception {
    File keyEncrypted = new File(pathToStorage + "EncryptedKey.enc");
    File IVEncrypted = new File(pathToStorage + "EncryptedKIV.enc");
    if (keyEncrypted.exists() && IVEncrypted.exists()) {
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(fileToSave);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        SecretKeySpec keySpec = new SecretKeySpec(globalKey, "AES");
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, globalIV);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
        CipherOutputStream cos = new CipherOutputStream(fos, cipher);
        int b;
        byte[] d = new byte[512];
        while ((b = fis.read(d)) != -1) {
            cos.write(d, 0, b); //Line 912
        }
        cos.flush();
        cos.close();
        fis.close();
    } else {
        throw new Exception("Key or IV does not exist, encryption can't be done. Please create a key first.");
    }
}

我已经尝试寻找一些解决方案,但没有成功。我不知道这段代码有什么问题。缓冲区大小不是太大,但即使在减小它之后 - 也没有任何反应。谁能帮帮我?

更新

问题已解决。在加密大文件时,我决定使用以下算法:将文件分成 X 大小的块,加密这些块并将它们放入 zip 存档中。解密大文件时,需要从压缩包中提取之前加密过的chunk,解密后将解密后的chunk合并为一个文件。

android:hardwareAccelerated="false" , android:largeHeap="true"

将这些添加到您的清单文件应用程序标签中

<application
android:allowBackup="true"
android:hardwareAccelerated="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">

注意:你的代码到处都是并且看起来完全不安全。即使你确实解决了这个问题。请参阅下面的注释。

您在 conscrypt 中发现了一个“错误”。根据规定的规范,Conscrypt 在技术上满足要求。我们可以指责 android/google 为图书馆提供这样一个无用的借口,或者指责社区没有更好的图书馆,指责规范作者允许这样一个无用的借口声称他们在技术上不违反规范,或在 concrypt 编写如此糟糕的实现。我把它留给你。结果是:它不起作用,它不会起作用,任何你愿意向其投诉、修复它的人都会简单地将矛头指向别处。

具体来说,conscrypt 只会缓存所有内容,直到最后才执行加密,这很奇怪,但这就是它的工作原理。有关证明和详细信息,请参阅

解决方案:

我没有 android phone 来为你测试这个,我很少做 android 开发,这使事情变得复杂 - 这里没有明显的解决办法。

  1. 考虑使用 BouncyCastle。 android 曾经随附它,并且在某种程度上仍然是为了向后兼容,但我很确定它不会在这里可靠地使用,所以你必须考虑将 BC 库作为你的一部分应用程序。考虑使用特定于 BC 的 API(不是构建在 javax.crypto 之上),它保证 android 不会对您进行切换并选择损坏的 concrypt 实现。

  2. 在 conscrypt 中找到写得如此糟糕的代码并发送带有修复的拉取请求。如果您提交错误,他们可以整天指责,但他们的代码库有所改进,一切准备就绪,只需按合并 - 他们更难否认。

  3. 更改您的代码以改为保存在块中。写一个小协议:文件不是作为加密的 blob 存储的,而是作为一系列加密单元存储的,其中每个单元都是一个长度(作为一个大端 32 位整数),然后是那么多字节的加密,然后是另一个单元。一系列密码单元总是以长度为 0 的单元结尾,这告诉您它已完成。要编写这些,选择任意大小(可能是 10MB:10*1024*1024),然后写入数据(我猜也可能在内存中完成所有操作 - 在任何情况下 concrypt 都会这样做),然后你知道有多少您拥有的数据,然后可以通过写入长度来保存,然后写入所有数据。然后继续,直到你的文件完成。

注意:你的代码到处都是并且看起来完全不安全。你有 'keyEncrypted' 和 'IVEncrypted',然后你不使用它们(除非在这些文件不可用时异常中止)。相反,您直接使用 'global key' 进行加密。

这意味着任何人都可以解密您的应用程序生成的任何内容,除了随处可见的应用程序 apk 之外什么都不需要。这肯定不是你想要的。也许你打算使用全局密钥解密iv和key,然后使用解密的iv/key组合来加密或解密文件?请注意,这个 'encryptedIV.key' 恶意是安全问题。即使你修复了你的错误,它也是糟糕的加密:使用你的应用程序的人看到你将这些文件命名为 encryptedKey,然后可能会认为它们是加密的。这是一个误导性的名称:它们是用 public 信息加密的,用 public 信息加密的东西是愚蠢的:你想完成什么?一个 2 位黑客用 5 秒来阅读你的代码(即使你试图混淆它)会弄清楚这一点,提取 'globalKey',并解密这些文件。

如果用户自己不需要输入任何密码来启动某些加密或解密,您就不能使用 android 内置的安全功能例如要求他们通过指纹扫描进行确认,然后接受任何有权访问 phone 的磁盘内容或 phone 本身已解锁的人都可以读取所有这些内容,期间,并通过不建议来明确这些密钥文件已加密(因为它们实际上没有加密,因为它们加密的唯一内容是 public 众所周知的密钥,这不算数)。