The explicit crypto API
You are building a secure note app. You need to encrypt user data before saving it to the database. You have used cryptography in Python or crypto in Node, and you expect a simple encrypt(key, data) function that handles everything. Rust gives you crates, traits, nonces, and authentication tags. The API looks different, and for good reason. Rust's crypto ecosystem separates the raw cipher from the mode of operation and forces you to handle randomness explicitly. This prevents the most common encryption mistakes. You cannot accidentally reuse a nonce if the API requires you to generate one. You cannot use a weak key if the type system enforces the length.
AES and GCM in plain words
AES is the algorithm. It is the mathematical function that scrambles bytes. AES-256 means you use a 256-bit key. That is the standard for new applications. GCM stands for Galois/Counter Mode. AES alone is a block cipher. It encrypts 16 bytes at a time. If you encrypt the same block twice with the same key, you get the same output. That leaks information. Patterns in the plaintext become visible in the ciphertext.
GCM is a mode of operation. It turns the block cipher into a stream cipher and adds authentication. Authentication means the receiver can verify the data has not been tampered with. If a bit flips in transit, decryption fails instead of returning garbage. You always want authenticated encryption. Never use raw AES-CBC or AES-ECB. GCM gives you confidentiality and integrity in one shot.
The nonce is a number used once. It ensures that encrypting the same message twice produces different ciphertexts. The nonce does not need to be secret, but it must be unique for every encryption with a given key. Reusing a nonce breaks security completely. The aes-gcm crate requires you to provide the nonce. This forces you to think about uniqueness.
GCM gives you confidentiality and integrity. Raw AES gives you neither. Pick the mode that protects you.
Minimal example
The aes-gcm crate provides a safe, high-level API. It re-exports the aes crate, so you only need one dependency. Add aes-gcm = "0.10" to your Cargo.toml.
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::{Aead, OsRng}};
/// Encrypts and decrypts a message using AES-256-GCM.
/// Demonstrates key generation, nonce generation, and the encrypt/decrypt cycle.
fn main() {
// Generate a random 32-byte key using the OS CSPRNG.
// In production, load the key from a secure store, not generate it randomly.
let key = Aes256Gcm::generate_key(OsRng);
// Create the cipher instance.
// The KeyInit trait ensures the key length matches the cipher type.
let cipher = Aes256Gcm::new(&key);
// Generate a random 12-byte nonce.
// GCM requires a 12-byte nonce for optimal security.
// OsRng provides cryptographically secure randomness.
let nonce = Nonce::generate(OsRng);
let plaintext = b"secret message";
// Encrypt the data.
// The output includes the ciphertext and the authentication tag.
// The tag is appended to the ciphertext automatically.
let ciphertext = cipher.encrypt(&nonce, plaintext.as_ref()).unwrap();
// Decrypt the data.
// The function verifies the authentication tag first.
// If the tag is invalid, decryption returns an error.
let decrypted = cipher.decrypt(&nonce, ciphertext.as_ref()).unwrap();
assert_eq!(decrypted, plaintext);
}
Convention aside: aes-gcm re-exports aes. You depend on aes-gcm in Cargo.toml. You do not need to add aes separately. The crate authors align the versions so aes-gcm pulls in the compatible aes version automatically.
What happens under the hood
Aes256Gcm::new(&key) sets up the cipher state. It checks the key length. If you pass a 32-byte key, it works. If you pass a 16-byte key, the compiler rejects you with E0308 (mismatched types) because Aes256Gcm expects a key type that holds 32 bytes. The type system enforces the key size at compile time. This prevents runtime errors from wrong key lengths.
encrypt takes the nonce and the payload. It performs key expansion to generate round keys. It uses a counter to generate a keystream. It XORs the keystream with the plaintext to produce the ciphertext. It also computes a GHASH of the ciphertext and any associated data. The GHASH is encrypted to produce the authentication tag. The tag is appended to the ciphertext.
decrypt verifies the tag first. It recomputes the GHASH and compares it to the tag in the ciphertext. If they match, it decrypts the data. If they do not match, it returns an error. It never returns partial data. This prevents oracle attacks where an attacker learns information from error messages or partial decryptions.
The Aead trait stands for Authenticated Encryption with Associated Data. The trait name tells you exactly what it does. It provides encrypt and decrypt methods that handle authentication automatically.
Trust the trait name. Aead means you get encryption and authentication together.
Realistic example with associated data
Real applications often have metadata that should not be encrypted but must be authenticated. For example, you might encrypt a message but want to ensure the user ID or timestamp has not been modified. Associated data allows you to authenticate data without encrypting it.
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::{Aead, Payload, OsRng}};
/// Encrypts data while authenticating metadata.
/// The metadata is included in the authentication calculation but remains visible.
fn encrypt_with_metadata(
key: &[u8; 32],
nonce: &[u8; 12],
aad: &[u8],
plaintext: &[u8],
) -> Result<Vec<u8>, aes_gcm::Error> {
// Create the cipher from the key slice.
// new_from_slice returns a Result, allowing error handling for invalid keys.
let cipher = Aes256Gcm::new_from_slice(key)?;
// Wrap the plaintext and AAD in a Payload struct.
// The AAD is authenticated but not encrypted.
// If the AAD changes, decryption will fail.
let payload = Payload { aad, msg: plaintext };
// Encrypt the payload.
// The nonce is converted into the Nonce type via Into.
cipher.encrypt(nonce.into(), payload)
}
/// Decrypts data and verifies the associated metadata.
fn decrypt_with_metadata(
key: &[u8; 32],
nonce: &[u8; 12],
aad: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, aes_gcm::Error> {
let cipher = Aes256Gcm::new_from_slice(key)?;
// Decrypt using the same AAD used during encryption.
// Mismatched AAD causes authentication failure.
let payload = Payload { aad, msg: ciphertext };
cipher.decrypt(nonce.into(), payload)
}
Associated data is useful for database records where you store the nonce and ciphertext in one column and the user ID in another. You can authenticate the user ID to ensure the ciphertext belongs to that user. If an attacker swaps ciphertexts between users, decryption fails because the AAD does not match.
Use AAD to bind ciphertext to context. It prevents ciphertext shuffling attacks.
Pitfalls and compiler errors
Nonce reuse destroys security. If you encrypt two messages with the same key and nonce, the keystream repeats. An attacker can XOR the two ciphertexts to recover the XOR of the two plaintexts. This leaks information even without knowing the key. The aes-gcm crate does not track nonces. It does not prevent you from reusing a nonce. You must manage uniqueness yourself. Use OsRng to generate nonces. Never use a counter unless you have a robust persistence mechanism that survives restarts.
Hardcoding keys is a common mistake. The minimal example generates a random key for demonstration. In production, load keys from a secure key store or environment variable. Never commit keys to version control.
Using the wrong key size triggers compiler errors. If you try to use Aes128Gcm with a 32-byte key, you get E0308 (mismatched types). The compiler expects a [u8; 16] key for Aes128Gcm. If you use new_from_slice, the function returns an error at runtime for the wrong length. Prefer new with fixed-size arrays when possible to catch errors at compile time.
If you forget to implement Aead for a custom type, you get E0277 (trait bound not satisfied). The aes-gcm crate provides the implementation for Aes256Gcm. You do not need to implement it yourself.
Nonce reuse is a silent killer. Generate fresh nonces every time.
When to use AES-GCM vs alternatives
Use aes-gcm when you need standard AES-256-GCM with a clean Rust API and you want control over nonces and associated data. Use ring when you prefer a library that restricts algorithm choices to prevent configuration errors and you want high assurance from a security-focused maintainer. Use chacha20poly1305 when you are deploying to embedded devices or older hardware that lacks AES-NI instructions and you need fast software encryption. Use openssl bindings only when you are wrapping a C library that mandates the OpenSSL API and you cannot switch to a pure Rust alternative.
Convention aside: cargo fmt formats every file the same way. Do not argue style in crypto code. Argue logic. The community expects aes-gcm for most applications. ring is the choice for high-assurance scenarios where you want to minimize configuration surface.
Pick the crate that matches your threat model. aes-gcm is the safe default for most apps.