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).
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.
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).
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).
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.
#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).
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".
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");
}