Data encryption in Go using OpenSSL
Go has its own crypto implementation which does not use OpenSSL. This is all great and convenient, but it makes harder to interact with openssl-encoded data that's likely produced by code written in other languages, like Ruby. As a backstory i tried to experiment with a service that should hook into PostgreSQL database used by rails application. Rails app uses ActiveRecord and symmetric-encryption gem that provides functionality to automatically encrypt/decrypt model attributes using AES-256-CBC cipher. My objective was to write Go code that does the same thing - encrypts/decrypts data compatible with ruby's OpenSSL library.
Here's an example code in used to encrypt data in Ruby:
require "openssl"
cipher = OpenSSL::Cipher.new("aes-256-cbc")
cipher.encrypt
cipher.key = "1234567890ABCDEF1234567890ABCDEF"
cipher.iv = "1234567890ABCDEF"
ciphertext = cipher.update("hello world")
ciphertext << cipher.final
# final base64-encoded encoded string
encoded = [ciphertext].pack("m")
puts encoded
# => RanFyUZSP9u/HLZjyI5zXQ==
Unfortunately Go does not have official OpenSSL bindings, but there a pretty good third-party library available right now. Make sure to install it first:
go get github.com/spacemonkeygo/openssl
And here's our "class" to implement encryption/decryption using openssl bindings:
package main
import (
"github.com/spacemonkeygo/openssl"
)
type Crypter struct {
key []byte
iv []byte
cipher *openssl.Cipher
}
func NewCrypter(key []byte, iv []byte) (*Crypter, error) {
cipher, err := openssl.GetCipherByName("aes-256-cbc")
if err != nil {
return nil, err
}
return &Crypter{key, iv, cipher}, nil
}
func (c *Crypter) Encrypt(input []byte) ([]byte, error) {
ctx, err := openssl.NewEncryptionCipherCtx(c.cipher, nil, c.key, c.iv)
if err != nil {
return nil, err
}
cipherbytes, err := ctx.EncryptUpdate(input)
if err != nil {
return nil, err
}
finalbytes, err := ctx.EncryptFinal()
if err != nil {
return nil, err
}
cipherbytes = append(cipherbytes, finalbytes...)
return cipherbytes, nil
}
func (c *Crypter) Decrypt(input []byte) ([]byte, error) {
ctx, err := openssl.NewDecryptionCipherCtx(c.cipher, nil, c.key, c.iv)
if err != nil {
return nil, err
}
cipherbytes, err := ctx.DecryptUpdate(input)
if err != nil {
return nil, err
}
finalbytes, err := ctx.DecryptFinal()
if err != nil {
return nil, err
}
cipherbytes = append(cipherbytes, finalbytes...)
return cipherbytes, nil
}
And simple usage:
import(
"log"
"encoding/base64"
)
func main() {
// same key and initialization vector as in ruby example
key := []byte("1234567890ABCDEF1234567890ABCDEF")
iv := []byte("1234567890ABCDEF")
// Initialize new crypter struct. Errors are ignored.
crypter, _ := NewCrypter(key, iv)
// Lets encode plaintext using the same key and iv.
// This will produce the very same result: "RanFyUZSP9u/HLZjyI5zXQ=="
encoded, _ := crypter.Encrypt([]byte("hello world"))
log.Println(base64.StdEncoding.EncodeToString(encoded))
// Decode previous result. Should print "hello world"
decoded, _ := crypter.Decrypt(encoded)
log.Printf("%s\n", decoded)
}
Now we've got working code that's compatible with existing ruby codebase.
Great! What's next? Well, if you noticed, key
variable in both (ruby/go) examples
is hardcoded. That's a key that should be kept secret.
Per symmetric-encryption gem's documentation, encryption key is not supposed to be stored within codebase. Usually the key is provided to the application via environment variable during startup. Symmetric-encryption gem uses base64-encoded version of the key, which is also encrypted with a private RSA key generated by a rails generator. That private RSA key is stored in the same configuration file.
Here's how to decrypt the key using private RSA key in go (code is simplified):
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
)
// input - our encrypted data
// key - private RSA key data
func rsaDecode(input, key []byte) ([]byte, error) {
block, _ := pem.Decode(key)
rsaKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
return rsa.DecryptPKCS1v15(rand.Reader, rsaKey, input)
}
Most of the production configuration data in symmetric-encryption gem config files are base64-encoded, so make sure to decode that first before using any encryption/decryption functions.
Overall, this experiment with porting symmetric-encryption functionality into go codebase is successful. Just a side note: i did not actually deployed the codebase powered by openssl-bindings (or any of the samples i provided in this post) to production, everything was purely for a side project, so keep that in mind.