Golang (Go) AES CBC 密文由于某种原因被填充了 16 个 0x00 字节

Golang (Go) AES CBC ciphertext gets padded with 16 0x00 bytes for some reason

我正在 Golang (Go) 中测试 AES 256 CBC 实现。

plaintext: {"key1": "value1", "key2": "value2"}

因为明文是 36 B 并且需要是块大小 (16 B) 的倍数,所以我用 12 个随机字节手动填充到 48 B。 我知道这不是最安全的方法,但我只是在测试,我会找到更好的生产设置方法。

输入:

plaintext: aaaaaaaaaaaa{"key1": "value1", "key2": "value2"}
AES 256 key: b8ae2fe8669c0401fb289e6ab6247924
AES IV: e0332fc2a9743e4f

here:

中提取的代码摘录,但稍作修改
block, err := aes.NewCipher(key)
if err != nil {
    fmt.Println("Error creating a new AES cipher by using your key!");
    fmt.Println(err);
    os.Exit(1);
}

ciphertext := make([]byte, aes.BlockSize+len(plaintext))

mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, plaintext)

fmt.Printf("%x\n", ciphertext)
fmt.Println("len(ciphertext):",len(ciphertext))

CipherText = PlainText + Block - (PlainText MOD Block)

这个等式给出了 CBC 的密文长度。

因此,ciphertext := make([]byte, aes.BlockSize+len(plaintext)) 行满足了这个要求,因为我的明文总是被填充为块大小的倍数。

问题:

使用 Go 我得到以下密文: caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c7400000000000000000000000000000000

无论明文长度如何,我总是在密文末尾得到 16 个 0x00 字节。

如果我用在线 AES 计算器做同样的事情,我会得到这个密文: caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c74ccd202bac41937be75731f23796f1516

前 48 个字节 caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c74 相同。但我遗漏了最后 16 个字节。

This 说:

It is acceptable to pass a dst bigger than src, and in that case, CryptBlocks will only update dst[:len(src)] and will not touch the rest of dst.

但为什么会这样呢?密文的长度需要比明文的长度长,在线AES计算器证明。

由于 Go 代码中使用的 AES/CBC 实现不会隐式填充,代码仅在满足大小标准时才有效,即如果明文大小是块大小的整数倍(16 字节AES)。后者通过 explicit 填充 a 满足明文示例。在这些条件下,密文大小等于明文大小,即len(plaintext).

因为在Go代码中ciphertext的大小是用aes.BlockSize+len(plaintext)分配的,其中aes.BlockSize是AES块大小(16字节),ciphertext是16字节大于实际密文,这是最后 16 个 0x00 值的原因。要删除这些,ciphertext 的大小只需要分配 len(plaintext).

此外,由于明文一般不符合大小标准,因此需要添加padding。可靠的填充是 PKCS#7,它在 Go 中实现为例如pkcs7pad.

由于在线工具使用PKCS#7 padding,以下Go代码提供在线工具的结果:

import (
    ...
    "github.com/zenazn/pkcs7pad"
)
...
key := []byte("b8ae2fe8669c0401fb289e6ab6247924")
iv := []byte("e0332fc2a9743e4f")
plaintext := []byte("aaaaaaaaaaaa{\"key1\": \"value1\", \"key2\": \"value2\"}")
plaintext = pkcs7pad.Pad(plaintext, aes.BlockSize)
block, err := aes.NewCipher(key)
if err != nil {
    panic(err)
}
ciphertext := make([]byte, len(plaintext))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, plaintext)
fmt.Printf("%x\n", ciphertext)

PKCS#7 如果已经满足大小标准,则填充会添加一个完整的块,如本例所示(因为使用 a 进行了显式填充),这就是密文较长的原因没有 PKCS#7 填充的密文。

请注意,由于 PKCS#7 填充,当然不再需要 a 的显式填充。


由于 key/IV 对的重用是不安全的,因此静态 IV(如代码中所示)也是不安全的。因此,为了避免这种情况,通常会为每次加密生成一个 random IV。 IV 不是秘密的,需要解密,因此通常与密文连接 (IV|ciphertext).
为此,必须用 aes.BlockSize+len(plaintext) 定义 ciphertext 的大小。因此,如果要使用随机生成的 IV 而不是静态 IV,则可以保留原始大小定义:

import (
    ...
    "crypto/rand"
    "io"
    "github.com/zenazn/pkcs7pad"
)
...
key := []byte("b8ae2fe8669c0401fb289e6ab6247924")
plaintext := []byte("{\"key1\": \"value1\", \"key2\": \"value2\"}")
plaintext = pkcs7pad.Pad(plaintext, aes.BlockSize)
block, err := aes.NewCipher(key)
if err != nil {
    panic(err)
}
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
_, err = io.ReadFull(rand.Reader, iv)
if err != nil {
    panic(err)
}
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
fmt.Printf("%x\n", ciphertext)

在此实现中,前 16 个字节对应于 IV,其余为实际密文。在解密过程中,两部分必须分开。