use aes_gcm::{
Aes256Gcm, Nonce,
aead::{Aead, KeyInit, Payload},
};
use anyhow::{Context, Result, anyhow};
use argon2::{Algorithm, Argon2, Params, Version};
use rand::{RngExt, rng};
use zeroize::Zeroizing;
const MAGIC: &[u8; 4] = b"enk1";
const SALT_SIZE: usize = 16; const NONCE_SIZE: usize = 12; const KEY_SIZE: usize = 32; const TAG_SIZE: usize = 16; const HEADER_SIZE: usize = MAGIC.len() + SALT_SIZE + NONCE_SIZE;
const ARGON2_M_COST: u32 = 256 * 1024; const ARGON2_T_COST: u32 = 13; const ARGON2_P_COST: u32 = 1;
pub fn encrypt(plaintext: &[u8], secret: &[u8]) -> Result<Vec<u8>> {
if secret.is_empty() {
anyhow::bail!("secret is empty");
}
let salt: [u8; SALT_SIZE] = rng().random();
let nonce_data: [u8; NONCE_SIZE] = rng().random();
let key = Key::from_secret(secret, &salt)?;
let nonce = Nonce::try_from(nonce_data.as_slice()).expect("nonce size is fixed");
let aad = make_aad(&salt);
let ciphertext = key.cipher.encrypt(&nonce, Payload { msg: plaintext, aad: &aad }).map_err(|e| anyhow!("encryption failed: {e}"))?;
let mut output = Vec::with_capacity(HEADER_SIZE + ciphertext.len());
output.extend_from_slice(MAGIC);
output.extend_from_slice(&salt);
output.extend_from_slice(&nonce_data);
output.extend_from_slice(&ciphertext);
Ok(output)
}
pub fn decrypt(ciphertext: &[u8], secret: &[u8]) -> Result<Vec<u8>> {
if secret.is_empty() {
anyhow::bail!("secret is empty");
}
if ciphertext.len() < HEADER_SIZE + TAG_SIZE {
anyhow::bail!("encrypted data too short");
}
let (magic, rest) = ciphertext.split_at(MAGIC.len());
if magic != MAGIC {
anyhow::bail!("unsupported encrypted data format");
}
let (salt_data, rest) = rest.split_at(SALT_SIZE);
let (nonce_data, encrypted_data) = rest.split_at(NONCE_SIZE);
let salt: &[u8; SALT_SIZE] = salt_data.try_into().expect("salt size is fixed");
let nonce = Nonce::try_from(nonce_data).expect("nonce size is fixed");
let key = Key::from_secret(secret, salt)?;
let aad = make_aad(salt);
key.cipher
.decrypt(&nonce, Payload { msg: encrypted_data, aad: &aad })
.map_err(|_| anyhow!("decryption failed: wrong password/key or corrupted data"))
}
fn make_aad(salt: &[u8; SALT_SIZE]) -> [u8; MAGIC.len() + SALT_SIZE] {
let mut aad = [0u8; MAGIC.len() + SALT_SIZE];
aad[..MAGIC.len()].copy_from_slice(MAGIC);
aad[MAGIC.len()..].copy_from_slice(salt);
aad
}
struct Key {
cipher: Aes256Gcm,
}
impl Key {
fn from_secret(secret: &[u8], salt: &[u8; SALT_SIZE]) -> Result<Self> {
let params = Params::new(ARGON2_M_COST, ARGON2_T_COST, ARGON2_P_COST, Some(KEY_SIZE)).context("invalid Argon2 parameters")?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut key = Zeroizing::new([0u8; KEY_SIZE]);
argon2.hash_password_into(secret, salt, key.as_mut()).context("key derivation failed")?;
let cipher = Aes256Gcm::new_from_slice(key.as_ref()).context("invalid AES-256 key")?;
Ok(Self { cipher })
}
}