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.
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:
aes.BlockSize + len(plaintext)
bytes is allocated for the ciphertext. This causes the allocated size to be too large by aes.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.
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.