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"
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()))
}
Key derivation from password
Never use a password directly as an AES key. Use Argon2id to derive a key:
argon2 = "0.5"
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)
}
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)