Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Encrypt And Decrypt Data In A Golang Application With The Crypto Packages

TwitterFacebookRedditLinkedInHacker News

Being able to encrypt and decrypt data within an application is very useful for a lot of circumstances. Let’s not confuse encryption and decryption with hashing like that found in a bcrypt library, where a hash is only meant to transform data in one direction.

Not too long ago I wrote about in a previous article how to encrypt and decrypt data using Node.js. This was partially inspired by me learning how to build software wallets for cryptocurrencies and encrypting the sensitive information. However, what if we wanted to use Go instead of Node.js?

We’re going to take a look at encrypting data and then decrypting it within a Go application by using the already available crypto packages.

Going forward, a lot of this tutorial was inspired by the official Go documentation and another tutorial, one that I didn’t write. Some code was taken from a book titled, Build Web Applications with Golang, more specifically chapter nine, the chapter on security and encryption. In any sense, there were things left to be desired, which is where I hope to come in. I am by no means an expert at ciphers or encryption and decryption, and while there may be better solutions to what I’m demonstrating, what I did works.

Hashing Passwords to Compatible Cipher Keys

When encrypting and decrypting data, it is important that you are using a 32 character, or 32 byte key. Being realistic, you’re probably going to want to use a passphrase and that passphrase will never be 32 characters in length.

To get around this, you can actually hash your passphrase using a hashing algorithm that produces 32 byte hashes. I found a list of hashing algorithms on Wikipedia that provide output lengths. We’re going to be using a simple MD5 hash. It is insecure, but it doesn’t really matter since we won’t be storing the output.

Within a Go project, we can add the following function:

func createHash(key string) string {
	hasher := md5.New()
	hasher.Write([]byte(key))
	return hex.EncodeToString(hasher.Sum(nil))
}

The above function will take a passphrase or any string, hash it, then return the hash as a hexadecimal value.

Remember, we just need keys that meet the length criteria that AES demands.

Encrypting Data with an AES Cipher

Now that we have a key of an appropriate size, we can start the encryption process. We can be encrypting text, or any binary data, it doesn’t really matter.

Within a Go project, include the following function:

func encrypt(data []byte, passphrase string) []byte {
	block, _ := aes.NewCipher([]byte(createHash(passphrase)))
	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)
	return ciphertext
}

Remember, the code I’m using is a Frankenstein from several different sources. So what exactly is happening in the above function?

block, _ := aes.NewCipher([]byte(createHash(passphrase)))

First we create a new block cipher based on the hashed passphrase. Once we have our block cipher, we want to wrap it in Galois Counter Mode (GCM) with a standard nonce length.

Before we can create the ciphertext, we need to create a nonce.

nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
    panic(err.Error())
}

The nonce that we create needs to be the length specified by GCM. It is important to note that the nonce used for decryption must be the same nonce used for encryption.

There are a few strategies that can be used to make sure our decryption nonce matches the encryption nonce. One strategy would be to store the nonce alongside the encrypted data if it is going into a database. Another option is to prepend or append the nonce to the encrypted data. We’ll prepending the nonce.

ciphertext := gcm.Seal(nonce, nonce, data, nil)

The first parameter in the Seal command is our prefix value. The encrypted data will be appended to it. With the ciphertext, we can return it back to a calling function.

Decrypting Data that uses an AES Cipher

Now that we have potentially encrypted some data, we probably want to be sure that we can decrypt that same data. The process for decryption is nearly the same as the encryption process.

Take the following function for example:

func decrypt(data []byte, passphrase string) []byte {
	key := []byte(createHash(passphrase))
	block, err := aes.NewCipher(key)
	if err != nil {
		panic(err.Error())
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		panic(err.Error())
	}
	nonceSize := gcm.NonceSize()
	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		panic(err.Error())
	}
	return plaintext
}

In the above code we create a new block cipher using a hashed passphrase. We wrap the block cipher in Galois Counter Mode and get the nonce size.

Remember, we prefixed our encrypted data with the nonce. This means that we need to separate the nonce and the encrypted data.

nonceSize := gcm.NonceSize()
nonce, ciphertext := data[:nonceSize], data[nonceSize:]

When we have our nonce and ciphertext separated, we can decrypt the data and return it as plaintext.

Encrypting and Decrypting Files

Encrypting and decrypting data as we demand is cool, but what if we wanted to encrypt and decrypt files. One way to handle file encryption is to take what we’ve already done and just use the file commands.

Take the following function for example:

func encryptFile(filename string, data []byte, passphrase string) {
	f, _ := os.Create(filename)
	defer f.Close()
	f.Write(encrypt(data, passphrase))
}

The above function wile create and open a file based on the filename passed. With the file open, we can encrypt some data and write it to the file. The file will close when we’re done.

To decrypt the file, we could use the following function:

func decryptFile(filename string, passphrase string) []byte {
	data, _ := ioutil.ReadFile(filename)
	return decrypt(data, passphrase)
}

Now I’m not saying that this is the best way to handle file encryption and decryption, but I’m saying that this is just one of many ways to accomplish the task. If you’ve got a better way, I’d love to hear it in the comments.

The Full Project Code

We covered a lot of little bits and pieces in this example. You can find a full and working example of our project below:

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"io/ioutil"
	"os"
)

func createHash(key string) string {
	hasher := md5.New()
	hasher.Write([]byte(key))
	return hex.EncodeToString(hasher.Sum(nil))
}

func encrypt(data []byte, passphrase string) []byte {
	block, _ := aes.NewCipher([]byte(createHash(passphrase)))
	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)
	return ciphertext
}

func decrypt(data []byte, passphrase string) []byte {
	key := []byte(createHash(passphrase))
	block, err := aes.NewCipher(key)
	if err != nil {
		panic(err.Error())
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		panic(err.Error())
	}
	nonceSize := gcm.NonceSize()
	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		panic(err.Error())
	}
	return plaintext
}

func encryptFile(filename string, data []byte, passphrase string) {
	f, _ := os.Create(filename)
	defer f.Close()
	f.Write(encrypt(data, passphrase))
}

func decryptFile(filename string, passphrase string) []byte {
	data, _ := ioutil.ReadFile(filename)
	return decrypt(data, passphrase)
}

func main() {
	fmt.Println("Starting the application...")
	ciphertext := encrypt([]byte("Hello World"), "password")
	fmt.Printf("Encrypted: %x\n", ciphertext)
	plaintext := decrypt(ciphertext, "password")
	fmt.Printf("Decrypted: %s\n", plaintext)
	encryptFile("sample.txt", []byte("Hello World"), "password1")
	fmt.Println(string(decryptFile("sample.txt", "password1")))
}

When you run the above code, it should demo the file encryption as well as encryption without ever touching a file. It is very awesome and fast when using Go.

Conclusion

You just saw how to encrypt and decrypt data using the Go programming language and Golang’s crypto package. Remember not to confuse encryption and decryption with hashing. When you encrypt something, you’re anticipating being able to get that data back. When you’re hashing data using something like bcrypt, you’re anticipating never being able to read the hashed value again, but instead compare against the hashed value.

If you think you have better solutions for encryption with Golang, I’d love to hear about them in the comments. If you think my solutions are good, but could be improved, I’d love to hear about that as well.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.