6

在 Java 中,“默认”AES/GCM 提供程序 SunJCE 将 - 在解密过程中 - 在内部缓冲 1)用作输入的加密字节或 2)作为结果生成的解密字节。执行解密的应用程序代码会注意到Cipher.update(byte[])返回一个空字节数组并Cipher.update(ByteBuffer, ByteBuffer)返回写入长度 0。然后当过程完成时,Cipher.doFinal()将返回所有解码的字节。

第一个问题是:上面的数字 1 或数字 2 正在缓冲哪些字节?

我假设缓冲只发生在解密期间而不是加密期间,因为首先,这种缓冲产生的问题(简短描述)不会发生在我的 Java 客户端对从磁盘读取的文件进行加密时,它总是发生在服务器端,接收这些文件并进行解密。其次,这里是这么说。仅根据我自己的经验判断,我无法确定,因为我的客户使用CipherOutputStream. 客户端没有显式使用 Cipher 实例上的方法。因此我无法推断是否使用了内部缓冲,因为我看不到 update- 和 final 方法返回什么。

当我从客户端传输到服务器的加密文件变大时,我的真正问题就出现了。大我的意思是超过 100 MB。

然后发生的是 Cipher.update() 抛出一个OutOfMemoryError. 显然是由于内部缓冲区越来越大。

此外,尽管有内部缓冲并且没有从 Cipher.update() 接收到结果字节,但Cipher.getOutputSize(int) 会持续报告不断增长的目标缓冲区长度。因此,我的应用程序代码被迫分配一个不断增长ByteBuffer的输入 Cipher.update(ByteBuffer, ByteBuffer)。如果我尝试作弊并传入容量较小的字节缓冲区,则更新方法会抛出#1。知道我创建了无用的巨大字节缓冲区是非常令人沮丧的。ShortBufferException

鉴于内部缓冲是万恶之源,那么我在这里应用的明显解决方案是将文件分成块,每个块 1 MB - 我发送小文件从来没有问题,只有大文件。但是,我很难理解为什么首先会发生内部缓冲。

先前链接的SO 答案说 GCM:s 身份验证标签“添加在密文的末尾”,但它“不必放在末尾”,这种做法“搞乱了 GCM 的在线性质”解密”。

为什么将标签放在最后只会弄乱服务器的解密工作?

这就是我的推理方式。为了计算身份验证标签或 MAC(如果您愿意),客户端使用某种散列函数。显然,MessageDigest.update()不使用不断增长的内部缓冲区。

那么在接收端,服务器不能做同样的事情吗?首先,他可以解密字节,尽管是未经身份验证的字节,将它们输入哈希算法的更新函数,当标签到达时,完成摘要并验证客户端发送的 MAC。

我不是一个加密人,所以请跟我说话,就好像我既愚蠢又疯狂,但足够爱关心一些人=)我衷心感谢您花时间阅读这个问题,甚至可能会有所启发!

更新#1

我不使用 AD(关联数据)。

更新#2

编写的软件演示了使用 Java 的 AES/GCM 加密,以及 Java EE 中的安全远程协议(SRP) 和二进制文件传输。前端客户端是用 JavaFX 编写的,可用于动态更改加密配置或使用块发送文件。在文件传输结束时,会显示一些有关传输文件所用时间和服务器解密时间的统计信息。该存储库还有一个文档,其中包含我自己的一些 GCM 和 Java 相关研究。

享受:https ://github.com/MartinanderssonDotcom/secure-login-file-transfer/


#1

有趣的是,如果进行解密的服务器自己不处理密码,而是使用 a CipherInputStream,则不会抛出 OutOfMemoryError 。相反,客户端设法通过线路传输所有字节,但在解密期间的某个地方,请求线程无限期挂起,我可以看到一个 Java 线程(可能是同一个线程)充分利用了 CPU 内核,同时保持文件打开磁盘不可访问并且报告的文件大小为 0。然后在很长一段时间后,Closeable源被关闭,我的 catch 子句设法捕获由以下原因引起的 IOException:“javax.crypto.AEADBadTagException:输入太短 - 需要标签” .

使这种情况变得奇怪的是,传输较小的文件可以使用完全相同的代码完美地工作 - 因此显然可以正确验证标签。问题的根本原因必须与显式使用密码时相同,即不断增长的内部缓冲区。我无法在服务器上跟踪成功读取/解密了多少字节,因为一旦开始读取密码输入流,编译器重新排序或其他 JIT 优化就会使我所有的日志记录语句化为乌有。他们[显然]根本没有被执行。

请注意,这个 GitHub 项目及其相关的博客文章说 CipherInputStream 已损坏。但是当我使用 Java 8u25 和 SunJCE 提供程序时,这个项目提供的测试对我来说并没有失败。正如已经说过的,只要我使用小文件,一切都对我有用。

4

3 回答 3

7

简短的回答是 update() 无法区分密文和标签。final() 函数可以。

长答案:由于 Sun 的规范要求将标签附加到密文中,因此需要在解密期间(或更确切地说,在解密之前)从源缓冲区(密文)中剥离标签。然而,由于密文可以在多次 update() 调用过程中提供,Sun 的代码不知道何时取消标记(在 update() 的上下文中)。最后一个 update() 调用不知道它是最后一个 update() 调用。

通过等到 final() 实际执行任何加密,它知道已经提供了完整的密文 + 标签,并且它可以轻松地将标签从末尾剥离,给定标签长度(在参数规范中提供)。它不能在更新期间进行加密,因为它会将某些密文视为标签,反之亦然。

基本上,这是简单地将标签附加到密文的缺点。大多数其他实现(例如 OpenSSL)将提供密文和标签作为单独的输出(final() 返回密文,其他一些 get() 函数返回标签)。Sun 无疑选择这样做是为了使 GCM 适合他们的 API(并且不需要开发人员提供特殊的 GCM 特定代码)。

加密更直接的原因是它不需要像解密那样修改其输入(明文)。它只是将所有数据作为明文。在决赛期间,标签很容易附加到密文输出中。

@blaze 关于保护你免受自己的伤害是一个可能的理由,但在知道所有密文之前不能返回任何东西是不正确的。只需要一个密文块(例如,OpenSSL 会给你)。Sun 的实现只是等待,因为它不知道第一个密文块只是第一个密文块。据它所知,您加密的块少于一个块(需要填充)并一次提供所有标签。当然,即使它确实逐渐地为您提供了明文,您也无法确定真实性,直到 final()。为此需要所有密文

当然,Sun 可以通过多种方式完成这项工作。通过特殊函数传递和检索标签,在 init() 期间要求密文的长度,或者要求在 final() 调用中传递标签都可以。但是,就像我说的,他们可能希望使用尽可能接近其他 Cipher 实现并保持 API 的一致性。

于 2015-05-26T21:10:08.937 回答
3

我不知道为什么,但无论你做什么,当前的实现都会将你扔给它的每个编码字节写入缓冲区,直到 doFinal()。

源代码可以在这里找到:GaloisCounterMode.java

该方法被调用update并被赋予字节(+缓冲的),并且应该在可以解密的情况下进行解密。

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;
}

但它只是将数据添加到ibuffer( ByteArrayOutputStream) 并返回 0 作为解密字节数。然后它在 doFinal 中进行整个解密。

鉴于这种实现,您唯一的选择是避免加密或手动构建您知道您的服务器可以处理的数据块。没有办法提前提供标签数据并使其表现得更好。

于 2014-11-14T01:29:44.740 回答
1

在知道整个密文之前,算法无法判断它是正确的还是被篡改的。在解密和验证完成之前,不能返回任何已解密的字节以供使用。

密文缓冲可能是由@NameSpace 提到的原因引起的,但明文缓冲在这里是为了不让你射到自己的腿上。

您最好的选择是以小块加密数据。并且不要忘记更改它们之间的 nonce 值。

于 2014-11-14T01:57:51.070 回答