Search code examples
c#goencryptionaescbc-mode

go AES encryption CBC


Problem

I have to port a function from C# to GO, which is using AES encryption. obviously i have to get the same result with GO that i get with C#

C#

code fiddle

I prepared a small fiddle with C#

using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

public class Program
{
    public static void Main()
    {
        var query = "csharp -> golang";
        var key = Encoding.UTF8.GetBytes("12345678901234567890123456789012");
        var iv = Encoding.UTF8.GetBytes("1234567890123456");
        using (var aes = (RijndaelManaged)RijndaelManaged.Create())
        {
            aes.KeySize = 256;
            aes.Mode = CipherMode.CBC;
            aes.Key = key;
            aes.IV = iv;
            using (var transform = aes.CreateEncryptor())
            {
                Console.WriteLine("query => " + query);
                var toEncodeByte = Encoding.UTF8.GetBytes(query);
                Console.WriteLine("toEncodeByte = " + ToString(toEncodeByte));
                var encrypted = transform.TransformFinalBlock(toEncodeByte, 0, toEncodeByte.Length);
                Console.WriteLine("encrypted = " + ToString(encrypted));
            }
        }
    }

    public static string ToString(byte[] b)
    {
        return "[" + String.Join(" ", b.Select(h => h.ToString())) + "]";
    }
}

console output

query => csharp -> golang
toEncodeByte = [99 115 104 97 114 112 32 45 62 32 103 111 108 97 110 103]
encrypted = [110 150 8 224 44 118 15 182 248 172 105 14 61 212 219 205 216 31 76 112 179 76 214 154 227 112 159 176 24 61 108 100]

GO

code fiddle

i prepared a small fiddle wit GO

package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "encoding/hex"
    "fmt"
)

func main() {
    query := "csharp -> golang"
    key := []byte("12345678901234567890123456789012")
    iv := []byte("1234567890123456")

    if len(key) != 32 {
        fmt.Printf("key len must be 16. its: %v\n", len(key))
    }
    if len(iv) != 16 {
        fmt.Printf("IV len must be 16. its: %v\n", len(iv))
    }
    var encrypted string
    toEncodeByte := []byte(query)
    fmt.Println("query =>", query)
    fmt.Println("toEncodeByte = ", toEncodeByte)
    toEncodeBytePadded := PKCS5Padding(toEncodeByte, len(key))

    // aes
    block, err := aes.NewCipher(key)
    if err != nil {
        fmt.Println("CBC Enctryping failed.")
    }
    ciphertext := make([]byte, len(toEncodeBytePadded))
    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(ciphertext, toEncodeBytePadded)
    encrypted = hex.EncodeToString(ciphertext)
    // end of aes
    fmt.Println("encrypted", []byte(encrypted))
}

func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
    padding := (blockSize - len(ciphertext)%blockSize)
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(ciphertext, padtext...)
}

console output

query => csharp -> golang
toEncodeByte =  [99 115 104 97 114 112 32 45 62 32 103 111 108 97 110 103]
encrypted [54 101 57 54 48 56 101 48 50 99 55 54 48 102 98 54 102 56 97 99 54 57 48 101 51 100 100 52 100 98 99 100 100 56 49 102 52 99 55 48 98 51 52 99 100 54 57 97 101 51 55 48 57 102 98 48 49 56 51 100 54 99 54 52]

Summary

I noticed that in the C# the input does not have to be same size as block.
In GO this seems not to work without padding (therefore I added Padding in the GOcode) but the result is different.
An solution to gain an equal result would be great


Solution

  • First of all, the ciphertexts of both codes are identical. However, the ciphertext in the Golang code is converted incorrectly.

    In the C# code the content of the byte[] is printed in decimal format. To get an equivalent output in the Golang code, the content of the []byte must also be printed in decimal format, which is easily achieved with:

    fmt.Println("ciphertext ", ciphertext)
    

    and produces the following output:

    ciphertext  [110 150 8 224 44 118 15 182 248 172 105 14 61 212 219 205 216 31 76 112 179 76 214 154 227 112 159 176 24 61 108 100]
    

    and is identical to the output of the C# code.


    In the current code, the ciphertext is first encoded with:

    encrypted = hex.EncodeToString(ciphertext)
    

    into a hexadecimal string, which is easily verified with:

    fmt.Println("encrypted (hex)", encrypted)
    

    producing the following output:

    encrypted (hex) 6e9608e02c760fb6f8ac690e3dd4dbcdd81f4c70b34cd69ae3709fb0183d6c64
    

    When converting the hexadecimal string to a []byte with []byte(encrypted), a Utf8 encoding takes place which doubles the size of the data, as the output with:

    fmt.Println("encrypted", []byte(encrypted))
    

    in the current code shows:

    encrypted [54 101 57 54 48 56 101 48 50 99 55 54 48 102 98 54 102 56 97 99 54 57 48 101 51 100 100 52 100 98 99 100 100 56 49 102 52 99 55 48 98 51 52 99 100 54 57 97 101 51 55 48 57 102 98 48 49 56 51 100 54 99 54 52]
    

    CBC is a block cipher mode, i.e. the plaintext length must be an integer multiple of the block size (16 bytes for AES). If this is not the case, padding must be applied. The C# code implicitly uses PKCS7 padding. This is the reason why plaintexts that do not meet the length condition are also processed.

    In contrast, in the Golang code padding must be done explicitly, which is accomplished with the PKCS5Padding() method, which implements PKCS7 padding. The second argument of the PKCS5Padding() method is the block size, which for AES is 16 bytes. For this parameter actually aes.BlockSize should be passed. Currently, len(key) is passed here, which has a size of 32 bytes for AES-256. Although this is compatible with the C# code for the current plaintext length, it is not compatible for arbitrary plaintext lengths (e.g. 32 bytes).


    The following code contains the changes and outputs explained above:

    package main
    
    import (
        "bytes"
        "crypto/aes"
        "crypto/cipher"
        "encoding/hex"
        "fmt"
    )
    
    func main() {
        query := "csharp -> golang"
        key := []byte("12345678901234567890123456789012")
        iv := []byte("1234567890123456")
    
        if len(key) != 32 {
            fmt.Printf("key len must be 16. its: %v\n", len(key))
        }
        if len(iv) != 16 {
            fmt.Printf("IV len must be 16. its: %v\n", len(iv))
        }
        var encrypted string
        toEncodeByte := []byte(query)
        fmt.Println("query =>", query)
        fmt.Println("toEncodeByte = ", toEncodeByte)
        toEncodeBytePadded := PKCS5Padding(toEncodeByte, aes.BlockSize)
    
        // aes
        block, err := aes.NewCipher(key)
        if err != nil {
            fmt.Println("CBC Enctryping failed.")
        }
        ciphertext := make([]byte, len(toEncodeBytePadded))
        mode := cipher.NewCBCEncrypter(block, iv)
        mode.CryptBlocks(ciphertext, toEncodeBytePadded)
        encrypted = hex.EncodeToString(ciphertext)
        // end of aes
        fmt.Println("encrypted", []byte(encrypted))
        fmt.Println("encrypted (hex)", encrypted)
        fmt.Println("ciphertext", ciphertext)
        fmt.Println("aes.BlockSize", aes.BlockSize)
    }
    
    func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
        padding := (blockSize - len(ciphertext)%blockSize)
        padtext := bytes.Repeat([]byte{byte(padding)}, padding)
        return append(ciphertext, padtext...)
    }