Encrypt and Decrypt Data in Go with AES-256

April 30, 2024
Written by
Temitope Taiwo Oyedele
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Encrypt and Decrypt Data in Go with AES-256

Safeguarding sensitive information is a cornerstone of secure application development! Its importance cannot be overstated — especially in today's world where cyber threats constantly evolve. Secure development practices are no longer optional – they are essential for protecting user privacy, preventing data breaches, and ensuring the overall trustworthiness of your application.

In the Go programming language, understanding how to encrypt and decrypt data is crucial for implementing robust security measures.

In this article, you'll go through the process of encrypting and decrypting data using AES-256 (Advanced Encryption Standard) encryption in Go, leveraging the standard library's crypto/aes and crypto/cipher packages.

Prerequisites

Before diving into encryption and decryption, ensure you have the following available:

  • Go installed on your machine
  • A terminal emulator
  • A text editor
  • A basic understanding of Go programming
  • Some prior knowledge of encryption would be helpful, but is not necessary

Encrypt data using AES-256

AES-256 encryption is a highly secure symmetric encryption algorithm that uses a 256-bit key to encrypt data. The process of AES-256 encryption is designed to ensure the highest level of security, making it highly resistant to brute-force attacks and other forms of cyber threats.

The AES-256 encryption process begins by dividing the plaintext, or original, data into blocks 128 bits in size. To break this down, AES arranges the data to be encrypted in a 4x4 grid where each cell contains 16 bytes. Given that each byte is composed of 8 bits, the total bit count within each block is precisely 128 bits. This allows the algorithm to operate on manageable chunks of data. The data is then transformed into ciphertext through a series of rounds.

Rounds refers to the multiple stages of processing that the plaintext undergoes to be transformed into ciphertext. Each round involves a complex series of operations like substitution and permutation, all aimed at scrambling the original data (plaintext) into an unreadable format (ciphertext), making it practically impossible to reverse without the correct key. This process is designed to be secure and complex, making it highly resistant to brute-force attacks.

In AES, the size of encrypted data remains constant. This means that it takes 128 bits of the data and encrypts it into 128 bits of ciphertext.

AES-256 encryption offers several key advantages, some of which include:

  • Complexity Enhancement: AES-256 uses a unique key for each round, significantly increasing encryption complexity and making it resistant to brute-force attacks 2
  • Nonlinear Data Modification: It employs byte substitution, obscuring the relationship between plaintext and ciphertext, enhancing security against cryptanalysis 2
  • Data Diffusion: Through shifting rows and mixing columns, AES-256 diffuses data, complicating encryption and ensuring encrypted data doesn't retain any direct relationship with the original plaintext

Due to its exceptional strength, AES-256 is widely used by governments, militaries, and financial institutions to protect their most sensitive information.

Open up the terminal and create a folder for this project.

mkdir encryption

Next, inside the folder, create a file called main.go. Then, in the file, add the following code:

package main

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

func main() {
   data := "private employee records restricted to personnel only"
   plaintext := []byte(data)
   key := make([]byte, 32)

   if _, err := rand.Reader.Read(key); err != nil {
       fmt.Println("error generating random encryption key ", err)
       return
   }
}

In the code above, we prepared the data we want to encrypt, which is a string, and then converted it into a byte slice because AES cryptographic algorithms require data to be in a binary format, meaning it needs to be expressed in a sequence of 0’s and 1’s.

We then enhanced the security of our data by generating a 32-byte encryption key. This step is important, especially for AES-256 encryption, because it requires exactly a 32-byte key length. The length of this key is directly tied to the security and the complexity of the encryption process. In simpler terms, the longer the key, the more resistant it is to security breaches.

Afterward, we created the encryption key using the byte slice and then used the `rand.Reader.Read()` to ensure that our key was secure and composed of random numbers.

Next, we’ll create an AES-256 block cipher. Think of it as a fundamental tool for keeping data secure. This cipher processes data in fixed 128-bit blocks, which is similar to a lock and key system. The block is like a lock, and our key is the secret code used to access it.

We’ll use the crypto/aes package to make this happen. Once again, the block cipher is vital because it lays the foundation of data security. It processes our data in fixed-size blocks, adding a strong protection layer.

Right after the error handling block at the end of main(), add the following code:

block, err := aes.NewCipher(key)
   if err != nil {
       fmt.Println("error creating aes block cipher", err)
       return
   }

Use GCM For enhanced security

Now, we’ll introduce the GCM (Galois/Counter Mode), a powerful encryption method. GCM is a cryptographic mode of operation that combines the security of a symmetric block cipher (like AES) with message authentication.

GCM does two key things. First, it encrypts data, making it unreadable to unauthorized eyes. Second, it generates a tag (checksum) that verifies the data's integrity during decryption. Any modification to the ciphertext will result in a verification failure.

In simple terms, using GCM is like adding a digital seal to our encrypted data. This ensures that it remains intact and not tampered with during transmission. We’ll set the GCM using the crypto/cipher package, by adding the following code below to the end of main():

gcm, err := cipher.NewGCM(block)
if err != nil {
    fmt.Println("error setting gcm mode", err)
    return
}

Generate a NONCE

You next need to create a nonce, which stands for "number used once". It is a random or pseudo-random value used along with a GCM for cryptographic operations. A nonce is a crucial part of the encryption process. It's a unique value used only once for each encryption. This helps ensure the confidentiality of the data and prevent replay attacks.

Add the code below to the end of the main() function.

nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
    fmt.Println("error generating the nonce ", err)
    return
}

The nonce ensures that every encryption operation is unique and secure. Think of a nonce as a unique identifier for each encryption, like a lottery ticket number. It ensures that even if we encrypt the same data multiple times, each encryption is unique and secure. We’ll generate one and fill it with unpredictable data using the crypto/rand package.

Now, we'll use the gcm.Seal() function to turn our data into a secure, unreadable form to protect it from prying eyes by adding the code below to the end of the main() function.

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

Convert the ciphertext to hexadecimal

The last step is to convert the ciphertext to hexadecimal and print out the result. All that you need to do is add the following to the end of main().

enc := hex.EncodeToString(ciphertext)
fmt.Println("original data:", data)
fmt.Println("encrypted data:", enc)

So, in our encryption process, we prepped the data for encryption, generated a secure 32-byte encryption key, created an AES block cipher, used GCM mode for encryption, and finally generated and utilized the nonce to ensure the uniqueness of each encryption operation.

Decrypt data using AES-256

Now that we've encrypted our data, we need to know how to decrypt it and retrieve the original information. Decryption involves, effectively, reversing the encryption process. We'll need the same encryption key and other parameters used during encryption to decrypt the data successfully.

In short, to decrypt the data we'll:

  • Convert the ciphertext from hexadecimal back to binary format
  • Create the AES block cipher using the same encryption key

Use the GCM mode to decrypt the ciphertext and retrieve the original plaintext

Right after the code that converts to hexadecimal, add the following code

decodedCipherText, err := hex.DecodeString(enc)
if err != nil {
    fmt.Println("error decoding hex", err)
    return
}

decryptedData, err := gcm.Open(nil, decodedCipherText[:gcm.NonceSize()], decodedCipherText[gcm.NonceSize():], nil)
if err != nil {
    fmt.Println("error decrypting data", err)
    return
}

In the code above, we decode the hexadecimal ciphertext to obtain the original binary data. We then create a new instance of the AES block cipher and GCM mode using the same key and nonce. Finally, we use the gcm.Open() function to decrypt the ciphertext, producing the original plaintext.

Print the decrypted data. After the code that prints the encrypted data, add this:

fmt.Println("Decrypted data:", string(decryptedData))

Test the Application

Run the code in the terminal using the go run command:

go run main.go

Below is a screenshot of the output:

You have successfully encrypted and decrypted data in Go!

Conclusion

In this tutorial, we learned how to encrypt and decrypt data using AES-256 in Go. We used the crypto package to perform encryption and decryption operations. We also discussed the importance of using a secure random number generator for generating keys and nonces.

Temitope Taiwo Oyedele is a software engineer and technical writer. He likes to write about things he’s learned and experienced.