use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit},
};
use anyhow::{Result, anyhow};
use argon2::{Algorithm, Argon2, Params, Version};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use log::trace;
use rand::RngCore;
const ARGON2_MEMORY_COST: u32 = 65536; const ARGON2_TIME_COST: u32 = 3;
const ARGON2_PARALLELISM: u32 = 1;
const SALT_LENGTH: usize = 16;
const NONCE_LENGTH: usize = 12;
const KEY_LENGTH: usize = 32;
#[derive(Debug, Clone, serde::Serialize)]
pub struct EncryptedContent {
pub ciphertext: String,
pub salt: String,
pub nonce: String,
}
fn derive_key(password: &str, salt: &[u8]) -> Result<[u8; KEY_LENGTH]> {
let params = Params::new(
ARGON2_MEMORY_COST,
ARGON2_TIME_COST,
ARGON2_PARALLELISM,
Some(KEY_LENGTH),
)
.map_err(|e| anyhow!("Failed to create Argon2 params: {}", e))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut key = [0u8; KEY_LENGTH];
argon2
.hash_password_into(password.as_bytes(), salt, &mut key)
.map_err(|e| anyhow!("Failed to derive key: {}", e))?;
Ok(key)
}
pub fn encrypt_content(content: &str, password: &str) -> Result<EncryptedContent> {
trace!("Encrypting content ({} bytes)", content.len());
let mut salt = [0u8; SALT_LENGTH];
let mut nonce_bytes = [0u8; NONCE_LENGTH];
rand::thread_rng().fill_bytes(&mut salt);
rand::thread_rng().fill_bytes(&mut nonce_bytes);
trace!("Deriving key with Argon2id");
let key = derive_key(password, &salt)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, content.as_bytes())
.map_err(|e| anyhow!("Encryption failed: {}", e))?;
trace!("Content encrypted successfully");
Ok(EncryptedContent {
ciphertext: BASE64.encode(&ciphertext),
salt: BASE64.encode(salt),
nonce: BASE64.encode(nonce_bytes),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_content() {
let content = "This is a secret message!";
let password = "test-password-123";
let encrypted = encrypt_content(content, password).unwrap();
assert!(!encrypted.ciphertext.is_empty());
assert!(!encrypted.salt.is_empty());
assert!(!encrypted.nonce.is_empty());
let ciphertext_bytes = BASE64.decode(&encrypted.ciphertext).unwrap();
let salt_bytes = BASE64.decode(&encrypted.salt).unwrap();
let nonce_bytes = BASE64.decode(&encrypted.nonce).unwrap();
assert!(ciphertext_bytes.len() > content.len());
assert_eq!(salt_bytes.len(), SALT_LENGTH);
assert_eq!(nonce_bytes.len(), NONCE_LENGTH);
}
#[test]
fn test_encrypt_decrypt_roundtrip() {
let content = "Secret content for roundtrip test!";
let password = "roundtrip-password";
let encrypted = encrypt_content(content, password).unwrap();
let salt = BASE64.decode(&encrypted.salt).unwrap();
let nonce_bytes = BASE64.decode(&encrypted.nonce).unwrap();
let ciphertext = BASE64.decode(&encrypted.ciphertext).unwrap();
let key = derive_key(password, &salt).unwrap();
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
let nonce = Nonce::from_slice(&nonce_bytes);
let decrypted = cipher.decrypt(nonce, ciphertext.as_ref()).unwrap();
let decrypted_str = String::from_utf8(decrypted).unwrap();
assert_eq!(decrypted_str, content);
}
#[test]
fn test_wrong_password_fails() {
let content = "Secret content";
let password = "correct-password";
let wrong_password = "wrong-password";
let encrypted = encrypt_content(content, password).unwrap();
let salt = BASE64.decode(&encrypted.salt).unwrap();
let nonce_bytes = BASE64.decode(&encrypted.nonce).unwrap();
let ciphertext = BASE64.decode(&encrypted.ciphertext).unwrap();
let key = derive_key(wrong_password, &salt).unwrap();
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
let nonce = Nonce::from_slice(&nonce_bytes);
assert!(cipher.decrypt(nonce, ciphertext.as_ref()).is_err());
}
}