在 Golang 中使用 AES 加密和解密大型数据流
加密是当今互联网安全的关键组成部分。Go 编程语言的“包含电池”理念意味着加密和解密数据所需的许多基元都包含在该语言中。然而,文档可能简洁且难以理解,密码学本身就是一个复杂而困难的主题。因此,要理解如何使用这些基元来实现更高级别的目标(例如加密文件)并不容易。
如果你在互联网上搜索过在 Golang 中使用 AES 加密,你无疑会发现有很多很多错误使用 AES 加密的例子,或者用于非常特殊的极端情况,比如加密与 AES 块大小完全相等的纯文本,或者通过一次将整个文件读取到内存中来一举加密文件。显然,对于大多数应用程序来说,这些用例都不实用。(大多数加密整个文件的演示都包含一个明显的错误,我将在下面描述。
如果你想做一些理智的事情,比如加密比单个AES块更长的明文,而不先将整个文件读入内存,那么可以提供帮助的资源少得惊人。
这些要求对于在内存受限的系统(如嵌入式平台或云功能即服务资源)中编写代码的开发人员尤其重要。就我而言,我在 AWS Lambda 上运行代码,并且需要加密数 GB 的文件,而我的代码在只有 128MB RAM 的 VM 中运行。显然,在这种情况下,不可能将整个文件读取到内存中并在单个操作中对其进行加密。
我发现,就像你一样,这个用例没有太多的指导。AES 完全能够一次加密一个块的明文,而无需将整个明文或密文保存在内存中,但是没有手持文章演示如何在 Go 中做到这一点。所以我决定写一个。
在本文中,我将向您展示如何使用 AES 在可管理的块中加密大型数据流,就像您在 for 循环中所做的那样。此外,我们将使用 Go 的标准 io 来做到这一点。Reader 和 io.编写器接口,这将使这些组件易于集成到更大的数据管道中。
让我们从(只是一点点)理论开始,因为当我们开始编码时,背景知识是有帮助的。
高级加密标准
AES(最初名称为“Rijndael”)是一种块加密标准,于 2001 年被 NIST 采用以取代 DES 标准。AES 是一种分组密码,这意味着它只能对块中的数据进行加密或解密。AES 块为 16 字节。如果加密的明文的长度不是 AES 块大小的整数倍,则必须在明文末尾添加填充以达到偶数块大小倍数。(如果你一直在阅读关于AES的“教程”,而作者没有解决这个问题,那么作者可能不知道他或她在说什么!
AES 密钥的长度可以是 128、192 或 256 位(16、24 或 32 字节)。请注意,无论密钥有多长,块大小始终为 16 字节。
AES 是一种对称密码,这意味着使用相同的密钥进行加密和解密。
尽管年代久远,但 AES 已被证明非常安全。迄今为止,还没有已知的实际攻击允许攻击者读取使用 AES 加密的数据,前提是实施得当。
AES Modes AES 模式
AES 本身只能将 16 字节的明文块加密为 16 字节的密文(反之亦然)。AES 的实际应用要求在更大的上下文中使用 AES,称为“模式”,以便为密码系统提供附加功能。
AES 可以在以下几种模式下运行,最流行的是:
- (电子代码簿)
- (密码块链接)
- (密码反馈)
- (输出反馈)
- (计数器)
- (伽罗瓦/计数器)
在 ECB 模式下,明文的每个块都使用 AES 密钥独立加密。因此,重复的明文块将导致重复的密文块。这导致了明显的弱点,使欧洲央行几乎不适合任何实际应用。除非你真的、真的知道你在做什么以及为什么(在这种情况下,你可能没有阅读这篇文章),否则不要使用欧洲央行。
在 CBC 模式下,每个明文块都使用 AES 密钥和前一个块的输出的组合进行加密。由于前一个块实际上是一个随机的字节集,因此 CBC 可确保明文中的重复数据不会导致重复的密文块。每个密文块不仅取决于明文和密钥,还取决于前面的密文块。
CBC 不支持身份验证。无法确定密文是否在加密和解密之间被更改。如果攻击者可以获取密文、更改密文并要求您解密密文,则可以使用各种类型的攻击来恢复明文。如果攻击者可能有权访问加密数据(例如,您正在加密浏览器 Cookie),则 CBC 可能不是适合您的加密模式。事实上,各种权威机构都开始弃用 AES-CBC,转而使用支持身份验证的模式。也许我会做一些研究,并在以后写一篇关于这些模式之一的文章。
其他操作模式适用于某些用例,但 CBC 是 AES 最常用的模式,适用于加密文件,这是我怀疑大多数读者都感兴趣的用例。(但是,请参阅本文末尾的免责声明,了解重要注意事项。
AES 密钥和初始化向量
AES 密钥是 16、24 或 32 字节的字符串,用于加密和解密数据。密钥是执行加密的进程和执行解密的进程之间的共享密钥。“共享密钥”必须同时可用于加密过程和解密过程,但不能用于其他任何人。加密数据的安全性取决于潜在攻击者无法使用的密钥。密钥管理(将密钥分发给应该拥有密钥的各方,同时防止其他各方访问它们)是任何密码系统的核心组成部分。密钥管理可能是一个非常困难的话题,我将在本文中完全忽略这个话题,因为它与 AES 加密/解密的机制无关,但作为使用这些技术的开发人员,密钥管理绝对必须成为您更大解决方案的一部分。
CBC 模式下的每个数据块可以描述为:
Ct = f(Pt, K, C(t-1))
Ct = f(Pt, K, C(t-1))
也就是说,密文块“t”是明文块“t”、AES 密钥和前一个块的密文的函数。
这给我们带来了一个先有鸡还是先有蛋的问题:如果加密函数的输入之一需要前一个块的输出,我们如何加密第一个数据块?
CBC 通过使用“初始化向量”(IV)来解决这个问题。IV 是一个 16 字节的字符串,用于为加密过程的初始状态设定种子。IV 需要从 AES-CBC 加密密文中恢复第一个明文块。
请注意,AES 本身需要密钥,但 IV 仅与 CBC 模式相关。使用 AES 的其他模式不需要 IV。
AES 密钥和 IV 都应由加密安全的随机数生成器生成。每个加密数据流的 AES 密钥和 IV 都应该是唯一的。IV 不应从可预测的过程(例如计数器)生成。在多个加密会话中重复使用 AES 密钥或 IV(或使用可预测的密钥/IV)使攻击者能够使用重复使用的密钥或 IV 获取大量密文语料库,从而更实际地恢复明文。如果攻击者可能能够任意更改您的密文并要求您解密它,那么您需要考虑一些严重的事情,这些事情远远超出了本文的范围,但您的密码系统可能比您想象的要弱得多。(如果您想深入那个兔子洞,请参阅此 Stack Overflow 问题的第一个答案。
根据密码学理论,密钥必须在解密方和加密方之间保持机密,但 IV 不是机密,可以在不损失安全性的情况下暴露给潜在的攻击者。由于 IV 必须可用于解密过程,因此将未加密的 IV 作为加密数据流的一部分包含在内的情况并不少见。或者,由于 IV 仅与解密加密数据的初始块相关,因此一些 AES-CBC 解决方案将 16 字节的随机数预置到密文中,并使用第二个 AES 块启动“真实”数据流。
使用 AES 逐块加密数据
有了这个理论,让我们构建一些实用的东西!
在 Go 中,密码模式与底层密码本身分开管理。BlockMode 类型管理从上一个块传播信息以加密下一个块所需的状态。当下一个区块被加密/解密时,BlockMode 将执行必要的操作,调用密码对象进行加密/解密,并保留加密/解密下一个区块所需的信息。在实践中,BlockMode 是密码之上的抽象。我们的代码必须创建密码对象,但只能通过 BlockMode 对象与它交互。(是的,这确实表明可以为其他 AES 操作模式开发其他块模式类型。
Go 在 cipher.go 包中提供了 BlockMode 接口。此接口提供单个方法 CryptBlocks() 来加密或解密多个块。CBCEncrypter 类型(实现 BlockMode 接口)在调用 CryptBlocks() 之间维护内部状态,因此前一个块的输出可以用作下一个块加密的输入之一。因此,我们需要创建一个 AES 密码,但我们将通过 CBC 结构的 BlockMode 接口与它进行交互。
让我们从定义 AESCBCEncryptor 类型开始:
package aescbc
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
)
type AESCBCEncryptor struct {
IV []byte
AESKey []byte
inputoverflow []byte
outputoverflow []byte
cipher cipher.Block
cbc cipher.BlockMode
isClosed bool
}
该结构已导出字段以保存 AES 密钥和 IV。在内部,它管理字节片以存储客户端尚未读取的加密数据以及长度小于完整 AES 块大小的未加密数据。它还维护对 AES 密码和 CBC 块模式的引用,并跟踪客户端是否写入了最后一位数据。
我们首先构建一个函数来创建结构体的新实例并初始化其成员:
func NewAESCBCEncryptor() (*AESCBCEncryptor, error) {
var err error
var e AESCBCEncryptor
//generate a random AES key
e.AESKey = make([]byte, 32)
_, err = rand.Read(e.AESKey)
if err != nil {
return nil, err
}
//generate a random initialization vector
e.IV = make([]byte, 16)
_, err = rand.Read(e.IV)
if err != nil {
return nil, err
}
//input overflow is used when Write() gives us a partial
//AES block. The remaining bytes, to be used in the next
//block, are stored here
//input overflow should never exceed the AES block size
e.inputoverflow = make([]byte, 0, 16)
//The output overflow holds encrypted data that hasn't yet been
//read by Read().
e.outputoverflow = make([]byte, 0)
//Generate an AES cipher in CBC mode
e.cipher, err = aes.NewCipher(e.AESKey)
if err != nil {
return nil, err
}
e.cbc = cipher.NewCBCEncrypter(e.cipher, e.IV)
e.isClosed = false
return &e, nil
}
请注意,使用 crypto/rand 创建 AES 密钥和 IV.
为了加密数据,我们的客户会将明文写入我们的加密器。io.编写器接口是 Go 中执行此操作的标准方法,支持此接口将使我们的组件与许多现有模式兼容。该接口接受一个字节数组,并返回接受的字节数和发生的任何错误。
虽然 AES 坚持输入大小是 16 的整数倍,但我们不能在客户端上强制执行。因此,我们必须在内部截断 16 字节边界处的传入数据,并将多余的数据保存为“溢出”。当然,在执行此操作之前,我们需要将任何现有的溢出预置到传入数据中:
func (e *AESCBCEncryptor) Write(p []byte) (n int, err error) {
//If the writer is closed, error out
if e.isClosed {
return 0, errors.New("writer has been closed")
}
//If there's any existing input overflow, that must be
//prepended to the incoming data
if len(e.inputoverflow) > 0 {
p = append(e.inputoverflow, p...)
}
//We can only encrypt multiples of the block size. If
//the input is not a multiple of the block size, save the
//extra in the inputoverflow
if len(p)%e.cbc.BlockSize() != 0 {
//Take advantage of integer division here to calculate sizes
numFullBlocks := len(p) / e.cbc.BlockSize()
sizeFullBlocks := numFullBlocks * e.cbc.BlockSize()
extraBytes := len(p) - sizeFullBlocks
e.inputoverflow = make([]byte, extraBytes)
e.inputoverflow = p[len(p)-extraBytes:]
p = p[:sizeFullBlocks]
} else {
if len(e.inputoverflow) > 0 {
e.inputoverflow = make([]byte, 0)
}
}
//Encrypt the plaintext to a ciphertext slice
cipherText := make([]byte, len(p))
e.cbc.CryptBlocks(cipherText, p)
//append the new ciphertext to the output waiting to be read
e.outputoverflow = append(e.outputoverflow, cipherText...)
return len(p), nil
}
请注意,实际执行加密是一行代码;这里的其余代码是内务管理。outputoverflow 字节片包含供客户端读取的加密数据。
上述代码中对 isClosed 的检查表明加密器可能处于关闭状态。原因如下:回想一下,AES 只能加密 16 字节的数据块。如果输入数据不是 16 个字节的偶数个字节(这很有可能),那么如何加密最后一个明文块?
答案是,最后一个块必须填充其他数据才能到达 16 字节的边界。PKCS7 填充是一种标准方法:如果需要一个额外的字节,则将单个0x01字节追加到部分块。如果需要两个字节,则附加0x02 0x02,依此类推。这样,可以很容易地确定需要从解密的明文中剥离的填充字节数。出于同样的原因,如果明文长度恰好是 16 个字节的偶数倍,则会添加额外的完整填充块。
因此,当客户端完成向加密器写入纯文本时,应调用 Close() 方法来添加所需的填充:
func (e *AESCBCEncryptor) Close() {
//We need to pad the last block and encrypt it
p := Pkcs7Pad(e.inputoverflow, e.cbc.BlockSize())
e.outputoverflow = make([]byte, len(p))
e.cbc.CryptBlocks(e.outputoverflow, p)
e.isClosed = true
}
Pkcs7Pad 函数不是标准 Go 库的一部分;我在 https://wgallagher86.medium.com/pkcs-7-padding-in-go-6da5d1d14590 找到了 William Gallagher 的实现,我已经成功地将其与我的代码集成在一起。有关更多详细信息,请参阅他的文章。
在任何情况下,调用 Close() 都会向最后一个明文块添加填充,加密最后一个块,并设置 isClosed 标志,以便将来对 Write() 的任何调用都会引发错误。
这就是您在 CBC 模式下使用 AES 将明文转换为密文的方式。
获取加密数据
如果 Write() 方法允许客户端将明文写入加密器,则只有使用 Read() 才能将密文输出。read 方法很简单,几乎不需要注释:
func (e *AESCBCEncryptor) Read(p []byte) (n int, err error) {
if e.isClosed && len(e.outputoverflow) == 0 {
return 0, io.EOF
}
if len(p) >= len(e.outputoverflow) {
//We can send all of our data to the caller
n = copy(p, e.outputoverflow)
e.outputoverflow = make([]byte, 0)
return n, nil
} else {
//We can only return some of our waiting data
n = copy(p, e.outputoverflow)
e.outputoverflow = e.outputoverflow[n:]
return n, nil
}
}
这里唯一的特殊之处在于,如果调用方的读取缓冲区不够大,无法容纳所有密文,我们将返回尽可能多的数据,并通过调整 outputoverflow 字节片的大小来保留其余数据。
将密文解密为明文
AESCBCDecryptor 实际上与加密器完全相同。在创建结构时必须提供这些密钥和 IV,而不是生成密钥和 IV。解密器必须从 Read() 中“保留”至少 16 个字节的数据,直到调用 Close(),以便它可以在将 PKCS7 填充传递到客户端之前从明文中剥离它。除此之外,它是加密器的虚拟复制和粘贴:
package encryptionhelpers
import (
"crypto/aes"
"crypto/cipher"
"errors"
"io"
)
type AESCBCDecryptor struct {
iv []byte
aesKey []byte
inputoverflow []byte
outputoverflow []byte
cipher cipher.Block
cbc cipher.BlockMode
}
func NewAESCBCDecryptor(aesKey []byte, iv []byte) (*AESCBCDecryptor, error) {
var err error
var e AESCBCDecryptor
if len(aesKey) != 32 {
return nil, errors.New("aes key must be 32 bytes long")
}
e.aesKey = aesKey
if len(iv) != 16 {
return nil, errors.New("IV must be 16 bytes long")
}
e.iv = iv
//input overflow is used when Write() gives us a partial
//AES block. The remaining bytes, to be used in the next
//block, are stored here
//input overflow should never exceed the AES block size
e.inputoverflow = make([]byte, 0)
//Generate an AES cipher in CBC mode
e.cipher, err = aes.NewCipher(e.aesKey)
if err != nil {
return nil, err
}
//Contrary to the documentation, the IV for decryption does not need
//to match the IV for encryption.
e.cbc = cipher.NewCBCDecrypter(e.cipher, e.iv)
e.outputoverflow = make([]byte, 0)
return &e, nil
}
func (e *AESCBCDecryptor) BytesAvailable() int {
return len(e.outputoverflow)
}
func (e *AESCBCDecryptor) Write(p []byte) (n int, err error) {
//If there's any existing input overflow, that must be
//prepended to the incoming data
if len(e.inputoverflow) > 0 {
p = append(e.inputoverflow, p...)
}
//We can only encrypt multiples of the block size. If
//the input is not a multiple of the block size, save the
//extra in the inputoverflow
if len(p)%e.cbc.BlockSize() != 0 {
//Take advantage of integer division here to calculate sizes
numFullBlocks := len(p) / e.cbc.BlockSize()
sizeFullBlocks := numFullBlocks * e.cbc.BlockSize()
extraBytes := len(p) - sizeFullBlocks
e.inputoverflow = make([]byte, extraBytes)
e.inputoverflow = p[len(p)-extraBytes:]
p = p[:sizeFullBlocks]
} else {
if len(e.inputoverflow) > 0 {
e.inputoverflow = make([]byte, 0)
}
}
plaintext := make([]byte, len(p))
e.cbc.CryptBlocks(plaintext, p)
if err != nil {
return 0, err
}
e.outputoverflow = append(e.outputoverflow, plaintext...)
return len(p), nil
}
func (e *AESCBCDecryptor) Read(p []byte) (n int, err error) {
if len(e.outputoverflow) == 0 {
return 0, io.EOF
}
if len(p) >= len(e.outputoverflow) {
//We can send all of our data to the caller
n = copy(p, e.outputoverflow)
e.outputoverflow = make([]byte, 0)
return n, nil
} else {
//We can only return some of our waiting data
n = copy(p, e.outputoverflow)
e.outputoverflow = e.outputoverflow[n:]
return n, nil
}
}
一个简单的例子
Go Playground 上提供了加密和解密数据的简单示例,网址为 https://go.dev/play/p/Pr_Xdn8bmei:
//A working example of aecsbc
package main
import (
"fmt"
"github.com/MatthewMucker/aescbc"
)
func main() {
plaintext := "This is a string of plaintext. It is longer than a single AES block and can be of any arbitrary length."
fmt.Printf("The original plaintext is: %s\n", plaintext)
//Create an encryptor
encryptor, _ := aescbc.NewAESCBCEncryptor()
fmt.Printf("The AES encryption key is: %x\n", encryptor.AESKey)
fmt.Printf("The initializion vector used is: %x\n", encryptor.IV)
//Write the plaintext to the encryptor
encryptor.Write([]byte(plaintext))
encryptor.Close()
//Read the ciphertext from the encryptor, allocating enough space for padding in the last AES block
ciphertext := make([]byte, len(plaintext)+16)
n, _ := encryptor.Read(ciphertext)
fmt.Printf("The ciphertext is: %x\n", ciphertext[:n])
//Now let's decrypt it!
decryptor, _ := aescbc.NewAESCBCDecryptor(encryptor.AESKey, encryptor.IV)
decryptor.Write(ciphertext[:n])
decryptor.Close()
recoveredPlaintext := make([]byte, len(plaintext))
decryptor.Read(recoveredPlaintext)
fmt.Printf("The recovered plaintext is: %s\n", string(recoveredPlaintext))
}
添加 Copy() 方法
虽然上面的代码适合作为几乎任何用例的指南,但这些类型的典型用例是加密和解密文件。由于 Go 为我们提供了 io.读者,io。Writer 和 Copy() 模式,我们可以使用它们来封装此功能。
Copy() 函数中有一些额外的内务处理;该函数本质上是两个管道:一个从源文件到加密器,第二个从加密器到目标文件,在我们从源文件获取 EOF 并在加密器上调用 Close() 之前,我们无法读取密文的最后一段。我不确定它是否是最优雅的实现,但相当多的测试使我确信这是可行的:
func (e *AESCBCEncryptor) Copy(dst io.Writer, src io.Reader) (written int64, err error) {
shouldClose := false
written = int64(0)
inputBuf := make([]byte, e.CopyBufferSize)
//Write the IV to the output stream
dst.Write(e.IV)
written += int64(len(e.IV))
//Read until we get an EOF
for {
//Read from the source file
n, err := src.Read(inputBuf)
if err == io.EOF {
shouldClose = true
} else {
if err != nil {
return written, err
}
}
//Write plaintext to the encryptor
e.Write(inputBuf[:n])
//Read ciphertext from the encryptor
n, _ = e.Read(inputBuf)
//Write ciphertext to the destination file
n, err = dst.Write(inputBuf[:n])
written += int64(n)
if err != nil {
return written, err
}
if shouldClose {
e.Close()
//Read any remaining ciphertext
for {
n, _ = e.Read(inputBuf)
if n == 0 {
break
}
n, err = dst.Write(inputBuf[:n])
if err != nil {
return written, err
}
written += int64(n)
}
break
}
}
return written, nil
}
Code Availability and Quality
代码可用性和质量
完整的代码可在我的 github 上找到,网址为 https://github.com/MatthewMucker/aescbc,可以通过以下方式导入到您的代码中:
import "github.com/MatthewMucker/aescbc"
我对 Go 编程相当陌生,并且不相信我已经学会了该语言的细微差别和最佳实践。上面的代码比我的 github 存储库中更完整的代码略有简化。存储库中的代码会做额外的工作来最大限度地减少分配,这是朝着更高的代码质量迈出的一步,但我欢迎任何能够提高代码质量的人提出拉取请求。
- 点赞
- 收藏
- 关注作者
评论(0)