java - 在解密过程中,如何将GCM身份验证标签放在密码流的末尾需要内部缓冲?

标签 java encryption cryptography aes aes-gcm

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

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

我认为缓冲仅发生在解密过程中,而不是加密过程中,因为首先,在我的Java客户端对从磁盘读取的文件进行加密时,这种缓冲(简短描述)不会出现问题,它总是在服务器端发生,接收这些文件并进行解密。其次,据说here。仅凭我自己的经验来看,我不能确定,因为我的客户使用了 CipherOutputStream 。客户端未在Cipher实例上显式使用方法。因此,我无法推断是否使用了内部缓冲,因为我看不到update-和final方法返回的内容。

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

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

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

鉴于内部缓冲是万恶之源,因此我可以在此处应用的明显解决方案是将文件分成多个块,每个块1 MB-我从没有遇到过发送小文件的问题,只有大文件才有问题。但是,我很难理解为什么首先要进行内部缓冲。

先前链接的SO answer表示,“在密文的末尾添加了GCM:s身份验证标签”,但是“不必在末尾添加了该标签”,这种做法是“弥补了GCM解密的在线本质” ”。

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

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

那么在接收端,服务器不能做同样的事情吗?对于初学者,他可以解密字节(尽管未经身份验证的字节),将其送入哈希算法的更新功能,并在标记到达时完成摘要并验证客户端发送的MAC。

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

更新#1

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

更新#2

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

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

#1

有趣的是,如果我执行解密的服务器未亲自处理密码,而是使用 CipherInputStream ,则不会引发OutOfMemoryError。取而代之的是,客户端设法通过线路传输所有字节,但是在解密过程中的某个地方,请求线程无限期挂起,我可以看到一个Java线程(可能是同一线程)完全利用了CPU内核,而所有文件都保留在该线程上。磁盘无法访问,并且报告的文件大小为0。然后经过了很长的时间,Closeable源关闭了,我的catch子句设法捕获了IOException,其原因是:“javax.crypto.AEADBadTagException:输入太短-需要标记”。

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

请注意,this GitHub project及其关联的blog post表示CipherInputStream已损坏。但是,使用Java 8u25和SunJCE提供程序时,该项目提供的测试对我来说不会失败。就像已经说过的,只要我使用小文件,一切对我来说都是有效的。

最佳答案

简短的答案是update()无法将密文与标记区分开。 final()函数可以。

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

通过等待final()实际上进行任何加密,它知道已提供完整的密文+标签,并且可以在给定标签长度的情况下轻松地将标签从末尾剥离(这在参数spec中提供)。它在更新期间无法进行加密,因为它将要么将某些密文视为标记,反之亦然。

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

加密更直接的原因是它不需要像解密一样修改其输入(纯文本)。它只是将所有数据作为纯文本。在决赛期间,可以轻松将标签附加到密文输出中。

@blaze关于保护您自己的说法是可能的,但在知道所有密文之前,什么也不会返回。仅需要一个密文块(例如,OpenSSL会将其提供给您)。 Sun的实现仅在等待,因为它无法知道密文的第一个块只是密文的第一个块。就其所知,您正在加密的数据少于一个块(需要填充)并一次全部提供标签。当然,即使它确实为您提供了渐进的纯文本,也要等到final()后才能确定其真实性。为此,需要所有密文

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

关于java - 在解密过程中,如何将GCM身份验证标签放在密码流的末尾需要内部缓冲?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26920906/

相关文章:

java - 是否在 for 循环中重新计算条件

java - Blowfish CBC算法空间Padding如何实现

azure - 如何在 Windows Azure(操作系统或网站)中配置完美前向保密

java - 半秒后更新 JLabel 中包含的图片

Java - encodeBase64 方法

java - .NET 和 Java 之间的对称加密

javascript - pidcrypt 和 openssl_crypt 的兼容性问题

android - 在 Android M 和旧版本中使用 KeyStore 加密和解密用户名?

c - 在多少个值之后 rand() 可能会重复一个双工模式?

java - 将解析的 CSV 文件插入 SQL Server