DEV Community

Cover image for AES-256-GCM Encryption in Rust — Securing Local App Data
hiyoyo
hiyoyo

Posted on

AES-256-GCM Encryption in Rust — Securing Local App Data

All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.

Hiyoko PDF Vault encrypts PDFs with AES-256-GCM. Here's the implementation — the parts that aren't obvious from the docs.


Why AES-256-GCM

AES-256-GCM is authenticated encryption. It provides both confidentiality (the data is encrypted) and integrity (you know if the data was tampered with). Without authentication, encrypted data can be modified without detection.

For document encryption, use authenticated encryption. AES-256-GCM is the standard choice.


The implementation with aes-gcm

[dependencies]
aes-gcm = "0.10"
rand = "0.8"
Enter fullscreen mode Exit fullscreen mode
use aes_gcm::{
    aead::{Aead, AeadCore, KeyInit, OsRng},
    Aes256Gcm, Key, Nonce,
};

pub fn encrypt(data: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, AppError> {
    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));

    // Generate random nonce — never reuse a nonce with the same key
    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);

    let ciphertext = cipher
        .encrypt(&nonce, data)
        .map_err(|_| AppError::Encryption("Encryption failed".into()))?;

    // Prepend nonce to ciphertext — you need it for decryption
    let mut result = nonce.to_vec();
    result.extend_from_slice(&ciphertext);

    Ok(result)
}

pub fn decrypt(data: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, AppError> {
    if data.len() < 12 {
        return Err(AppError::Encryption("Data too short".into()));
    }

    let (nonce_bytes, ciphertext) = data.split_at(12);
    let nonce = Nonce::from_slice(nonce_bytes);
    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));

    cipher
        .decrypt(nonce, ciphertext)
        .map_err(|_| AppError::Encryption("Decryption failed — wrong key or corrupted data".into()))
}
Enter fullscreen mode Exit fullscreen mode

Key derivation from password

Never use a password directly as an AES key. Use Argon2id to derive a key:

argon2 = "0.5"
Enter fullscreen mode Exit fullscreen mode
use argon2::{Argon2, PasswordHasher, password_hash::SaltString};

pub fn derive_key(password: &str, salt: &[u8; 32]) -> Result<[u8; 32], AppError> {
    let argon2 = Argon2::default();
    let mut key = [0u8; 32];

    argon2
        .hash_password_into(password.as_bytes(), salt, &mut key)
        .map_err(|_| AppError::Encryption("Key derivation failed".into()))?;

    Ok(key)
}
Enter fullscreen mode Exit fullscreen mode

Store the salt with the encrypted data. The salt is not secret — it just prevents rainbow table attacks.


The nonce rule

Never reuse a nonce with the same key. With a 96-bit random nonce and OsRng, collision probability is negligible for any realistic number of encryptions. Always generate fresh nonces with a cryptographically secure RNG.


Storing encrypted PDFs

Format: [salt (32 bytes)][nonce (12 bytes)][ciphertext]

Everything needed for decryption is in the file. The password is the only secret.


TL;DR: Use aes-gcm crate for AES-256-GCM encryption — authenticated encryption that catches tampering. Derive keys from passwords with Argon2id (never use passwords directly as keys). Store format: [salt][nonce][ciphertext]. Generate a fresh nonce per encryption with OsRng.


If this was useful, a ❤️ helps more than you'd think — thanks!

Hiyoko PDF Vault | X → @hiyoyok

Top comments (0)