lazytables/security/
password.rs1use aes_gcm::{
4 aead::{Aead, KeyInit, OsRng},
5 Aes256Gcm, Key, Nonce,
6};
7use argon2::{password_hash::rand_core::RngCore, Argon2};
8use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
9use serde::{Deserialize, Serialize};
10use std::env;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum PasswordSource {
15 Environment {
17 var_name: String,
19 },
20 Encrypted(EncryptedPassword),
22 PlainText(String),
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct EncryptedPassword {
29 pub ciphertext: String,
31 pub nonce: String,
33 pub salt: String,
35 pub hint: Option<String>,
37}
38
39pub struct PasswordManager;
41
42impl PasswordManager {
43 fn derive_key(password: &str, salt: &[u8]) -> Result<Key<Aes256Gcm>, String> {
45 let argon2 = Argon2::default();
46 let mut key_bytes = [0u8; 32];
47
48 argon2
50 .hash_password_into(password.as_bytes(), salt, &mut key_bytes)
51 .map_err(|e| format!("Failed to derive key: {e}"))?;
52
53 Ok(*Key::<Aes256Gcm>::from_slice(&key_bytes))
54 }
55
56 pub fn encrypt_password(
58 plaintext: &str,
59 encryption_key: &str,
60 hint: Option<String>,
61 ) -> Result<EncryptedPassword, String> {
62 let mut salt = [0u8; 16];
64 OsRng.fill_bytes(&mut salt);
65
66 let key = Self::derive_key(encryption_key, &salt)?;
68
69 let cipher = Aes256Gcm::new(&key);
71
72 let mut nonce_bytes = [0u8; 12];
74 OsRng.fill_bytes(&mut nonce_bytes);
75 let nonce = Nonce::from_slice(&nonce_bytes);
76
77 let ciphertext = cipher
79 .encrypt(nonce, plaintext.as_bytes())
80 .map_err(|e| format!("Encryption failed: {e}"))?;
81
82 Ok(EncryptedPassword {
83 ciphertext: BASE64.encode(&ciphertext),
84 nonce: BASE64.encode(nonce_bytes),
85 salt: BASE64.encode(salt),
86 hint,
87 })
88 }
89
90 pub fn decrypt_password(
92 encrypted: &EncryptedPassword,
93 encryption_key: &str,
94 ) -> Result<String, String> {
95 let ciphertext = BASE64
97 .decode(&encrypted.ciphertext)
98 .map_err(|e| format!("Invalid ciphertext: {e}"))?;
99 let nonce_bytes = BASE64
100 .decode(&encrypted.nonce)
101 .map_err(|e| format!("Invalid nonce: {e}"))?;
102 let salt = BASE64
103 .decode(&encrypted.salt)
104 .map_err(|e| format!("Invalid salt: {e}"))?;
105
106 let key = Self::derive_key(encryption_key, &salt)?;
108
109 let cipher = Aes256Gcm::new(&key);
111
112 let nonce = Nonce::from_slice(&nonce_bytes);
114
115 let plaintext = cipher
117 .decrypt(nonce, ciphertext.as_ref())
118 .map_err(|_| "Decryption failed - incorrect encryption key")?;
119
120 String::from_utf8(plaintext)
121 .map_err(|e| format!("Invalid UTF-8 in decrypted password: {e}"))
122 }
123
124 pub fn resolve_password(
126 source: &PasswordSource,
127 encryption_key: Option<&str>,
128 ) -> Result<String, String> {
129 match source {
130 PasswordSource::Environment { var_name } => env::var(var_name)
131 .map_err(|_| format!("Environment variable '{var_name}' not found")),
132 PasswordSource::Encrypted(encrypted) => {
133 let key = encryption_key.ok_or("Encryption key required for encrypted password")?;
134 Self::decrypt_password(encrypted, key)
135 }
136 PasswordSource::PlainText(password) => Ok(password.clone()),
137 }
138 }
139
140 pub fn from_environment(var_name: String) -> PasswordSource {
142 PasswordSource::Environment { var_name }
143 }
144
145 pub fn create_encrypted(
147 password: &str,
148 encryption_key: &str,
149 hint: Option<String>,
150 ) -> Result<PasswordSource, String> {
151 let encrypted = Self::encrypt_password(password, encryption_key, hint)?;
152 Ok(PasswordSource::Encrypted(encrypted))
153 }
154
155 pub fn requires_encryption_key(source: &PasswordSource) -> bool {
157 matches!(source, PasswordSource::Encrypted(_))
158 }
159
160 pub fn get_hint(source: &PasswordSource) -> Option<String> {
162 match source {
163 PasswordSource::Encrypted(encrypted) => encrypted.hint.clone(),
164 _ => None,
165 }
166 }
167
168 pub fn migrate_to_encrypted(
170 plaintext: &str,
171 encryption_key: &str,
172 hint: Option<String>,
173 ) -> Result<PasswordSource, String> {
174 Self::create_encrypted(plaintext, encryption_key, hint)
175 }
176}
177
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_encrypt_decrypt() {
185 let password = "my_database_password";
186 let encryption_key = "my_secret_key_123";
187 let hint = Some("Test hint".to_string());
188
189 let encrypted = PasswordManager::encrypt_password(password, encryption_key, hint.clone())
191 .expect("Encryption should succeed");
192
193 assert_eq!(encrypted.hint, hint);
195
196 let decrypted = PasswordManager::decrypt_password(&encrypted, encryption_key)
198 .expect("Decryption should succeed");
199
200 assert_eq!(decrypted, password);
201 }
202
203 #[test]
204 fn test_wrong_key_fails() {
205 let password = "my_database_password";
206 let encryption_key = "my_secret_key_123";
207 let wrong_key = "wrong_key";
208
209 let encrypted = PasswordManager::encrypt_password(password, encryption_key, None)
211 .expect("Encryption should succeed");
212
213 let result = PasswordManager::decrypt_password(&encrypted, wrong_key);
215
216 assert!(result.is_err());
217 assert!(result.unwrap_err().contains("Decryption failed"));
218 }
219
220 #[test]
221 fn test_environment_variable() {
222 let var_name = "TEST_DB_PASSWORD";
223 let password = "test_password";
224
225 env::set_var(var_name, password);
227
228 let source = PasswordManager::from_environment(var_name.to_string());
230
231 let resolved = PasswordManager::resolve_password(&source, None)
233 .expect("Should resolve from environment");
234
235 assert_eq!(resolved, password);
236
237 env::remove_var(var_name);
239 }
240
241 #[test]
242 fn test_password_source_serialization() {
243 let password = "test";
244 let key = "key";
245
246 let source = PasswordManager::create_encrypted(password, key, Some("hint".to_string()))
247 .expect("Should create encrypted source");
248
249 let json = serde_json::to_string(&source).expect("Should serialize");
251
252 let deserialized: PasswordSource = serde_json::from_str(&json).expect("Should deserialize");
254
255 let resolved = PasswordManager::resolve_password(&deserialized, Some(key))
257 .expect("Should resolve password");
258
259 assert_eq!(resolved, password);
260 }
261}