0

There might be just a minor mistake I have done that I cannot see. Perhaps someone else looking over this could figure out what I am doing wrong.

This is function in C# that I am trying to rewrite in Go, the objective is to output the same value when calling a function.

public static string NewEncrypt(string Input)
{
    RijndaelManaged rijndaelManaged = new RijndaelManaged();
    rijndaelManaged.KeySize = 256;
    rijndaelManaged.BlockSize = 256;
    rijndaelManaged.Padding = PaddingMode.PKCS7;
    rijndaelManaged.Key = Convert.FromBase64String(Convert.ToBase64String(Encoding.UTF8.GetBytes("095fc90fe8b18e8f243e4b07a9c0d170")));
    rijndaelManaged.IV = Convert.FromBase64String(Convert.ToBase64String(Encoding.UTF8.GetBytes("8bef55a546d27958ead1fdddba4d36ea")));
    ICryptoTransform transform = rijndaelManaged.CreateEncryptor(rijndaelManaged.Key, rijndaelManaged.IV);
    byte[] myArray = null;
    using (MemoryStream memoryStream = new MemoryStream())
    {
        using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write))
        {
            byte[] bytes = Encoding.UTF8.GetBytes(Input);
            cryptoStream.Write(bytes, 0, bytes.Length);
        }
        myArray = memoryStream.ToArray();
    }
    return Convert.ToBase64String(myArray);
}

You can call it using:

NewEncrypt("{\"randJsonList\":[ \"abc\" ], \"name\":\"baron\", \"support\":\"king\"}")

We have this return output (myArray):

DdSUyoYRYW/zDNSVaA1JZ39WqJt06qp0FiJUlCW5BbZWEt41GzsmtgVnGZuHigZNs7qKhI+kHAKMXL8EPnK1vg==

Now for my Go implementation (I was trying to make use of GitHub resources: https://gist.github.com/huyinghuan/7bf174017bf54efb91ece04a48589b22):

First thing you might notice is that I do not know where I can use global IV variable, each time you run this code you are presented with different output. I want to output the same result as in C#, unless the input string is modified

package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "errors"
    "fmt"
    "io"
)

var KEY = []byte("095fc90fe8b18e8f243e4b07a9c0d170")
var IV = []byte("8bef55a546d27958ead1fdddba4d36ea")

func encrypt(myInput string) []byte {
    plaintext := []byte(myInput)

    // Create the AES cipher
    block, err := aes.NewCipher(KEY)
    if err != nil {
        panic(err)
    }

    plaintext, _ = pkcs7Pad(plaintext, block.BlockSize())
    // The IV needs to be unique, but not secure. Therefore it's common to
    // include it at the beginning of the ciphertext.
    ciphertext := make([]byte, aes.BlockSize+len(plaintext))
    iv := ciphertext[:aes.BlockSize]
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        panic(err)
    }
    bm := cipher.NewCBCEncrypter(block, iv)
    bm.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
    //stream := cipher.NewCFBEncrypter(block, iv)
    //stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)

    return ciphertext
}

// pkcs7Pad right-pads the given byte slice with 1 to n bytes, where
// n is the block size. The size of the result is x times n, where x
// is at least 1.
func pkcs7Pad(b []byte, blocksize int) ([]byte, error) {
    if blocksize <= 0 {
        return nil, errors.New("invalid blocksize")
    }
    if b == nil || len(b) == 0 {
        return nil, errors.New("invalid PKCS7 data (empty or not padded)")
    }
    n := blocksize - (len(b) % blocksize)
    pb := make([]byte, len(b)+n)
    copy(pb, b)
    copy(pb[len(b):], bytes.Repeat([]byte{byte(n)}, n))
    return pb, nil
}

func main() {
    plainText := "{\"randJsonList\":[ \"abc\" ], \"name\":\"baron\", \"support\":\"king\"}"
    x := encrypt(plainText)

    outputString := base64.StdEncoding.EncodeToString(x)
    fmt.Println(outputString)
}

Example output (not the same as C#):

PS D:\Software\Git\repositories\tet> go run .\main.go
N+hm5TItq367eXAz+WbtKXhhhMAy4woEKSngTf6rGUt8GZce7LsUxaqNtheceGDZ2dK8Bx187x87NeRPC1UQ6lUokjy7t1MLU8NcCtjODCM=

PS D:\Software\Git\repositories\tet> go run .\main.go
OT/CngTVs2O4BR4czjvR3MLVPoKFH2dUtW8LsIDUgLXfikJrRKsvKGaf0JFe39Cwf1/00HP7mvmCure7+IO+vupzAtdLX6nTQt1KZGsNp4o=

PS D:\Software\Git\repositories\tet> go run .\main.go
yDRHxWTvjX4HnSW8jbao+0Mhf77zgRj9tKXA3MNtAoF1I3bRou5Sv4Ds+r0HRuiA7NkoBR57m4aCYcU6quYzQA3R0GCGB8TGUfrWS5PvMNU=
arizona525
  • 13
  • 4
  • 1
    Just my 2 notes: your C#-code is using Rijndael algorithm with a chosen block length of 256 bit = 32 byte. The AES algorithm you are using in the linked Go code has a block length of 128 bit = 16 byte, so you need to find a Go implementation that supports a block length of 256 bit/32 byte. Second: in your C# code you are using a **static IV** that makes your complete encryption **unsecure**, so my advice would be to use a randomly generated IV and pass it along with the ciphertext to the recipient for decryption. – Michael Fehr Sep 11 '21 at 21:35
  • The C# code is not doing a hex decode. Don't do that. Also, you should pass `IV` to `cipher.NewCBCEncrypter(block, iv)` rather than `iv`, if you want it to work the same way as the C# code. – Brannon Sep 11 '21 at 21:43
  • @Brannon When you specify IV in this way, the console panics with the following: `panic: cipher.NewCBCEncrypter: IV length must equal block size`. I believe this is what Michael mentioned, it will not work because the Go implementation is 128bit/16byte. Also, for this scenario I want to use static IV. So, in the end.. do not use hex decode? It would be great to get some examples, but I will try and find another go implementation with 256bit/32byte. – arizona525 Sep 11 '21 at 22:10

1 Answers1

4

The C# code uses Rijndael with a block size of 256 bits (see comments), a static IV (see comments) and returns only the ciphertext (i.e. without prepended IV).
The Go code applies AES with by definition a block size of 128 bits, a randomly generated IV (the static IV in the code is ignored) and returns the concatenation of IV and ciphertext.

AES is a subset of Rijndael. Rijndael defines different block sizes and key sizes between 128 and 256 bits in 32 bit steps, see here. For AES only the block size 128 bits is defined and the key sizes 128, 192 and 256 bits. Note that the standard is AES and not Rijndael, so AES should be preferred over Rijndael (many libraries do not even implement Rijndael, but AES instead).

A static IV is insecure. Key/IV pairs must not repeat for security reasons. Therefore, in general, with a fixed key, a random IV is generated for each encryption. The IV is not secret and is passed along with the ciphertext to the decryption side, typically concatenated in the order IV|ciphertext.

Therefore, the current Go code is a secure implementation (even more secure is authenticated encryption e.g. via GCM), while the C# code is not. So it would make more sense to modify the C# code to be functionally equivalent to the Go code.
However, since the C# code seems to be the reference, the following changes are needed in the Go code to make it functionally identical to the C# code:

  • Instead of AES, Rijndael must be applied. In the following example, pkg.go.dev/github.com/azihsoyn/rijndael256 is used. To do this, import "github.com/azihsoyn/rijndael256" and formally replace aes with rijndael256. You can of course apply another implementation.
  • The static IV is to be applied: bm := cipher.NewCBCEncrypter(block, IV). iv and its filling is to be removed together with associated imports.
  • Only the ciphertext is returned in the enecrypt()-method: return ciphertext[rijndael256.BlockSize:].

The following Go code gives the result of the C# code:

package main

import (
    "bytes"
    "github.com/azihsoyn/rijndael256"
    "crypto/cipher"
    "encoding/base64"
    "errors"
    "fmt"
)

var KEY = []byte("095fc90fe8b18e8f243e4b07a9c0d170")
var IV = []byte("8bef55a546d27958ead1fdddba4d36ea")

func encrypt(myInput string) []byte {
    plaintext := []byte(myInput)

    // Create the AES cipher
    block, err := rijndael256.NewCipher(KEY)
    if err != nil {
        panic(err)
    }

    plaintext, _ = pkcs7Pad(plaintext, block.BlockSize())
    // The IV needs to be unique, but not secure. Therefore it's common to
    // include it at the beginning of the ciphertext.
    ciphertext := make([]byte, rijndael256.BlockSize+len(plaintext))
    bm := cipher.NewCBCEncrypter(block, IV)
    bm.CryptBlocks(ciphertext[rijndael256.BlockSize:], plaintext)

    return ciphertext[rijndael256.BlockSize:]
}

// pkcs7Pad right-pads the given byte slice with 1 to n bytes, where
// n is the block size. The size of the result is x times n, where x
// is at least 1.
func pkcs7Pad(b []byte, blocksize int) ([]byte, error) {
    if blocksize <= 0 {
        return nil, errors.New("invalid blocksize")
    }
    if b == nil || len(b) == 0 {
        return nil, errors.New("invalid PKCS7 data (empty or not padded)")
    }
    n := blocksize - (len(b) % blocksize)
    pb := make([]byte, len(b)+n)
    copy(pb, b)
    copy(pb[len(b):], bytes.Repeat([]byte{byte(n)}, n))
    return pb, nil
}

func main() {
    plainText := "{\"randJsonList\":[ \"abc\" ], \"name\":\"baron\", \"support\":\"king\"}"
    x := encrypt(plainText)

    outputString := base64.StdEncoding.EncodeToString(x)
    fmt.Println(outputString)
}

The output:

DdSUyoYRYW/zDNSVaA1JZ39WqJt06qp0FiJUlCW5BbZWEt41GzsmtgVnGZuHigZNs7qKhI+kHAKMXL8EPnK1vg==

is equal to that of the C# code.

Topaco
  • 40,594
  • 4
  • 35
  • 62