你如何在 Go 中加密大文件/字节流?

How do you encrypt large files / byte streams in Go?

我有一些大文件想在通过网络发送或保存到磁盘之前进行 AES 加密。虽然 encrypt streams, there seems to be warnings against doing this 似乎是可行的,但人们建议将文件分成块并使用 GCM 或 crypto/nacl/secretbox。

Processing streams of data is more difficult due to the authenticity requirement. We can’t encrypt-then-MAC: by it’s nature, we usually don’t know the size of a stream. We can’t send the MAC after the stream is complete, as that usually is indicated by the stream being closed. We can’t decrypt a stream on the fly, because we have to see the entire ciphertext in order to check the MAC. Attempting to secure a stream adds enormous complexity to the problem, with no good answers. The solution is to break the stream into discrete chunks, and treat them as messages.

Files are segmented into 4KiB blocks. Each block gets a fresh random 128 bit IV each time it is modified. A 128-bit authentication tag (GHASH) protects each block from modifications.

If a large amount of data is decrypted it is not always possible to buffer all decrypted data until the authentication tag is verified. Splitting the data into small chunks fixes the problem of deferred authentication checks but introduces a new one. The chunks can be reordered... ...because every chunk is encrypted separately. Therefore the order of the chunks must be encoded somehow into the chunks itself to be able to detect rearranging any number of chunks.

任何具有实际密码学经验的人都可以指出正确的方向吗?

更新

我在问了这个问题后意识到,根本无法将整个字节流放入内存(加密 10GB 文件)和字节流之间是有区别的 是一个未知的长度,可能会持续很长时间,直到流开始被解码(24 小时实时视频流)。

我最感兴趣的是大型 blob,在需要解码开始之前可以到达流的末尾。也就是说,不需要将整个plaintext/ciphertext同时加载到内存的加密

正如您已经从研究中发现的那样,对于大文件的已验证加密,没有多少优雅的解决方案。

传统上有两种方法可以解决这个问题:

  • 将文件拆分成块,对每个块单独加密,让每个块都有自己的身份验证标签。 AES-GCM 将是用于此的最佳模式。此方法会导致文件大小与文件大小成比例地膨胀。您还需要为每个块分配一个唯一的随机数。您还需要一种方法来指示块 begin/end.

  • 的位置
  • 使用 AES-CTR 和缓冲区进行加密,在 HMAC 上为每个加密数据缓冲区调用 Hash.Write。这样做的好处是可以一次完成加密。缺点是解密需要一次通过来验证 HMAC,然后再通过一次来实际解密。这里的好处是文件大小保持不变,加上大约 48 字节左右的 IV 和 HMAC 结果。

两者都不理想,但对于非常大的文件(~2GB 或更大),第二个选项可能是首选。

我已经包含了一个使用下面第二种方法在 Go 中进行加密的示例。在这种情况下,最后 48 个字节是 IV(16 个字节)和 HMAC 的结果(32 个字节)。还要注意 IV 的 HMACing。

const BUFFER_SIZE int = 4096
const IV_SIZE int = 16

func encrypt(filePathIn, filePathOut string, keyAes, keyHmac []byte) error {
    inFile, err := os.Open(filePathIn)
    if err != nil { return err }
    defer inFile.Close()

    outFile, err := os.Create(filePathOut)
    if err != nil { return err }
    defer outFile.Close()

    iv := make([]byte, IV_SIZE)
    _, err = rand.Read(iv)
    if err != nil { return err }

    aes, err := aes.NewCipher(keyAes)
    if err != nil { return err }

    ctr := cipher.NewCTR(aes, iv)
    hmac := hmac.New(sha256.New, keyHmac)

    buf := make([]byte, BUFFER_SIZE)
    for {
        n, err := inFile.Read(buf)
        if err != nil && err != io.EOF { return err }

        outBuf := make([]byte, n)
        ctr.XORKeyStream(outBuf, buf[:n])
        hmac.Write(outBuf)
        outFile.Write(outBuf)

        if err == io.EOF { break }
    }

    outFile.Write(iv)
    hmac.Write(iv)
    outFile.Write(hmac.Sum(nil))

    return nil
}

加密后使用HMAC是有效的方法。但是,HMAC 可能会非常慢,尤其是在使用 SHA-2 时。实际上,您可以对 GMAC(GCM 的基础 MAC)执行相同的操作。找到一个实现可能很棘手,但是 GMAC 在密文之上,所以如果你真的想要,你可以简单地单独执行它。还有其他方法,例如用于 TLS 1.2 和 1.3 的带有 AES 的 Poly1305。

对于 GCM(或 CCM 或 EAX 或任何其他经过身份验证的密码),您需要验证块的顺序。为此,您可以创建一个单独的文件加密密钥,然后使用随机数输入(12 字节 IV)来指示块的编号。这将解决 IV and 的存储确保块是有序的。您可以使用 KDF 生成文件加密密钥(如果您有唯一的方式来指示文件)或使用主密钥包装随机密钥。