如何将GCM身份validation标记放在密码流的末尾,在解密期间需要内部缓冲?

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

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

我假设缓冲仅在解密期间发生而不是加密,因为首先,在我的Java客户端执行从磁盘读取的文件的加密时,不会发生由此缓冲(稍后描述)引起的问题,它总是发生在服务器端,接收这些文件并进行解密。 其次,这里也是如此。 仅根据我自己的经验判断,我无法确定,因为我的客户端使用CipherOutputStream 。 客户端未明确使用Cipher实例上的方法。 因此,我无法推断是否使用了内部缓冲,因为我无法看到更新和最终方法返回的内容。

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

接下来发生的是Cipher.update()抛出一个OutOfMemoryError 。 显然由于内部缓冲区的增长和增长。

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

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

以前链接的SO答案说GCM:s认证标签是“在密文末尾添加的”,但它“不必放在最后”这种做法是“弄乱了GCM的在线性质”解密”。

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

这是我的推理方式。 要计算身份validation标记,或者MAC(如果愿意),客户端使用某种哈希函数。 显然, MessageDigest.update()不使用不断增长的内部缓冲区。

然后在接收端,服务器不能做同样的事情吗? 对于初学者来说,他可以解密字节,尽管是未经validation的字节,将这些字节提供给他的哈希算法的更新function,当标签到达时,完成摘要并validation客户端发送的MAC。

我不是一个加密的人,所以请跟我说话,好像我既愚蠢又疯狂,但又爱得足以照顾一些=)我全心全意地感谢你花时间阅读这个问题,甚至可能会有所启发!

更新#1

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

更新#2

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

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


#1

有趣的是,如果执行解密的服务器本身不处理密码,而是使用CipherInputStream ,则不会抛出OutOfMemoryError。 相反,客户端设法通过线路传输所有字节,但在解密期间的某处,请求线程无限期挂起,我可以看到一个Java线程(可能是同一个线程)完全利用CPU核心,所有这些都保留了文件磁盘不可访问且报告的文件大小为0.然后经过相当长的时间后, Closeable源关闭,我的catch子句设法捕获由以下原因引起的IOException:“javax.crypto.AEADBadTagException:输入太短 – 需要标记”。

使这种情况变得奇怪的是,使用完全相同的代码传输较小的文件完美无缺 – 显然标签可以被正确validation。 该问题必须具有与明确使用密码时相同的根本原因,即不断增长的内部缓冲区。 我无法在服务器上跟踪成功读取/解密的字节数,因为只要读取密码输入流开始,编译器重新排序或其他JIT优化就会使我的所有日​​志记录都消失得无影无踪。 它们[显然]根本没有被执行。

请注意, 此GitHub项目及其相关博客文章称CipherInputStream已损坏。 但是,当使用Java 8u25和SunJCE提供程序时,此项目提供的测试不会失败。 正如已经说过的那样,只要我使用小文件,一切都适合我。

简短的回答是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的一致性。

我不知道为什么,但是当前的实现将你抛出的每个编码字节写入缓冲区直到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; } 

但它只是将数据添加到ibufferByteArrayOutputStream )并返回0作为解密字节数。 然后它在doFinal中完成整个解密。

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

在知道整个密文之前,算法无法判断它是否正确或被篡改。 在解密和validation完成之前,不能返回使用的解密字节。

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

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