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

Posted

技术标签:

【中文标题】你如何在 Go 中加密大文件/字节流?【英文标题】:How do you encrypt large files / byte streams in Go? 【发布时间】:2018-09-07 20:25:00 【问题描述】:

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

由于真实性要求,处理数据流更加困难。我们不能先加密然后 MAC:本质上,我们通常不知道流的大小。流完成后我们不能发送 MAC,因为这通常由流被关闭来指示。我们不能即时解密流,因为我们必须查看整个密文才能检查 MAC。试图保护一个流给问题增加了巨大的复杂性,没有好的答案。解决方案是将流分成离散的块,并将它们视为消息。

https://leanpub.com/gocrypto/read

文件被分割成 4KiB 块。每个块每次被修改时都会获得一个新的随机 128 位 IV。一个 128 位的身份验证标签 (GHASH) 保护每个块不被修改。

https://nuetzlich.net/gocryptfs/forward_mode_crypto/

如果大量数据被解密,在验证标签被验证之前,并不总是可以缓冲所有解密的数据。将数据分成小块解决了延迟身份验证检查的问题,但引入了一个新问题。这些块可以重新排序... ...因为每个块都是单独加密的。因此,必须以某种方式将块的顺序编码到块本身中,以便能够检测重新排列任意数量的块。

https://github.com/minio/sio

有实际密码学经验的人能指出我正确的方向吗?

更新

我在问了这个问题后意识到,根本无法将整个字节流放入内存(加密 10GB 文件)和字节流 也是是未知长度之间存在差异可以持续很长时间,无需对流的开始进行解码(24 小时实时视频流)。

我最感兴趣的是在需要解码开始之前可以到达流末尾的大 blob。换句话说,加密不需要将整个明文/密文同时加载到内存中。

【问题讨论】:

在开始加密过程之前,您的文件大小是否已知?还是在加密窗口期间计算它们的大小? 两者。我在文件系统上有文件我知道 的大小,以及来自我不信任的客户端的流。但是,无论哪种方式,我都规定了最大字节大小。 【参考方案1】:

正如您在研究中已经发现的那样,对于大文件的身份验证加密并没有什么优雅的解决方案。

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

将文件拆分成块,单独加密每个块,并让每个块有自己的身份验证标签。 AES-GCM 将是为此使用的最佳模式。此方法会导致文件大小与文件大小成比例地膨胀。您还需要为每个块设置一个唯一的随机数。您还需要一种方法来指示块开始/结束的位置。

使用带有缓冲区的 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

【讨论】:

我修改了your code to run a real demo。 AES-CTR 与其他方案相比如何?你如何处理有人改变块的顺序? D.A.R.E 点击率与 GCM 一样接近,只是没有身份验证部分。它是一种流密码,非常适合这种情况。唯一的缺点是,如果您对两条消息重复使用 nonce 和 key,它就会崩溃。但是,只要始终使用随机 nonce,就很容易避免这样做。 Google 云端硬盘客户端(在 Go 中)使用 AES-CTR + AES-512 HMAC,就像您展示的那样。 GoCryptfs 使用GCM with 4kB ordered chunks @LukeJoshuaPark 你可以写一个解密样本并详细说明一下吗?【参考方案2】:

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

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

【讨论】:

以上是关于你如何在 Go 中加密大文件/字节流?的主要内容,如果未能解决你的问题,请参考以下文章

第四次作业:文件的复制速度

常规web流量分析总结及例题(普通http流量,http传输文件流量,https加密流量,视频流)

JAVA输入/输出流程序例题(文件和目录字节流字符流)

php如何将图片转成字节流

如何在Laravel中加密大文件?

如何使用 Go 有效地下载大文件?