在Java中,"默认" AES / GCM提供程序SunJCE将 - 在解密过程中 - 内部缓冲区1)加密字节用作输入或2)解密字节作为结果生成。执行解密的应用程序代码会注意到Cipher.update(byte[])
返回一个空字节数组,Cipher.update(ByteBuffer, ByteBuffer)
返回写入长度为0.然后,当该过程完成时,Cipher.doFinal()
将返回所有解码字节。
第一个问题是:缓冲的是哪个字节,上面是数字1还是数字2?
我认为缓冲仅在解密过程中发生而不是加密,因为首先,我的Java客户端在执行从磁盘读取的文件的加密时,不会发生由此缓冲(很快描述)引起的问题,它总是发生在服务器端,接收这些文件并进行解密。其次,据说here。仅根据我自己的经验判断,我无法确定,因为我的客户使用了CipherOutputStream
。客户端未明确使用Cipher实例上的方法。因此,我无法推断是否使用了内部缓冲,因为我无法看到更新和最终方法返回的内容。
当我从客户端传输到服务器的加密文件变大时,我出现了真正的问题。我的意思是超过100 MB。
然后,Cipher.update()会抛出OutOfMemoryError
。显然由于内部缓冲区的增长和增长。
此外,尽管内部缓冲并且没有从Cipher.update()收到结果字节,Cipher.getOutputSize(int)会不断报告不断增长的目标缓冲区长度。因此,我的应用程序代码被迫分配一个不断增长的ByteBuffer
,它被提供给Cipher.update(ByteBuffer,ByteBuffer)。如果我试图欺骗并传入容量较小的字节缓冲区,则更新方法抛出ShortBufferException
#1 。知道我创建了大量的字节缓冲区以供不使用,这是非常令人沮丧的。
鉴于内部缓冲是所有邪恶的根源,那么我在这里应用的明显解决方案是将文件分成块,每个1 MB - 我从来没有问题发送小文件,只有大文件。但是,我很难理解为什么内部缓冲首先发生。
之前链接的SO answer表示GCM的身份验证标记是在密文"末尾添加的,但是它不需要放在最后" #34;而这种做法就是"弄乱了GCM解密的在线性质"。
为什么将标签放在最后只会扰乱服务器的解密工作?
以下是我的推理方式。要计算身份验证标记或MAC(如果您愿意),客户端将使用某种哈希函数。显然,MessageDigest.update()
不使用不断增长的内部缓冲区。
然后在接收端,服务器不能做同样的事情吗?对于初学者,他可以解密字节,尽管是未经验证的字节,将其提供给哈希算法的更新功能,当标签到达时,完成摘要并验证客户端发送的MAC。
我不是一个加密的家伙,所以请告诉我,好像我既愚蠢又疯狂但又爱得足以照顾一些=)我全心全意地感谢你花时间阅读这个问题,甚至可能有所启发!
我没有使用AD(相关数据)。
使用Java编写演示AES / GCM加密的软件,以及Java EE中的Secure Remote Protocol(SRP)和二进制文件传输。前端客户端使用JavaFX编写,可用于动态更改加密配置或使用块发送文件。在文件传输结束时,会显示一些有关传输文件所用时间和服务器解密时间的统计信息。该存储库还有一个文档,其中包含我自己的一些GCM和Java相关研究。
享受:https://github.com/MartinanderssonDotcom/secure-login-file-transfer/
有趣的是,如果执行解密的服务器本身不处理密码,而是使用CipherInputStream
,则不会抛出OutOfMemoryError。相反,客户端设法通过线路传输所有字节,但在解密过程中某处,请求线程无限期挂起,我可以看到一个Java线程(可能是同一个线程)完全利用CPU核心,同时保留文件磁盘不可访问且报告的文件大小为0.然后,在很长一段时间后,Closeable
源被关闭,我的catch子句设法捕获由以下原因引起的IOException:" javax.crypto.AEADBadTagException :输入太短 - 需要标签"。
使这种情况变得奇怪的是,使用完全相同的代码传输较小的文件完美无缺 - 所以显然可以正确验证标签。该问题必须具有与明确使用密码时相同的根本原因,即不断增长的内部缓冲区。我无法在服务器上跟踪成功读取/解密的字节数,因为只要读取密码输入流开始,编译器重新排序或其他JIT优化就会使我的所有日志记录都消失得无影无踪。它们[显然]根本没有被执行。
请注意,this GitHub project及其关联的blog post表示CipherInputStream已损坏。但是,当使用Java 8u25和SunJCE提供程序时,此项目提供的测试不会失败。正如已经说过的那样,只要我使用小文件,一切都适合我。
答案 0 :(得分:5)
简短的回答是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的一致性。
答案 1 :(得分:2)
我不知道为什么,但是当前的实现将你抛出的每个编码字节写入缓冲区,直到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完成了整个解密。
鉴于实施,您唯一的选择是避免加密或手动构建您知道服务器可以处理的数据块。没有办法提前提供标签数据并使其表现得更好。
答案 2 :(得分:0)
在知道整个密文之前,算法无法判断它是否正确或被篡改。在解密和验证完成之前,不能返回使用的解密字节。
密文缓冲可能是由@NameSpace提到的原因引起的,但明文缓冲是为了不让你射入自己的腿。
您最好的选择是以小块加密数据。并且不要忘记改变它们之间的nonce值。