Home › Security › Crypto primitives
algorithm low

AEAD (AES-GCM, ChaCha20-Poly1305)

Problem Developers encrypting data at rest, tokens, cookies or messages need encryption that also detects tampering, so attackers cannot flip bits, truncate or splice ciphertexts unnoticed.

Authenticated encryption that encrypts and detects tampering in one call; the only kind of symmetric encryption you should use.

O(n) per message; AES-GCM order of 1 cycle/byte with AES-NI, ChaCha20-Poly1305 order of 1-3 cycles/byte with SIMD
encryptionaeadaes-gcmchacha20-poly1305symmetric
Overview

Encryption without authentication lets attackers flip bits, truncate or splice ciphertexts. An AEAD (Authenticated Encryption with Associated Data) seals plaintext plus unencrypted "associated data" under a key and a nonce, producing ciphertext and a 16-byte tag; decryption returns either the exact plaintext or an error. The one rule: never reuse a (key, nonce) pair. The same primitive protects cloud storage and key management, VPN and IPsec links, and government-grade profiles.

Key facts

Use when

  • Encrypting data at rest, tokens, cookies, messages, file chunks.
  • Cloud storage and key management: AWS KMS encrypts customer data and keys with 256-bit AES-GCM (AWS); Azure Storage encrypts objects server-side with AES-GCM (Microsoft).
  • Network links and VPNs: IPsec ESP with GCM (RFC 4106); WireGuard uses ChaCha20-Poly1305 (protocol).
  • Government and defence profiles: the CNSA suite names AES-256 in GCM (RFC 9151).
  • You control both ends and need a symmetric primitive.

Avoid when

  • You need transport security: use TLS 1.3, not a hand-built channel.
  • You need public verifiability: use signatures.

Deep dive

How it works

In: 32-byte key, 12- or 24-byte nonce, plaintext, optional associated data · Out: ciphertext plus 16-byte tag (on open: the plaintext or an error) · State: none; the caller must never repeat a nonce under one key · Cost: O(n), order of 1-3 cycles/byte

  1. Encrypt: a stream cipher (AES in counter mode, or ChaCha20) turns (key, nonce, counter) into a keystream; ciphertext = plaintext XOR keystream.
  2. Authenticate: a one-time polynomial MAC (GHASH for GCM, Poly1305 for ChaCha) runs over AAD || ciphertext || lengths, keyed from the same key/nonce. The result is the tag.
  3. Decrypt: recompute the tag, compare in constant time, and only then release plaintext.
key, nonce ──► keystream ──XOR── plaintext ──► ciphertext ─┐
                  └─► MAC key ── MAC(AAD, ciphertext) ─────┴─► tag (16 B)
wire format: nonce (12|24 B) || ciphertext || tag

Why nonce reuse is fatal: two messages under the same (key, nonce) share a keystream, so XOR of ciphertexts = XOR of plaintexts. For GCM it also leaks the GHASH key H, which lets an attacker forge tags for any message under that key.

Code

Rust, RustCrypto crates (chacha20poly1305 = "0.10", aes-gcm = "0.10").

use chacha20poly1305::{
    aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
    Key, XChaCha20Poly1305, XNonce,
};

const NONCE_LEN: usize = 24;
const TAG_LEN: usize = 16;

/// Returns nonce || ciphertext || tag. `aad` is authenticated but not encrypted
/// (bind it to context: record id, user id, version) so ciphertexts can't be swapped.
pub fn seal(key: &[u8; 32], plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
    let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
    let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); // 192-bit random: collision-safe
    let ct = cipher
        .encrypt(&nonce, Payload { msg: plaintext, aad })
        .expect("only fails on absurd lengths");
    let mut out = Vec::with_capacity(NONCE_LEN + ct.len());
    out.extend_from_slice(&nonce);
    out.extend_from_slice(&ct);
    out
}

pub fn open(key: &[u8; 32], sealed: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
    if sealed.len() < NONCE_LEN + TAG_LEN {
        return None;
    }
    let (nonce, ct) = sealed.split_at(NONCE_LEN);
    let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
    cipher.decrypt(XNonce::from_slice(nonce), Payload { msg: ct, aad }).ok()
}

// High-volume channel with AES-256-GCM: counter nonces, one key per direction.
use aes_gcm::Aes256Gcm;

pub struct Sealer {
    cipher: Aes256Gcm,
    counter: u64,
}

impl Sealer {
    pub fn new(key: &[u8; 32]) -> Self {
        Self { cipher: Aes256Gcm::new(aes_gcm::Key::<Aes256Gcm>::from_slice(key)), counter: 0 }
    }
    pub fn seal(&mut self, plaintext: &[u8]) -> (u64, Vec<u8>) {
        let n = self.counter;
        self.counter = n.checked_add(1).expect("rekey before the counter wraps");
        let mut nonce = [0u8; 12];
        nonce[4..].copy_from_slice(&n.to_be_bytes()); // never repeats under this key
        let ct = self.cipher.encrypt(aes_gcm::Nonce::from_slice(&nonce), plaintext).unwrap();
        (n, ct) // receiver rejects n <= last seen to stop replays
    }
}

Complexity and constants

  • AES-GCM with AES-NI + PCLMULQDQ (or VAES on recent x86): order of several GB/s per core. Without hardware AES, software AES is slow and often not constant-time.
  • ChaCha20-Poly1305: order of 1-3 GB/s per core with AVX2; the faster choice on low-end ARM without crypto extensions.
  • Overhead per message: 12 or 24 B nonce + 16 B tag.
  • Limits: AES-GCM max plaintext per message about 64 GiB; with random 96-bit nonces, at most 2^32 messages per key (NIST SP 800-38D). ChaCha20-Poly1305 (IETF) max about 256 GiB per message.

Efficiency

  • Cost: order of 1 cycle/byte (AES-GCM with AES-NI + PCLMULQDQ) to 1-3 cycles/byte (ChaCha20-Poly1305 with SIMD); 28-40 B nonce+tag overhead per message. Bound: compute on large messages, per-call overhead on small ones.
  • Levers:
    • Match the CPU — AES-GCM with AES-NI/ARMv8 crypto, ChaCha20-Poly1305 without (RustCrypto crates use AES-NI/CLMUL and AVX2 when present, per their READMEs) — every deployment.
    • Reuse the cipher per key and seal in place — saves key schedule and copies (order of) — many small messages.
    • Pack records into one message or 64 KiB chunks — overhead amortized — small records.
    • Counter nonces — no CSPRNG call per message — single writer per key.
  • Naive → standard → state of the art:
    • Naive · unauthenticated CBC/CTR plus separate MAC, new cipher object per message
    • Standard · one AEAD call, cached cipher, hardware-matched algorithm, order of GB/s per core
    • State of the art · AEGIS on AES hardware (faster than GCM, see Variants); or let TLS 1.3 do it

Implementation notes

  • Key: 32 random bytes from the OS CSPRNG, or derived with HKDF from a master key (see hashes). Never a password directly; use Argon2id first.
  • Nonce strategy: random with XChaCha; counter with AES-GCM/ChaCha20 when one writer owns the key. With multiple writers and 12-byte nonces, derive a per-writer subkey.
  • Large files: split into chunks (64 KiB is a common size), each sealed with nonce = prefix || chunk index and a "last chunk" flag in the AAD, so reordering and truncation fail. libsodium secretstream does this for you.
  • Envelope encryption: data key encrypts data; a KMS key encrypts the data key. Rotate the KMS key without re-encrypting data.
  • Version byte in front of the ciphertext lets you change algorithms later.

Pitfalls

  • Nonce reuse: random 12-byte nonces in a long-lived key, counters reset after restart, same key in two processes.
  • Using plaintext before the tag is checked (streaming decrypt that writes output early).
  • Comparing tags with == in hand-rolled code; libraries do constant-time compare, so use them.
  • Assuming key commitment: GCM and Poly1305 ciphertexts can be crafted to decrypt validly under two keys ("partitioning oracle" attacks on password-based or multi-key schemes). Add a key commitment (e.g., HMAC of the key in the header) if a ciphertext may be tried under several keys.
  • Using unauthenticated modes (CBC, CTR) or ECB at all.
  • Truncating tags to save bytes.

Variants and alternatives

AES-GCM-SIV (RFC 8452)nonce reuse only reveals equal plaintexts; pick when nonce uniqueness can't be guaranteed.
XChaCha20-Poly1305random nonces, simplest safe default.
AEGIS-128L/256faster than GCM with AES hardware; in libsodium, still an IETF draft.
Deterministic AEAD (AES-SIV, RFC 5297)key wrapping, deduplicating encryption.
Google Tink / libsodiumhigher-level APIs that pick nonces and formats for you; prefer them in application code.

In production

  • Cloud: AWS KMS (256-bit AES-GCM, docs); Azure Storage service-side encryption (AES-GCM, docs); envelope encryption in GCP KMS.
  • Networking: IPsec ESP (RFC 4106), WireGuard (protocol), TLS 1.3 and QUIC (RFC 9001).
  • Government: CNSA profile (RFC 9151).
  • Tools: OpenSSH (chacha20-poly1305@openssh.com, aes256-gcm), age (ChaCha20-Poly1305).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
aes-gcm, chacha20poly1305RustRustCrypto/AEADscargo add aes-gcm chacha20poly1305————pure Rust, constant time; each crate's README states one NCC Group audit; XChaCha20 included
aws-lc-rsRustaws/aws-lc-rscargo add aws-lc-rs————ring-style API over AWS-LC; FIPS option via aws-lc-fips-sys
ringRustbriansmith/ringcargo add ring————AES-GCM, ChaCha20-Poly1305; much C/asm from BoringSSL
libsodiumCjedisct1/libsodiumbrew install libsodium————XChaCha20-Poly1305, AES-256-GCM, secretstream; bindings for most languages
TinkJavatink-crypto/tink-javaMaven com.google.crypto.tink:tink————high-level API that manages nonces and key formats; also tink-py (pip install tink)

Sources

  • Dworkin, NIST SP 800-38D: Recommendation for Block Cipher Modes of Operation: GCM and GMAC, 2007. spec
  • Nir, Langley, RFC 8439: ChaCha20 and Poly1305 for IETF Protocols, 2018. RFC
  • Gueron, Langley, Lindell, RFC 8452: AES-GCM-SIV, 2019. RFC
  • Böck et al., Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in TLS, USENIX WOOT 2016. paper
  • Len, Grubbs, Ristenpart, Partitioning Oracle Attacks, USENIX Security 2021. paper
  • Harkins, RFC 5297: Synthetic Initialization Vector (SIV) Authenticated Encryption Using AES, 2008. RFC

Builds on

constant time code · plannedWriting secret-dependent code without branches or memory lookups on secrets; constant-time compare and select
csprng · plannedOS-backed cryptographically secure random numbers (getrandom), and when a fast PRNG is not enough

Used by

double ratchet · plannedSignal Double Ratchet: DH and symmetric ratchets for forward secrecy
Envelope encryptionServices storing sensitive data must encrypt it at rest, rotate keys and delete data reliably, without sending every byte through a key management service.
group messaging encryption · plannedSender keys and MLS TreeKEM for scalable end-to-end group encryption
mix networks · plannedVerifiable re-encryption shuffles that unlink inputs from outputs
onion routing · plannedLayered encryption through relay circuits: telescoping handshakes (ntor), fixed cells, stream multiplexing
QUICApplications on slow, lossy or mobile networks need a connection that starts quickly, survives a change of network, and does not let one lost packet stall unrelated data, all while staying encrypted.Secrets managementAny system with database passwords, API keys, signing keys or tokens must keep them out of code, logs and images and rotate them safely. This guide answers where secrets live and how workloads obtain them.TLS 1.3Any network service must keep traffic between clients, or between internal services, private, tamper-proof and authenticated to the right server, without hand-building a secure channel.
traffic obfuscation · plannedMaking tunneled traffic look like something else (obfs4, domain fronting, meek) to evade blocking

Systems (45):

Alternatives and related