数据静态加密的 Go 实现

简介

加密网络流量正成为默认做法。有像 SSH 和 TLS 这样的标准协议,以及像 Let’s Encrypt 这样的项目来保护网络上发送的数据。例如,TLS 会获取数据流,将其分割成消息块,并在通过网络发送之前加密每个消息。TLS 确保每个消息都经过加密和身份验证,以防止攻击者读取或修改通过 TLS 连接发送的任何数据。

令人惊讶的是,在文件加密(或更准确地说是静态数据加密)方面,没有可比的标准或解决方案。无论何时需要将数据安全地存储在不受信任的存储上,您或多或少都只能依靠自己。结果,许多项目和产品被发明出来,以提供某种形式的文件或数据加密。不幸的是,这些解决方案都是孤岛,不兼容问题是普遍现象。更糟糕的是,文件加密经常被错误地执行。以 GPG 为例,Adam Langley 的博文 中对此进行了说明。

DARE 格式

Minio,我们希望能够让用户存储加密后的数据。但我们不想再发明一个无人使用的孤岛解决方案,而是希望提供一个通用的静态数据加密解决方案。因此,我们提出了一个用于以安全方式加密任意数据流的 格式。该格式应该

  • 提供加密数据的机密性和完整性。
  • 适用于多种用例,而不仅仅是解决我们的特定问题。
  • 尽可能简单。
  • 保持较低的开销。

提供加密数据的机密性和完整性是最重要的特性。我们以非常简单的方式组合了所有原语,以减小攻击面并降低实现复杂度。因此,数据流被分成一系列数据包,并且每个数据包都单独加密。加密方式使得在不破坏所用密码的情况下,无法读取或修改加密数据包。加密数据包如下所示

header (16 bytes) | payload (1 byte - 64 KB) | tag (16 bytes)

标头包含数据包的一些元数据,例如版本、密码套件、有效负载长度、序列号和 nonce。标签是在加密有效负载时计算出的校验和,并且取决于标头和有效负载的每个字节。16 字节的标头包含以下字段

version | cipher | payload length | sequence number | nonce
1 byte    1 byte       2 bytes          4 bytes       8 bytes

每个数据包都使用 AEAD 密码(AES-256-GCM 或 ChaCha20Poly1305)进行加密。序列号基本上是一个计数器,在每个加密数据包后递增,以防止数据包重新排序。nonce 的目的是减少密钥重复使用造成的损害。如果加密密钥曾经被重复使用(并且没有 nonce),攻击者将能够对具有相同序列号的两个数据包的有效负载进行异或运算,从而得到两个明文文本的异或结果。nonce 是随机生成的,可以缓解由意外密钥重复使用引起的此类攻击。我们不是为每个数据包生成一个新的 nonce,而是在加密过程开始时生成一次 nonce,并在每个数据包标头中重复使用它。原因是为每个数据包生成单独的 nonce 没有优势。更糟糕的是,如果 nonce 是为每个数据包而不是每个数据流随机生成的,则在使用相同密钥加密的两个不同数据流中两次使用相同 nonce 的概率会更高。

DARE 格式结合了经过身份验证的密码方案和非常简单的重新排序保护机制,以实现防篡改特性。但是,一旦加密密钥被重复使用,这种防篡改特性就会丢失。因此,DARE 需要一个唯一的加密密钥。在 DARE 规范中,我们提供了一些关于如何正确执行此操作的 建议

Minio 作为对象存储,处理大量数据。因此,DARE 旨在处理大型数据流,同时保持较低的开销。每个加密数据包都包含一个标头和一个身份验证标签,这会使加密数据流的大小增加约 0.05%。举例说明:5 GB 数据流的开销约为 2.5 MB。此外,DARE 支持高达 256 TB 的大型数据流,这应该足以满足任何当前用例。但是,如果将来必须加密更大的流,则可以通过在更新的版本中进行不同的权衡来调整 DARE 以支持更大的流。

Secure-IO

Minio 还提供了 DARE 格式的 Go 参考 实现。它导出一个易于使用的 API,用于安全地加密/解密任意数据流。以下示例演示了如何使用 32 字节的主密钥加密文件。

masterkey := []byte("my-secret-32-byte-master-enc-key")
// this nonce must be unique (should generated randomly)
// It must be remembered but needn't be secret.
nonce := []byte{ 
    0, 1, 2, 3, 4, 5 ,6 ,7, 8, 9, 10, 11, 12, 13, 14, 15,
}
plaintext, err := os.Open("my-unencrypted-file")
if err != nil {
   fmt.Printf("Failed open 'my-unencrypted-file': %v", err)
   return
}
defer plaintext.Close()
ciphertext, err := os.Create("my-encrypted-file")
if err != nil {
   fmt.Printf("Failed create 'my-encrypted-file': %v", err)
   return
}
defer ciphertext.Close()
// derive an unique encryption key
kdf := hmac.New(sha256.New, masterkey)
kdf.Write(nonce)
config := sio.Config {
    Key: kdf.Sum(nil), 
    CipherSuites: []byte{sio.AES_256_GCM},
}
_, err = sio.Encrypt(ciphertext, plaintext, config)
if err != nil {
   fmt.Printf("Failed to encrypt data: %v", err)
   return
} 

可以在 godoc.org 上找到其他示例和包文档。此外,如果您想使用 DARE 格式并在您的机器上加密/解密数据流,可以查看此 演示工具

结论

Minio 将使用 DARE 进行服务器端和客户端加密。这将使我们的用户能够使用客户端加密加密他们的数据,并使用服务器端加密或反之解密数据。我们希望 DARE 不仅对我们的用户有用,而且对更广泛的开发人员社区也有用。如果您有任何疑问或建议,请加入我们的 Slack 频道。我们欢迎您的反馈!