Search code examples
phpgoencryptionaes

Porting PHP AES encryption to Golang


My ecommerce provider has this library in PHP, Java, JavaScript, C# and Python to encrypt my request, since my API is made with Go, naturally I thought, why not do it with Go?

Oh boy... I didn't know what I was getting into.

Here's the original PHP code:

class AesCrypto {
    /**
    * Encrypt string with a given key
    * @param strToEncrypt
    * @param key
    * @return String encrypted string
    */
    public static function encrypt($plaintext, $key128) {
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-128-cbc'));
        $cipherText = openssl_encrypt($plaintext, 'AES-128-CBC', hex2bin($key128), 1, $iv);
        return base64_encode($iv.$cipherText);
    }
}

I've tried several slightly different ways with Go, I guess the bare minimum is this:

func encrypt(text string, key string) string {
    data := []byte(text)
    block, _ := aes.NewCipher([]byte(key))
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        panic(err.Error())
    }
    nonce := make([]byte, gcm.NonceSize())
    if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
        panic(err.Error())
    }
    ciphertext := gcm.Seal(nonce, nonce, data, nil)
    encoded := base64.StdEncoding.EncodeToString([]byte(ciphertext))
    return encoded
}

I have created this function to encrypt and the decrypt and they work fine, but when I send it to my provider it doesn't work.

The key is assigned by the ecommerce provider and it is 32 length byte, I understand that the length "tells" newCipher to select AES-256, right? then it will never correspond to a AES-128, as indicated in the PHP func.

Other than checking with my ecommerce provider's service or trying to decrypt using the PHP code, how do I go about porting this PHP code?

Here's another attempt (from the Go crypto docs):

func encrypt4(text string, keyString string) string {
    key, _ := hex.DecodeString(keyString)
    plaintext := []byte(text)
    if len(plaintext)%aes.BlockSize != 0 {
        panic("plaintext is not a multiple of the block size")
    }

    block, err := aes.NewCipher(key)
    if err != nil {
        panic(err)
    }

    ciphertext := make([]byte, aes.BlockSize+len(plaintext))
    iv := ciphertext[:aes.BlockSize]
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        panic(err)
    }

    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
    final := base64.StdEncoding.EncodeToString(ciphertext)
    return final
}

Solution

  • GCM is not the same as CBC mode. The key is hex encoded, so a 32 byte string represents a 16 byte (or 128 bit) key.

    In CBC mode the plaintext must be padded so that it is a multiple of the block size. PHP's openssl_encrypt does this automatically (using PKCS#5/7), but in Go it must be done explicitely.

    Putting it all together we end up with a slight variation of the CBC encryption example in the docs:

    package main
    
    import (
        "bytes"
        "crypto/aes"
        "crypto/cipher"
        "crypto/rand"
        "encoding/base64"
        "encoding/hex"
        "io"
    )
    
    func encrypt(plaintext, key16 string) string {
        padded := pkcs7pad([]byte(plaintext), aes.BlockSize)
    
        key, err := hex.DecodeString(key16)
        if err != nil {
            panic(err)
        }
    
        block, err := aes.NewCipher(key)
        if err != nil {
            panic(err)
        }
    
        buffer := make([]byte, aes.BlockSize+len(padded)) // IV followed by ciphertext
        iv, ciphertext := buffer[:aes.BlockSize], buffer[aes.BlockSize:]
    
        if _, err := io.ReadFull(rand.Reader, iv); err != nil {
            panic(err)
        }
    
        mode := cipher.NewCBCEncrypter(block, iv)
        mode.CryptBlocks(ciphertext, padded)
    
        return base64.StdEncoding.EncodeToString(buffer)
    }
    
    func pkcs7pad(plaintext []byte, blockSize int) []byte {
        padding := blockSize - len(plaintext)%blockSize
    
        return append(plaintext, bytes.Repeat([]byte{byte(padding)}, padding)...)
    }