AES In Go using Crypto Package
Sample wrapper package implementing AES text encrytion/decrytion using Golang Crypto Package
Introduction
The Advanced Encryption Standard (AES), also known by its original name Rijndael is a specification for the encryption of electronic data. AES is a subset of the Rijndael block cipher developed by two Belgian cryptographers, Vincent Rijmen and Joan Daemen.
AES is a symmetric key algorithm, meaning the same key is used for both encryption and decryption of data.
Cryptography in Golang
Golang’s crypto package and subdirectories/sub packages provides implementation of various cryptography algorithms. In this article we will look at AES encryption capabilities.
Implementation
Lets start using AES in our code. We would need to import following crypto packages.
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
We would also need following pakcages to complete the implementation.
import (
base64 "encoding/base64"
"io"
"strings"
"bytes"
"fmt"
)
Encryption
Encrypt function accepts a string and key, encrypts string using either GCM or CBC and returns a base64 encoded string packing IV and encrypted string. Our wrapper is using following 2 variations of the transforms.
- AES/GCM/NoPadding (default)
- AES/CBC/PKCS7Padding - compatible with wrapper implementations in C# and Java (in the same repo)
We will start by creating aes
instance using NewCipher
function from aes
package by passing secret key.
aes, err := aes.NewCipher(key)
We will then check for current CipherMode
and either call EncryptGcm
or EncryptCbc
.
Complete code for the method is below
func (crypto AesCrypto) Encrypt(plainText string, key []byte) (string, error) {
// create a new aes cipher using key
aes, err := aes.NewCipher(key)
if err != nil {
return "", err
}
if crypto.CipherMode == GCM {
return crypto.EncryptGcm(aes, plainText)
} else {
return crypto.EncryptCbc(aes, plainText)
}
}
GCM/NoPadding
EncryptGcm
function accepts Block
cipher and plain text, encrypts plain text using GCM block cipher and returns base64 encoded string packing nonce and encrypted data.
We start by creating a new instace of gcm block cipher using NewGCM
function.
gcm, err := cipher.NewGCM(aes)
Next step would be to generate random nonce
nonce := make([]byte, gcm.NonceSize())
io.ReadFull(rand.Reader, nonce)
Next we would convert plain text to byte array and call Seal
function on gcm
instance to encrypt data and get cipher text.
plainTextBytes := []byte(plainText)
cipherText := gcm.Seal(nil, nonce, plainTextBytes, nil)
We finally pack the data in a byte array along with nonce and GCM tag size, convert to base64 string and return to caller.
Complete code for the method is below
func (crypto AesCrypto) EncryptGcm(aes cipher.Block, plainText string) (string, error) {
gcm, err := cipher.NewGCM(aes)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
plainTextBytes := []byte(plainText)
cipherText := gcm.Seal(nil, nonce, plainTextBytes, nil)
return crypto.PackCipherData(cipherText, nonce, gcm.Overhead()), nil
}
CBC/Pkcs7
EncryptCbc
function accepts Block
cipher and plain text, encrypts plain text using CBC block cipher and returns base64 encoded string packing nonce and encrypted data.
We start by generating random IV (initialization vector)
iv := make([]byte, AesIvSize)
io.ReadFull(rand.Reader, iv)
Next we would create encrypter
instance using NewCBCEncrypter
function by passing in block cipher and iv.
encrypter := cipher.NewCBCEncrypter(aes, iv)
Next we will convert plain text to bytes and encoding with Pkcs7. I couldn’t find PKCS7 encoding in official packages, so I am using implmentation courtesy of golang-examples
plainTextBytes := []byte(plainText)
plainTextBytes, err := pkcs7Pad(plainTextBytes, encrypter.BlockSize())
Next step is to call CryptBlocks
function of encrypter
to encrypt data.
cipherText := make([]byte, len(plainTextBytes))
encrypter.CryptBlocks(cipherText, plainTextBytes)
We finally pack the data in a byte array along with iv, convert to base64 string and return to caller.
Complete code for the method is below
func (crypto AesCrypto) EncryptCbc(aes cipher.Block, plainText string) (string, error) {
iv := make([]byte, AesIvSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
encrypter := cipher.NewCBCEncrypter(aes, iv)
plainTextBytes := []byte(plainText)
plainTextBytes, err := pkcs7Pad(plainTextBytes, encrypter.BlockSize())
if err != nil {
return "", err
}
cipherText := make([]byte, len(plainTextBytes))
encrypter.CryptBlocks(cipherText, plainTextBytes)
return crypto.PackCipherData(cipherText, iv, 0), nil
}
Decryption
Decrypt method works in conjunction with Encrypt method above, it accepts base64 encoded cipher text string, key and provider, we use provider in gcm to create correct instance of gcm block cipher as instance with non-default values is required to decrypt data encrypted in C#/Java. It unpacks base64 encoded string to get iv, gcm tag size if CipherMode is GCM and encrypted data bytes and performs decryption and returns plain text.
We will start by decoding base64 cipher text string to get packed cipher data.
data, err := base64.StdEncoding.DecodeString(cipherText)
We will then call UnpackCipherData
function to get encryptedBytes, iv/nonce and tagSize.
encryptedBytes, iv, tagSize := crypto.UnpackCipherData(data)
Next we will create aes
intance using NewCipher
function from aes
package by passing secret key.
aes, err := aes.NewCipher(key)
We will then check for current CipherMode
and either call DecryptGcm
or DecryptCbc
.
Complete code for the method is below
func (crypto AesCrypto) Decrypt(cipherText string, key []byte, provider string) (string, error) {
data, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
encryptedBytes, iv, tagSize := crypto.UnpackCipherData(data)
aes, err := aes.NewCipher(key)
if err != nil {
return "", err
}
if crypto.CipherMode == GCM {
return DecryptGcm(aes, encryptedBytes, iv, tagSize, provider)
} else {
return DecryptCbc(aes, encryptedBytes, iv)
}
}
GCM/NoPadding
DecryptGcm
function accepts Block
cipher, encrypted, nonce, tagSize and provider, decrypts encrypted bytes and returns plain text as string.
We start by creating a new instace of gcm block cipher AEAD
depending on provider.
var aesgcm cipher.AEAD
var err error
if strings.EqualFold("go", provider) {
aesgcm, err = cipher.NewGCM(aes)
} else {
aesgcm, err = cipher.NewGCMWithNonceSize(aes, tagSize) // only used for compatibility, NewGCM recomended
}
We then call Open
method on aesgcm
instance to decrypt data.
decryptedBytes, err := aesgcm.Open(nil, nonce, encrypted, nil)
We finally convert byte array to string and return decrypted string to caller.
Complete code for the method is below
func DecryptGcm(aes cipher.Block, encrypted []byte, nonce []byte, tagSize int, provider string) (string, error) {
var aesgcm cipher.AEAD
var err error
if strings.EqualFold("go", provider) {
aesgcm, err = cipher.NewGCM(aes)
} else {
aesgcm, err = cipher.NewGCMWithNonceSize(aes, tagSize) // only used for compatibility, NewGCM recomended
}
if err != nil {
return "", err
}
decryptedBytes, err := aesgcm.Open(nil, nonce, encrypted, nil)
if err != nil {
return "", err
}
return string(decryptedBytes[:len(decryptedBytes)]), nil
}
CBC/Pkcs7
DecryptCbc
function accepts Block
cipher, encrypted bytes and iv, decrypts encrypted bytes and returns plain text as string.
We start by creating a new instace of decryptor using function NewCBCDecrypter
.
decryptor := cipher.NewCBCDecrypter(aes, iv)
We then call CryptBlocks
method on decryptor
instance to decrypt data.
decryptedBytes := make([]byte, len(encrypted))
decryptor.CryptBlocks(decryptedBytes, encrypted)
Next we call pkcs7Unpad
on decrypted data to remove any padding charaters.
decryptedBytes, err := pkcs7Unpad(decryptedBytes, decryptor.BlockSize())
We finally convert byte array to string and return decrypted string to caller.
Complete code for the method is below
func DecryptCbc(aes cipher.Block, encrypted []byte, iv []byte) (string, error) {
decryptor := cipher.NewCBCDecrypter(aes, iv)
decryptedBytes := make([]byte, len(encrypted))
decryptor.CryptBlocks(decryptedBytes, encrypted)
decryptedBytes, err := pkcs7Unpad(decryptedBytes, decryptor.BlockSize())
if err != nil {
return "", err
}
return string(decryptedBytes[:len(decryptedBytes)]), nil
}
TL;DR
Complete code for the wrapper class that implements encryption and decryption using Golang Crypto package can be found at [aescrypto.go]https://github.com/kashifsoofi/crypto-sandbox/blob/master/go/aescrypto/aescrypto.go). Unit tests for the wrapper class can be found at aescrypto_test.go. Complete repository with other samples and languages can be found at CryptoSandbox.
References
In no particular order
https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
https://golang.org/pkg/crypto/
https://golang-examples.tumblr.com/post/98350728789/pkcs7-padding
https://golang.org/src/crypto/cipher/cbc.go
https://medium.com/@matryer/5-simple-tips-and-tricks-for-writing-unit-tests-in-golang-619653f90742
https://godoc.org/github.com/google/uuid
https://gobyexample.com/base64-encoding
https://yourbasic.org/golang/copy-explained/
https://tutorialedge.net/golang/go-encrypt-decrypt-aes-tutorial/
And many more
Leave a Comment
Your email address will not be published. Required fields are marked *