1use aes_gcm::{
4 Aes256Gcm, Key, Nonce,
5 aead::{Aead, KeyInit},
6};
7use anyhow::{Result, anyhow};
8use argon2::{Algorithm, Argon2, Params, Version};
9use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
10use log::trace;
11use rand::RngCore;
12
13const ARGON2_MEMORY_COST: u32 = 65536; const ARGON2_TIME_COST: u32 = 3;
16const ARGON2_PARALLELISM: u32 = 1;
17const SALT_LENGTH: usize = 16;
18const NONCE_LENGTH: usize = 12;
19const KEY_LENGTH: usize = 32; #[derive(Debug, Clone, serde::Serialize)]
23pub struct EncryptedContent {
24 pub ciphertext: String,
26 pub salt: String,
28 pub nonce: String,
30}
31
32fn derive_key(password: &str, salt: &[u8]) -> Result<[u8; KEY_LENGTH]> {
34 let params = Params::new(
35 ARGON2_MEMORY_COST,
36 ARGON2_TIME_COST,
37 ARGON2_PARALLELISM,
38 Some(KEY_LENGTH),
39 )
40 .map_err(|e| anyhow!("Failed to create Argon2 params: {}", e))?;
41
42 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
43
44 let mut key = [0u8; KEY_LENGTH];
45 argon2
46 .hash_password_into(password.as_bytes(), salt, &mut key)
47 .map_err(|e| anyhow!("Failed to derive key: {}", e))?;
48
49 Ok(key)
50}
51
52pub fn encrypt_content(content: &str, password: &str) -> Result<EncryptedContent> {
54 trace!("Encrypting content ({} bytes)", content.len());
55
56 let mut salt = [0u8; SALT_LENGTH];
58 let mut nonce_bytes = [0u8; NONCE_LENGTH];
59 rand::thread_rng().fill_bytes(&mut salt);
60 rand::thread_rng().fill_bytes(&mut nonce_bytes);
61
62 trace!("Deriving key with Argon2id");
64 let key = derive_key(password, &salt)?;
65
66 let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
68 let nonce = Nonce::from_slice(&nonce_bytes);
69
70 let ciphertext = cipher
71 .encrypt(nonce, content.as_bytes())
72 .map_err(|e| anyhow!("Encryption failed: {}", e))?;
73
74 trace!("Content encrypted successfully");
75 Ok(EncryptedContent {
76 ciphertext: BASE64.encode(&ciphertext),
77 salt: BASE64.encode(salt),
78 nonce: BASE64.encode(nonce_bytes),
79 })
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn test_encrypt_content() {
88 let content = "This is a secret message!";
89 let password = "test-password-123";
90
91 let encrypted = encrypt_content(content, password).unwrap();
92
93 assert!(!encrypted.ciphertext.is_empty());
95 assert!(!encrypted.salt.is_empty());
96 assert!(!encrypted.nonce.is_empty());
97
98 let ciphertext_bytes = BASE64.decode(&encrypted.ciphertext).unwrap();
100 let salt_bytes = BASE64.decode(&encrypted.salt).unwrap();
101 let nonce_bytes = BASE64.decode(&encrypted.nonce).unwrap();
102
103 assert!(ciphertext_bytes.len() > content.len());
105 assert_eq!(salt_bytes.len(), SALT_LENGTH);
106 assert_eq!(nonce_bytes.len(), NONCE_LENGTH);
107 }
108
109 #[test]
110 fn test_encrypt_decrypt_roundtrip() {
111 let content = "Secret content for roundtrip test!";
112 let password = "roundtrip-password";
113
114 let encrypted = encrypt_content(content, password).unwrap();
115
116 let salt = BASE64.decode(&encrypted.salt).unwrap();
118 let nonce_bytes = BASE64.decode(&encrypted.nonce).unwrap();
119 let ciphertext = BASE64.decode(&encrypted.ciphertext).unwrap();
120
121 let key = derive_key(password, &salt).unwrap();
122 let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
123 let nonce = Nonce::from_slice(&nonce_bytes);
124
125 let decrypted = cipher.decrypt(nonce, ciphertext.as_ref()).unwrap();
126 let decrypted_str = String::from_utf8(decrypted).unwrap();
127
128 assert_eq!(decrypted_str, content);
129 }
130
131 #[test]
132 fn test_wrong_password_fails() {
133 let content = "Secret content";
134 let password = "correct-password";
135 let wrong_password = "wrong-password";
136
137 let encrypted = encrypt_content(content, password).unwrap();
138
139 let salt = BASE64.decode(&encrypted.salt).unwrap();
141 let nonce_bytes = BASE64.decode(&encrypted.nonce).unwrap();
142 let ciphertext = BASE64.decode(&encrypted.ciphertext).unwrap();
143
144 let key = derive_key(wrong_password, &salt).unwrap();
145 let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
146 let nonce = Nonce::from_slice(&nonce_bytes);
147
148 assert!(cipher.decrypt(nonce, ciphertext.as_ref()).is_err());
150 }
151}