英文:
Golang (Go) AES CBC ciphertext gets padded with 16 0x00 bytes for some reason
问题
我正在测试Golang(Go)中的AES 256 CBC实现。
明文:{"key1": "value1", "key2": "value2"}
由于明文长度为36字节,需要是块大小(16字节)的倍数,因此我手动使用12个随机字节将其填充到48字节。
我知道这不是最安全的方法,但我只是在进行测试,我会找到更好的方法用于生产环境。
输入:
明文:aaaaaaaaaaaa{"key1": "value1", "key2": "value2";}
AES 256密钥:b8ae2fe8669c0401fb289e6ab6247924
AES IV:e0332fc2a9743e4f
以下是从这里提取的代码片段,稍作修改:
block, err := aes.NewCipher(key)
if err != nil {
fmt.Println("使用您的密钥创建新的AES密码时出错!")
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))
密文 = 明文 + 块大小 - (明文 MOD 块大小)
这个方程给出了CBC模式下密文的长度。
因此,行ciphertext := make([]byte, aes.BlockSize+len(plaintext))
满足了这个要求,因为我的明文总是被填充为块大小的倍数。
问题:
使用Go时,我得到以下密文:
caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c7400000000000000000000000000000000
无论我的明文长度如何,我始终得到16个0x00字节作为密文的结尾。
如果我在在线AES计算器上进行相同的操作,我得到以下密文:
caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c74ccd202bac41937be75731f23796f1516
前48个字节caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c74
是相同的。但我缺少最后的16个字节。
这里说:
> 可以传递一个比src更大的dst,这种情况下,CryptBlocks只会更新dst[:len(src)],不会触及dst的其余部分。
但为什么会这样?密文的长度需要比明文的长度长,在线AES计算器证明了这一点。
英文:
I am testing out the AES 256 CBC implementation in Golang (Go).
plaintext: {"key1": "value1", "key2": "value2"}
Because the plaintext is 36 B and needs to be a multiple of the block size (16 B) I pad it manually with 12 random bytes to 48 B.
I understand that this is not the most secure way of doing it, but I am just testing, I will find a better way for production setups.
Inputs:
plaintext: aaaaaaaaaaaa{"key1": "value1", "key2": "value2"}
AES 256 key: b8ae2fe8669c0401fb289e6ab6247924
AES IV: e0332fc2a9743e4f
The code excerpt extracted, but modified a bit, from 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)
This equation gives the length of the ciphertext for CBC.
So, the line ciphertext := make([]byte, aes.BlockSize+len(plaintext))
satisfies this requirement since my plaintext is always padded to be a multiple of the block size.
Problem:
With Go I get the following ciphertext:
caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c7400000000000000000000000000000000
I always get 16 0x00 bytes at the end of my ciphertext, no matter the length of my plaintext.
If i do the same with an online AES calculator I get this ciphertext:
caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c74ccd202bac41937be75731f23796f1516
The first 48 bytes caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c74
are the same. But I am missing the last 16 bytes.
This says:
> 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.
But why is this the case ? The length of the ciphertext needs to be longer than the length of the plaintext and the online AES calculators prove that.
答案1
得分: 1
在线工具结果的密文,如果明文是:
aaaaaaaaaaaa{"key1": "value1", "key2": "value2"}
使用PKCS#7填充,并且发布的密钥和IV是UTF8编码。由于明文的大小(48字节)已经是块大小(AES的块大小为16字节)的整数倍,根据PKCS#7填充规则,一个完整的块将被填充,结果是64字节的明文和密文。
从问题中并不清楚使用了哪个在线工具,但是可以使用任何可靠的加密工具(例如CyberChef)重构发布的密文,参见此在线计算。CyberChef默认使用AES/CBC进行PKCS#7填充。
发布的代码生成了不同的密文,原因如下:
- 没有应用PKCS#7填充。这使得密文缺少一个块(即最后一个块ccd202bac41937be75731f23796f1516)。
- 为密文分配了
aes.BlockSize + len(plaintext)
字节的大小。这导致分配的大小比实际需要的大aes.BlockSize
字节(即密文末尾包含16个0x00值)。
因此,为了使Go代码生成与在线工具相同的密文,需要进行以下操作:1. 添加PKCS#7填充;2. 为密文分配len(plaintext)
字节的大小。
以下代码是可能的实现(使用PKCS#7填充pkcs7pad):
import (
...
"github.com/zenazn/pkcs7pad"
)
...
key := []byte("b8ae2fe8669c0401fb289e6ab6247924")
iv := []byte("e0332fc2a9743e4f")
plaintext := []byte("aaaaaaaaaaaa{\"key1\": \"value1\", \"key2\": \"value2\"}")
plaintext = pkcs7pad.Pad(plaintext, aes.BlockSize) // 1. 使用PKCS#7填充明文
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
ciphertext := make([]byte, len(plaintext)) // 2. 分配len(plaintext)大小的密文
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, plaintext)
fmt.Printf("%x\n", ciphertext) // caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c74ccd202bac41937be75731f23796f1516
注意,由于PKCS#7填充,不再需要显式填充a
。
上述代码中使用的静态IV是一个漏洞,因为它导致了密钥/IV对的重用,这是不安全的。实际上,通常为每个加密操作生成一个随机IV。IV不是秘密的,需要用于解密,并且通常与密文连接在一起。在解密端,IV和密文被分开并用于解密。
由于IV的大小与块大小相对应,因此必须为密文分配aes.BlockSize + len(plaintext)
的大小,这与原始代码中的大小相同。可能这不是偶然的,而是考虑到了随机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) // 创建一个随机IV
if err != nil {
panic(err)
}
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
fmt.Printf("%x\n", ciphertext)
输出的前16个字节对应于(随机)IV,其余部分对应于实际的密文。
英文:
The ciphertext of the online tool results, if the plaintext:
aaaaaaaaaaaa{"key1": "value1", "key2": "value2"}
is padded with PKCS#7 and the posted key and IV are UTF8 encoded. Since the size of the plaintext (48 bytes) is already an integer multiple of the blocksize (16 bytes for AES), a full block is padded according to the rules of PKCS#7 padding, resulting in a 64 bytes plaintext and ciphertext.
It is not clear from the question which online tool was used, but the posted ciphertext can be reconstructed with any reliable encryption tool, e.g. CyberChef, s. this online calcualtion. CyberChef applies PKCS#7 padding for AES/CBC by default.
The posted code produces a different ciphertext because:
- no PKCS#7 padding is applied. This makes the ciphertext one block shorter (i.e. the last block ccd202bac41937be75731f23796f1516 is missing).
- a size of
aes.BlockSize + len(plaintext)
bytes is allocated for the ciphertext. This causes the allocated size to be too large byaes.BlockSize
bytes (i.e. the ciphertext contains 16 0x00 values at the end).
Therefore, for the Go code to produce the same ciphertext as the online tool, 1. the PKCS#7 padding must be added and 2. a size of only len(plaintext)
bytes must be allocated for the ciphertext.
The following code is a possible implementation (for PKCS#7 padding pkcs7pad is used):
import (
...
"github.com/zenazn/pkcs7pad"
)
...
key := []byte("b8ae2fe8669c0401fb289e6ab6247924")
iv := []byte("e0332fc2a9743e4f")
plaintext := []byte("aaaaaaaaaaaa{\"key1\": \"value1\", \"key2\": \"value2\"}")
plaintext = pkcs7pad.Pad(plaintext, aes.BlockSize) // 1. pad the plaintext with PKCS#7
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
ciphertext := make([]byte, len(plaintext)) // 2. allocate len(plaintext)
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, plaintext)
fmt.Printf("%x\n", ciphertext) // caf8fe667f4087e1b67d8c9c57fcb1f56b368cafb4bfecbda1e481661ab7b93d87703fb140368d3034d5187c53861c74ccd202bac41937be75731f23796f1516
Note that because of the PKCS#7 padding, explicit padding with a
is no longer required.
The static IV used in the above code is a vulnerability as it leads to reuse of key/IV pairs, which is insecure. In practice, therefore, a random IV is usually generated for each encryption. The IV is not secret, is needed for decryption, and is typically concatenated with the ciphertext. On the decryption side, IV and ciphertext are separated and used for decryption.<br>
Since the size of the IV corresponds to the blocksize, a size of aes.BlockSize + len(plaintext)
must be allocated for the ciphertext, which is equal to the size in the original code. Possibly this is not accidental and was designed with a random IV in mind, but then not implemented consequently. A consequent implementation is:
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) // create a random IV
if err != nil {
panic(err)
}
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
fmt.Printf("%x\n", ciphertext)
The first 16 bytes of the output correspond to the (random) IV and the rest to the actual ciphertext.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论