Applications that store tokens, private configuration, or other sensitive values often need encryption at rest. Go includes everything required to implement modern symmetric encryption in its standard library.

This example uses AES-GCM, an authenticated encryption mode that protects both confidentiality and integrity. That makes it a better default for new applications than older unauthenticated modes such as CFB.

AES-GCM in Brief

  • Symmetric encryption uses the same secret key for encryption and decryption.
  • AES accepts 16-, 24-, or 32-byte keys for AES-128, AES-192, or AES-256.
  • GCM adds authentication, so modified ciphertext is rejected during decryption.
  • A fresh nonce must be used for every encryption operation with the same key.

Complete Go Example

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"fmt"
)

func generateRandomKey() ([]byte, error) {
	key := make([]byte, 32) // AES-256
	if _, err := rand.Read(key); err != nil {
		return nil, err
	}
	return key, nil
}

func encrypt(plaintext, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, err
	}

	// Prefix the ciphertext with the nonce so decrypt can recover it.
	return gcm.Seal(nonce, nonce, plaintext, nil), nil
}

func decrypt(ciphertext, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return nil, fmt.Errorf("ciphertext too short")
	}

	nonce := ciphertext[:nonceSize]
	ciphertext = ciphertext[nonceSize:]

	return gcm.Open(nil, nonce, ciphertext, nil)
}

func main() {
	plaintext := []byte("Secret application data")

	key, err := generateRandomKey()
	if err != nil {
		panic(err)
	}

	ciphertext, err := encrypt(plaintext, key)
	if err != nil {
		panic(err)
	}

	fmt.Println("Ciphertext (base64):", base64.StdEncoding.EncodeToString(ciphertext))

	decrypted, err := decrypt(ciphertext, key)
	if err != nil {
		panic(err)
	}

	fmt.Println("Decrypted text:", string(decrypted))
}

How It Works

  1. generateRandomKey creates a cryptographically secure 32-byte key for AES-256.
  2. encrypt creates an AES cipher, wraps it with GCM, generates a random nonce, and encrypts the plaintext.
  3. The nonce is stored at the beginning of the returned byte slice. A nonce does not need to be secret, but it must not be reused with the same key.
  4. decrypt separates the nonce from the ciphertext and calls gcm.Open. If the ciphertext was modified, authentication fails and an error is returned.

Key Management Matters

The encryption code is only one part of a secure design. Do not hard-code real encryption keys in source control. In production, load keys from an appropriate secret-management system, protected environment, or key-management service.

Also remember that encrypting passwords is usually the wrong approach. User passwords should normally be stored with a dedicated password-hashing algorithm rather than reversible encryption.

Conclusion

AES-GCM gives Go applications authenticated symmetric encryption using only the standard library. Generate strong keys, use a unique nonce for each encryption operation, handle every error, and protect the key separately from the encrypted data.