Skip to main content

lazytables/security/
password.rs

1// FilePath: src/security/password.rs
2
3use 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/// Password source - environment variable or encrypted storage
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum PasswordSource {
15    /// Password comes from environment variable
16    Environment {
17        /// Name of the environment variable
18        var_name: String,
19    },
20    /// Password is stored encrypted
21    Encrypted(EncryptedPassword),
22    /// Password is stored in plain text (deprecated, for migration only)
23    PlainText(String),
24}
25
26/// Encrypted password storage
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct EncryptedPassword {
29    /// Encrypted password data (base64 encoded)
30    pub ciphertext: String,
31    /// Nonce used for encryption (base64 encoded)
32    pub nonce: String,
33    /// Salt used for key derivation (base64 encoded)
34    pub salt: String,
35    /// Hint for the encryption key (optional)
36    pub hint: Option<String>,
37}
38
39/// Password manager for secure password handling
40pub struct PasswordManager;
41
42impl PasswordManager {
43    /// Derive encryption key from user password using Argon2
44    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        // Use Argon2 for key derivation
49        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    /// Encrypt a password with a user-provided encryption key
57    pub fn encrypt_password(
58        plaintext: &str,
59        encryption_key: &str,
60        hint: Option<String>,
61    ) -> Result<EncryptedPassword, String> {
62        // Generate random salt for key derivation
63        let mut salt = [0u8; 16];
64        OsRng.fill_bytes(&mut salt);
65
66        // Derive key from user password
67        let key = Self::derive_key(encryption_key, &salt)?;
68
69        // Create cipher
70        let cipher = Aes256Gcm::new(&key);
71
72        // Generate random nonce
73        let mut nonce_bytes = [0u8; 12];
74        OsRng.fill_bytes(&mut nonce_bytes);
75        let nonce = Nonce::from_slice(&nonce_bytes);
76
77        // Encrypt the password
78        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    /// Decrypt a password with a user-provided encryption key
91    pub fn decrypt_password(
92        encrypted: &EncryptedPassword,
93        encryption_key: &str,
94    ) -> Result<String, String> {
95        // Decode base64 values
96        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        // Derive key from user password
107        let key = Self::derive_key(encryption_key, &salt)?;
108
109        // Create cipher
110        let cipher = Aes256Gcm::new(&key);
111
112        // Create nonce
113        let nonce = Nonce::from_slice(&nonce_bytes);
114
115        // Decrypt the password
116        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    /// Resolve a password from its source
125    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    /// Create a password source from environment variable
141    pub fn from_environment(var_name: String) -> PasswordSource {
142        PasswordSource::Environment { var_name }
143    }
144
145    /// Create an encrypted password source
146    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    /// Check if a password source requires an encryption key
156    pub fn requires_encryption_key(source: &PasswordSource) -> bool {
157        matches!(source, PasswordSource::Encrypted(_))
158    }
159
160    /// Get hint for encrypted password
161    pub fn get_hint(source: &PasswordSource) -> Option<String> {
162        match source {
163            PasswordSource::Encrypted(encrypted) => encrypted.hint.clone(),
164            _ => None,
165        }
166    }
167
168    /// Migrate plain text password to encrypted
169    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        // Encrypt password
190        let encrypted = PasswordManager::encrypt_password(password, encryption_key, hint.clone())
191            .expect("Encryption should succeed");
192
193        // Verify hint is preserved
194        assert_eq!(encrypted.hint, hint);
195
196        // Decrypt password
197        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        // Encrypt password
210        let encrypted = PasswordManager::encrypt_password(password, encryption_key, None)
211            .expect("Encryption should succeed");
212
213        // Try to decrypt with wrong key
214        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        // Set environment variable
226        env::set_var(var_name, password);
227
228        // Create password source from environment
229        let source = PasswordManager::from_environment(var_name.to_string());
230
231        // Resolve password
232        let resolved = PasswordManager::resolve_password(&source, None)
233            .expect("Should resolve from environment");
234
235        assert_eq!(resolved, password);
236
237        // Clean up
238        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        // Serialize to JSON
250        let json = serde_json::to_string(&source).expect("Should serialize");
251
252        // Deserialize from JSON
253        let deserialized: PasswordSource = serde_json::from_str(&json).expect("Should deserialize");
254
255        // Verify it still works
256        let resolved = PasswordManager::resolve_password(&deserialized, Some(key))
257            .expect("Should resolve password");
258
259        assert_eq!(resolved, password);
260    }
261}