Crypto recipe

Symmetric encryption with AES-256-GCM

Encrypt and authenticate data with AES-256-GCM — authenticated encryption done right.

When one party (or one key) both encrypts and decrypts, use authenticated encryption (AEAD): it provides confidentiality and integrity together, so any tampering is caught on decrypt. AES-256-GCM and ChaCha20-Poly1305 are the standard choices.

The critical rule is nonce uniqueness: never encrypt two messages with the same key and nonce. With GCM's 96-bit nonce, a fresh random nonce per message is the simplest safe approach. Store the nonce next to the ciphertext — it is not secret.

Get it right

  • Use an AEAD (AES-GCM or ChaCha20-Poly1305). Never raw AES-CBC or ECB.
  • A unique nonce per message, every time — reuse with one key is catastrophic.
  • Store nonce + ciphertext + tag together; neither nonce nor tag is secret.
  • Always check the decrypt result — a failed tag means tampered or wrong key; discard it.
  • Bind context (IDs, versions) as additional authenticated data (AAD).

Implementation

Setup Standard library (crypto/aes, crypto/cipher).

Go
package main

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

func main() {
	key := make([]byte, 32) // AES-256
	rand.Read(key)

	block, err := aes.NewCipher(key)
	if err != nil {
		panic(err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		panic(err)
	}

	// A fresh random nonce for every message. NEVER reuse one with the same key.
	nonce := make([]byte, gcm.NonceSize()) // 12 bytes
	rand.Read(nonce)

	plaintext := []byte("attack at dawn")
	// Seal appends to its first arg, so ciphertext = nonce || sealed-data.
	ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)

	// Decrypt: split the nonce back off the front.
	n := gcm.NonceSize()
	got, err := gcm.Open(nil, ciphertext[:n], ciphertext[n:], nil)
	if err != nil {
		panic("authentication failed") // tampered or wrong key
	}
	fmt.Println(string(got))
}

Setup pip install cryptography.

Python
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = AESGCM.generate_key(bit_length=256)
aead = AESGCM(key)

nonce = os.urandom(12)        # fresh random nonce per message — never reuse
aad = b"context: invoice-42"  # authenticated, not encrypted (optional)

ciphertext = aead.encrypt(nonce, b"attack at dawn", aad)  # tag is appended automatically

# Decryption raises InvalidTag if the data or aad was tampered with.
plaintext = aead.decrypt(nonce, ciphertext, aad)

Setup Built in (node:crypto).

Node.js
import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';

const key = randomBytes(32);   // AES-256
const nonce = randomBytes(12); // fresh per message — never reuse with the same key

const cipher = createCipheriv('aes-256-gcm', key, nonce);
const ciphertext = Buffer.concat([cipher.update('attack at dawn', 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag(); // 16-byte tag — store with the nonce and ciphertext

// Decrypt — final() throws if the tag does not verify.
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag);
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);

In the browser/edge, WebCrypto does the same via crypto.subtle with AES-GCM (async).

Setup Built in (System.Security.Cryptography.AesGcm).

.NET
using System.Security.Cryptography;
using System.Text;

byte[] key = RandomNumberGenerator.GetBytes(32);                       // AES-256
byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize); // 12
byte[] plaintext = Encoding.UTF8.GetBytes("attack at dawn");

byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];                    // 16

using var aes = new AesGcm(key, tag.Length);
aes.Encrypt(nonce, plaintext, ciphertext, tag);

// Decrypt — throws CryptographicException if the tag does not verify.
byte[] decrypted = new byte[ciphertext.Length];
aes.Decrypt(nonce, ciphertext, tag, decrypted);

Store nonce, ciphertext, and tag together. The .NET 8+ AesGcm constructor requires the tag size explicitly.

Setup libsodium.

C++
#include <sodium.h>
#include <string>
#include <vector>

int main() {
    if (sodium_init() < 0) return 1;
    // libsodium's AES-GCM needs hardware AES (AES-NI). Check first; prefer
    // crypto_aead_xchacha20poly1305 when you need portability.
    if (crypto_aead_aes256gcm_is_available() == 0) return 1;

    unsigned char key[crypto_aead_aes256gcm_KEYBYTES]; // 32
    crypto_aead_aes256gcm_keygen(key);

    unsigned char nonce[crypto_aead_aes256gcm_NPUBBYTES]; // 12
    randombytes_buf(nonce, sizeof nonce);                 // unique per message

    std::string msg = "attack at dawn";
    std::vector<unsigned char> ct(msg.size() + crypto_aead_aes256gcm_ABYTES);
    unsigned long long ct_len;

    crypto_aead_aes256gcm_encrypt(ct.data(), &ct_len,
        reinterpret_cast<const unsigned char *>(msg.data()), msg.size(),
        nullptr, 0, nullptr, nonce, key); // no additional data

    // Decrypt — returns -1 if authentication fails.
    std::vector<unsigned char> pt(msg.size());
    unsigned long long pt_len;
    int ok = crypto_aead_aes256gcm_decrypt(pt.data(), &pt_len, nullptr,
        ct.data(), ct_len, nullptr, 0, nonce, key);
}

Setup Built in (javax.crypto).

Java
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.SecureRandom;

KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256);
SecretKey key = kg.generateKey();

byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce); // unique per message — never reuse with a key

Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, nonce)); // 128-bit tag
byte[] ciphertext = cipher.doFinal("attack at dawn".getBytes()); // tag appended

// Decrypt — doFinal throws AEADBadTagException if authentication fails.
Cipher dec = Cipher.getInstance("AES/GCM/NoPadding");
dec.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, nonce));
byte[] plaintext = dec.doFinal(ciphertext);

Setup Cargo.toml: aes-gcm = "0.10".

Rust
use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng};
use aes_gcm::Aes256Gcm;

fn main() {
    let key = Aes256Gcm::generate_key(&mut OsRng);
    let cipher = Aes256Gcm::new(&key);

    // 96-bit nonce, random per message. Never reuse a nonce with the same key;
    // store it alongside the ciphertext so you can decrypt later.
    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);

    let ciphertext = cipher
        .encrypt(&nonce, b"attack at dawn".as_ref())
        .expect("encryption failure");

    // Decrypt — returns Err if the tag fails to verify.
    let plaintext = cipher.decrypt(&nonce, ciphertext.as_ref()).expect("auth failed");
    assert_eq!(&plaintext, b"attack at dawn");
}