将 CipherInputStream 与套接字一起使用,查找未加密文件的大小

Using CipherInputStream with Sockets, finding size of unencrypted file

我在两台机器之间发送文件,通常的做法是先发送文件的大小,以字节为单位,然后让另一端读取流,直到它收到准确的字节数,它写入 BufferedFileOutputStream。 像这样(接收端):

long dimension = (long) inStream.readObject();
BufferedOutputStream receivedFileBuffer = new BufferedOutputStream(new FileOutputStream("receivedFile"));

byte[] buffer = new byte[1024];

long count = 0;
int bytesRead;
while (count < dimension) {
    bytesRead = inStream.read(buffer, 0, (int) Math.min(dimension - count, 1024));

    receivedFileBuffer.write(buffer, 0, bytesRead);

    count += bytesRead;

}

receivedFileBuffer.flush();
receivedFileBuffer.close();

但是,我发送的文件实际上是用 AES 加密的,在发送端我通过 CipherInputStream 读取数据,它在通过套接字发送数据之前对其进行解密,例如:

// Previously created AES Cipher, BufferedFileInputStream, etc
CipherInputStream decryptionStream = new CipherInputStream(fileInputStream, decryptionCipher);

byte[] buffer = new byte[1024];

int bytesRead;
while ((bytesRead = decryptionStream.read(buffer, 0, 1024)) > 0) {
    outStream.write(buffer, 0, bytesRead);
}

decryptionStream.close();
fileInputStream.close();
outStream.flush();

我的问题是填充。由于我的实现要求我在传输之前发送文件大小,因此我 运行 遇到了一个问题,它将发送加密文件的大小,由于 AES 填充,它可以大 1-16 个字节.

所以发生的是接收端期望接收加密文件大小的字节,而实际上 CipherInputStream 只会产生未加密文件大小的字节。有什么方法可以知道未加密文件的大小而不必将其全部加载到内存中吗?

我不打算更改我的实现以使用不需要填充的 AES 模式,因为我不打算存储 IV。提前致谢。

I'm sending files between two machines using the common practice of sending the dimension of the file first, in bytes, and then having the other side read the stream until it has received the exact amount of bytes

如您所知,此 'common practice' 不能与加密一起使用。填充在理论上是可以预测的,但这是大多数加密库有意不公开的细节。因此,至少可以说这很老套,并且需要您对 AES 算法(或者更确切地说,您指定的 padding/IV 算法)有一些相当深入的了解。鉴于您现在正在编写专门针对您选择的确切填充算法的代码,您现在也让自己陷入困境:如果您想修改正在使用的算法,现在它比简单地替换设置的调用更复杂密码流:您还需要调整 'padding size calculator' 代码,即 non-trivial.

换句话说,这是个坏主意。

一般说明

您从一个流到另一个流的 'copy' 字节的片段是有问题的。您没有使用 try-with,因此任何中途崩溃都会泄漏资源,并且您没有使用 .transferToreadAllBytes/readNBytes。您正在使用 BufferedOutputStream 进行缓冲 - 无缘无故地创建 2 个缓冲区。

请记住这个答案其余部分的片段,因为我正在即时修复这些疏忽。

解决方案 1:错误的解决方案

你当然可以先将整个东西加密到磁盘或内存,然后你可以先发送加密数据的大小,然后再发送加密字节。但这需要磁盘 space 或内存 space 而您想避免这种情况。

解决方案 2:简单的解决方案

为什么你先送尺寸?如果流刚刚结束,就像你 close/flush 那样,就没有必要了。 TCP/IP 流完全能够发出它们已关闭的信号。您的服务器应该:

  • 为要发送的文件创建一个 InputStream。
  • 将其包装在密码流中
  • 在套接字上打开一个 OutputStream。或网络连接 - 他们也可以在带外发出 'close' 信号。
  • 只需全部传输,然后关闭所有流。

您的客户几乎也是这样做的。看起来像这样:

try (var fileOut = new FileOutputStream("receivedFile");
  var cipher = new CipherInputStream(fromSocket, decryptionCipher)) {

  cipher.transferTo(fileOut);
}

看看那个美女。这么小,这么简单。它将所有带有缓冲区和计数的混乱业务交给 transferTo 方法。您 没有 使用 transferto - 将其应用于您的过时代码,而不是计算您已处理的字节数,您只需永远循环,直到 in.read(buffer); returns -1,然后你爆发:你已经全部转移了。事实上,这正是 .transferTo 所做的。

解决方案 3:分块

也许您需要保持数据流畅通:这是一个复杂的协议,您要通过该协议发送许多不同的概念;你不能依赖 close() 作为信号。

在那种情况下,它会变得更难。现在您确实需要开发某种小协议。一个简单的方法是将大小与指示完成的特殊标记值交错。

发送:

// SERVER CODE:
// You have a 'socketOutputStream' from somewhere.

try (var fileIn = new FileInputStream("toSend");
  var cipherOut = new CipherOutputStream(socketOutputStream, crypto)) {

  byte[] buffer = new byte[65537];
  while (true) {
    int r = in.read(buffer, 2, 65535);
    if (r == -1) break;
    buffer[0] = (byte) (r >> 8);
    buffer[1] = (byte) r;
    cipherOut.write(buffer, 0, r + 2);
  }
  buffer[0] = 0; buffer[1] = 0;
  cipherOut.write(buffer, 0, 2);
  cipherOut.flush();
}

此代码始终将读取数据的大小作为无符号 2 字节值粘贴在前面,在最后发送 size = 0 告诉客户端:我们现在完成了。该系统可用于加密许多 GB 大小的文件,而不需要大量内存或磁盘 space。

客户端反向执行类似任务:

// CLIENT CODE:
// You have a 'socketInputStream' from somewhere.

try (var fileOut = new FileOutputStream("toReceive");
  var cipherIn = new CipherInputStream(socketInputStream, crypto)) {
  byte[] sizeBuffer = new byte[2];
  byte[] dataBuffer = new byte[65535];
  while (true) {
    cipherIn.readNBytes(sizeBuffer, 0, 2);
    int r = ((sizeBuffer[0] & 0xFF) << 8) | (sizeBuffer[1] & 0xFF);
    if (r == 0) break;
    cipherIn.readNBytes(dataBuffer, 0, r);
    fileOut.write(dataBuffer, 0, r);
  }
}

但是,我重复一遍:如果流之后立即关闭,这会使事情毫无意义地复杂化!

解决方案 4:Multi-connection

只有当您需要在一个 'session' 中完成多个任务时,这个也才有意义。

最后一个选择是有一个 'command' 行和一个 'data' 行:你永远不会通过命令行发送大数据,你只是发送简单的文本消息(加密或不加密 - 但因为它们很小,你不必担心必须流式传输所有这些东西,这一切都可以在内存中完成)。客户端发送到服务器的一条文本消息可能是 'DOWNLOAD fileToSend'.

服务器没有响应加密文件。不,它以类似 'READY 91584145104981234098124' 的方式响应,其中该数字是随机生成的 ID。客户端现在应该打开第二个连接;如果它是原始 TCP/IP,到相同的端口,但它现在发送 'FETCH 91584145104981234098124',此时服务器发送文件,加密,然后关闭连接(因此让您使用简单的 .transferTo代码)。如果是网络,那么您的客户端会打开 https://myserver.com/fetch/91584145104981234098124 以获得类似的效果 - 尽管如果是网络,https 已经在加密内容,不确定为什么您还需要对其进行加密。