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.
CodePudding user response:
Since the AES/CBC implementation used in the Go code does not implicitly pad, the code only works if the size criterion is satisfied, i.e. if the plaintext size is an integer multiple of the blocksize (16 bytes for AES). The latter is satisfied for the plaintext example here by explicit padding with a
. Under these conditions, the ciphertext size is equal to the plaintext size, i.e. len(plaintext)
.
Because in the Go code the size of ciphertext
is allocated with aes.BlockSize len(plaintext)
, where aes.BlockSize
is the AES blocksize (16 bytes), ciphertext
is 16 bytes larger than the actual ciphertext, which is the cause of the last 16 0x00 values. To remove these, the size of ciphertext
simply needs to be allocated with len(plaintext)
.
Furthermore, since plaintexts generally do not meet the size criterion, padding should be added. A reliable padding is PKCS#7, which is implemented in Go as e.g. pkcs7pad.
Since the online tool uses PKCS#7 padding, the following Go code provides the result of the online tool:
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 padding adds a complete block if the size criterion is already met, as in this example (because of the explicit padding with the a
), which is the reason for the longer ciphertext compared to the ciphertext without PKCS#7 padding.
Note that because of the PKCS#7 padding, explicit padding with a
is of course no longer required.
Since the reuse of key/IV pairs is insecure, a static IV (as in the code) is also insecure. Therefore, to avoid this, a random IV is usually generated for each encryption. The IV is not secret, is needed for decryption, and is thus typically concatenated with the ciphertext (IV|ciphertext).
For this, the size of ciphertext
must be defined with aes.BlockSize len(plaintext)
. Therefore, the original size definition can be kept if a randomly generated IV is to be used instead of the static 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)
In this implementation, the first 16 bytes correspond to the IV and the rest to the actual ciphertext. During decryption both parts have to be separated.