Skip to main content

reddb_server/storage/
keyring.rs

1//! Keyring integration for secure password storage
2//!
3//! Stores the database encryption password in the system keyring
4//! so users don't need to enter it every time.
5//!
6//! On Linux: Uses ~/.config/reddb/keyring (encrypted with user-specific key)
7//! On macOS: Uses Keychain when available (TBD)
8//! On Windows: Uses Credential Manager when available (TBD)
9
10use std::fs;
11use std::io::{Read, Write};
12use std::path::PathBuf;
13
14use crate::crypto::aes_gcm::{aes256_gcm_decrypt, aes256_gcm_encrypt};
15use crate::crypto::sha256::sha256;
16use crate::crypto::uuid::Uuid;
17
18const SERVICE_NAME: &str = "reddb";
19const KEYRING_FILE: &str = "keyring.enc";
20
21/// Result of password resolution
22#[derive(Debug, Clone)]
23pub enum PasswordSource {
24    /// Password from --db-password flag
25    Flag(String),
26    /// Password from REDDB_KEY environment variable
27    EnvVar(String),
28    /// Password from system keyring
29    Keyring(String),
30    /// No password configured - database will be unencrypted
31    None,
32}
33
34impl PasswordSource {
35    pub fn password(&self) -> Option<&str> {
36        match self {
37            PasswordSource::Flag(p) => Some(p),
38            PasswordSource::EnvVar(p) => Some(p),
39            PasswordSource::Keyring(p) => Some(p),
40            PasswordSource::None => None,
41        }
42    }
43
44    pub fn is_encrypted(&self) -> bool {
45        !matches!(self, PasswordSource::None)
46    }
47
48    pub fn source_name(&self) -> &'static str {
49        match self {
50            PasswordSource::Flag(_) => "flag",
51            PasswordSource::EnvVar(_) => "env",
52            PasswordSource::Keyring(_) => "keyring",
53            PasswordSource::None => "none",
54        }
55    }
56}
57
58/// Resolve password from multiple sources (priority order)
59/// 1. --db-password flag (highest priority)
60/// 2. REDDB_KEY environment variable
61/// 3. System keyring
62/// 4. None (no encryption)
63pub fn resolve_password(flag_password: Option<&str>) -> PasswordSource {
64    // Priority 1: Explicit flag
65    if let Some(pwd) = flag_password {
66        if !pwd.is_empty() {
67            return PasswordSource::Flag(pwd.to_string());
68        }
69    }
70
71    // Priority 2: Environment variable (try REDDB_KEY first, fall back to REDBLUE_DB_KEY)
72    if let Ok(pwd) = std::env::var("REDDB_KEY").or_else(|_| std::env::var("REDBLUE_DB_KEY")) {
73        if !pwd.is_empty() {
74            return PasswordSource::EnvVar(pwd);
75        }
76    }
77
78    // Priority 3: System keyring
79    if let Some(pwd) = get_from_keyring() {
80        return PasswordSource::Keyring(pwd);
81    }
82
83    // Priority 4: No password
84    PasswordSource::None
85}
86
87/// Get password from system keyring
88pub fn get_from_keyring() -> Option<String> {
89    let keyring_path = get_keyring_path()?;
90
91    if !keyring_path.exists() {
92        return None;
93    }
94
95    let mut file = fs::File::open(&keyring_path).ok()?;
96    let mut encrypted_data = Vec::new();
97    file.read_to_end(&mut encrypted_data).ok()?;
98
99    if encrypted_data.len() < 28 {
100        // Minimum: 12 (nonce) + 16 (tag)
101        return None;
102    }
103
104    let key = derive_keyring_key();
105    let nonce: [u8; 12] = encrypted_data[..12].try_into().ok()?;
106    let ciphertext_and_tag = &encrypted_data[12..];
107
108    let plaintext = aes256_gcm_decrypt(&key, &nonce, &[], ciphertext_and_tag).ok()?;
109
110    String::from_utf8(plaintext).ok()
111}
112
113/// Save password to system keyring
114pub fn save_to_keyring(password: &str) -> Result<(), String> {
115    let keyring_path = get_keyring_path().ok_or("Failed to determine keyring path")?;
116
117    // Ensure parent directory exists
118    if let Some(parent) = keyring_path.parent() {
119        fs::create_dir_all(parent)
120            .map_err(|e| format!("Failed to create keyring directory: {}", e))?;
121    }
122
123    let key = derive_keyring_key();
124
125    // Generate random nonce
126    let nonce = generate_nonce();
127
128    // Encrypt password
129    let ciphertext_and_tag = aes256_gcm_encrypt(&key, &nonce, &[], password.as_bytes());
130
131    // Write: nonce || ciphertext || tag
132    let mut data = Vec::with_capacity(12 + ciphertext_and_tag.len());
133    data.extend_from_slice(&nonce);
134    data.extend_from_slice(&ciphertext_and_tag);
135
136    let mut file = fs::File::create(&keyring_path)
137        .map_err(|e| format!("Failed to create keyring file: {}", e))?;
138    file.write_all(&data)
139        .map_err(|e| format!("Failed to write keyring: {}", e))?;
140
141    // Set restrictive permissions on Unix
142    #[cfg(unix)]
143    {
144        use std::os::unix::fs::PermissionsExt;
145        let permissions = std::fs::Permissions::from_mode(0o600);
146        fs::set_permissions(&keyring_path, permissions)
147            .map_err(|e| format!("Failed to set keyring permissions: {}", e))?;
148    }
149
150    Ok(())
151}
152
153/// Remove password from system keyring
154pub fn clear_keyring() -> Result<(), String> {
155    let keyring_path = get_keyring_path().ok_or("Failed to determine keyring path")?;
156
157    if keyring_path.exists() {
158        fs::remove_file(&keyring_path).map_err(|e| format!("Failed to remove keyring: {}", e))?;
159    }
160
161    Ok(())
162}
163
164/// Check if keyring has a stored password
165pub fn has_keyring_password() -> bool {
166    get_from_keyring().is_some()
167}
168
169/// Get keyring file path
170fn get_keyring_path() -> Option<PathBuf> {
171    // Try XDG config directory first
172    if let Ok(config_dir) = std::env::var("XDG_CONFIG_HOME") {
173        return Some(
174            PathBuf::from(config_dir)
175                .join(SERVICE_NAME)
176                .join(KEYRING_FILE),
177        );
178    }
179
180    // Fall back to ~/.config
181    if let Ok(home) = std::env::var("HOME") {
182        return Some(
183            PathBuf::from(home)
184                .join(".config")
185                .join(SERVICE_NAME)
186                .join(KEYRING_FILE),
187        );
188    }
189
190    // Windows fallback
191    if let Ok(appdata) = std::env::var("APPDATA") {
192        return Some(PathBuf::from(appdata).join(SERVICE_NAME).join(KEYRING_FILE));
193    }
194
195    None
196}
197
198/// Derive a unique key for keyring encryption based on machine/user identity
199fn derive_keyring_key() -> [u8; 32] {
200    let mut identity = String::new();
201
202    // Add hostname
203    if let Ok(hostname) = std::env::var("HOSTNAME") {
204        identity.push_str(&hostname);
205    } else if let Ok(name) = std::env::var("COMPUTERNAME") {
206        identity.push_str(&name);
207    }
208    identity.push(':');
209
210    // Add username
211    if let Ok(user) = std::env::var("USER") {
212        identity.push_str(&user);
213    } else if let Ok(user) = std::env::var("USERNAME") {
214        identity.push_str(&user);
215    }
216    identity.push(':');
217
218    // Add home directory as additional entropy
219    if let Ok(home) = std::env::var("HOME") {
220        identity.push_str(&home);
221    } else if let Ok(home) = std::env::var("USERPROFILE") {
222        identity.push_str(&home);
223    }
224
225    // Add a fixed salt for the keyring
226    identity.push_str(":reddb-keyring-v1");
227
228    sha256(identity.as_bytes())
229}
230
231/// Generate a random 12-byte nonce
232fn generate_nonce() -> [u8; 12] {
233    let uuid = Uuid::new_v4();
234    let mut nonce = [0u8; 12];
235    nonce.copy_from_slice(&uuid.as_bytes()[0..12]);
236    nonce
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use std::sync::Mutex;
243
244    // Mutex to serialize keyring tests (they modify shared state)
245    static KEYRING_TEST_LOCK: Mutex<()> = Mutex::new(());
246
247    #[test]
248    fn test_password_source_is_encrypted() {
249        assert!(PasswordSource::Flag("test".to_string()).is_encrypted());
250        assert!(PasswordSource::EnvVar("test".to_string()).is_encrypted());
251        assert!(PasswordSource::Keyring("test".to_string()).is_encrypted());
252        assert!(!PasswordSource::None.is_encrypted());
253    }
254
255    #[test]
256    fn test_password_source_name() {
257        assert_eq!(PasswordSource::Flag("".to_string()).source_name(), "flag");
258        assert_eq!(PasswordSource::EnvVar("".to_string()).source_name(), "env");
259        assert_eq!(
260            PasswordSource::Keyring("".to_string()).source_name(),
261            "keyring"
262        );
263        assert_eq!(PasswordSource::None.source_name(), "none");
264    }
265
266    #[test]
267    fn test_password_source_password() {
268        assert_eq!(
269            PasswordSource::Flag("mypass".to_string()).password(),
270            Some("mypass")
271        );
272        assert_eq!(
273            PasswordSource::EnvVar("envpass".to_string()).password(),
274            Some("envpass")
275        );
276        assert_eq!(
277            PasswordSource::Keyring("ringpass".to_string()).password(),
278            Some("ringpass")
279        );
280        assert_eq!(PasswordSource::None.password(), None);
281    }
282
283    #[test]
284    fn test_derive_keyring_key_deterministic() {
285        let key1 = derive_keyring_key();
286        let key2 = derive_keyring_key();
287        assert_eq!(key1, key2);
288        assert_eq!(key1.len(), 32);
289    }
290
291    #[test]
292    fn test_derive_keyring_key_length() {
293        let key = derive_keyring_key();
294        assert_eq!(key.len(), 32); // AES-256 key
295    }
296
297    #[test]
298    fn test_generate_nonce_uniqueness() {
299        let nonce1 = generate_nonce();
300        let nonce2 = generate_nonce();
301        assert_ne!(nonce1, nonce2);
302        assert_eq!(nonce1.len(), 12);
303        assert_eq!(nonce2.len(), 12);
304    }
305
306    #[test]
307    fn test_resolve_password_flag_priority() {
308        // Flag should have highest priority
309        let result = resolve_password(Some("flag_password"));
310        assert!(matches!(result, PasswordSource::Flag(_)));
311        if let PasswordSource::Flag(pwd) = result {
312            assert_eq!(pwd, "flag_password");
313        }
314    }
315
316    #[test]
317    fn test_resolve_password_empty_flag() {
318        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
319        std::env::remove_var("REDDB_KEY");
320        let _ = clear_keyring();
321
322        // Empty flag should not be used
323        let result = resolve_password(Some(""));
324        assert!(!matches!(result, PasswordSource::Flag(_)));
325    }
326
327    #[test]
328    fn test_resolve_password_env_var() {
329        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
330        let _ = clear_keyring();
331
332        std::env::set_var("REDDB_KEY", "env_test_password");
333        let result = resolve_password(None);
334        std::env::remove_var("REDDB_KEY");
335
336        assert!(matches!(result, PasswordSource::EnvVar(_)));
337        if let PasswordSource::EnvVar(pwd) = result {
338            assert_eq!(pwd, "env_test_password");
339        }
340    }
341
342    #[test]
343    fn test_resolve_password_flag_overrides_env() {
344        std::env::set_var("REDDB_KEY", "env_password");
345        let result = resolve_password(Some("flag_password"));
346        std::env::remove_var("REDDB_KEY");
347
348        assert!(matches!(result, PasswordSource::Flag(_)));
349    }
350
351    #[test]
352    fn test_keyring_save_and_retrieve() {
353        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
354
355        // Clear any existing keyring
356        let _ = clear_keyring();
357
358        // Save password
359        let result = save_to_keyring("test_keyring_password_12345");
360        assert!(result.is_ok(), "Failed to save to keyring: {:?}", result);
361
362        // Retrieve password
363        let retrieved = get_from_keyring();
364        assert!(retrieved.is_some());
365        assert_eq!(retrieved.unwrap(), "test_keyring_password_12345");
366
367        // Clean up
368        let _ = clear_keyring();
369    }
370
371    #[test]
372    fn test_keyring_has_password() {
373        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
374
375        let _ = clear_keyring();
376        assert!(!has_keyring_password());
377
378        let _ = save_to_keyring("check_password");
379        assert!(has_keyring_password());
380
381        let _ = clear_keyring();
382        assert!(!has_keyring_password());
383    }
384
385    #[test]
386    fn test_clear_keyring_nonexistent() {
387        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
388
389        // Should not error if keyring doesn't exist
390        let _ = clear_keyring();
391        let result = clear_keyring();
392        assert!(result.is_ok());
393    }
394
395    #[test]
396    fn test_keyring_special_characters() {
397        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
398        let _ = clear_keyring();
399
400        // Test password with special characters
401        let special_password = "p@$$w0rd!#%&*()[]{}|;':\",./<>?`~";
402        let result = save_to_keyring(special_password);
403        assert!(result.is_ok());
404
405        let retrieved = get_from_keyring();
406        assert_eq!(retrieved, Some(special_password.to_string()));
407
408        let _ = clear_keyring();
409    }
410
411    #[test]
412    fn test_keyring_unicode_password() {
413        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
414        let _ = clear_keyring();
415
416        // Test password with unicode characters
417        let unicode_password = "пароль🔒密码パスワード";
418        let result = save_to_keyring(unicode_password);
419        assert!(result.is_ok());
420
421        let retrieved = get_from_keyring();
422        assert_eq!(retrieved, Some(unicode_password.to_string()));
423
424        let _ = clear_keyring();
425    }
426
427    #[test]
428    fn test_keyring_empty_password() {
429        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
430        let _ = clear_keyring();
431
432        // Even empty password should work
433        let result = save_to_keyring("");
434        assert!(result.is_ok());
435
436        let retrieved = get_from_keyring();
437        assert_eq!(retrieved, Some("".to_string()));
438
439        let _ = clear_keyring();
440    }
441
442    #[test]
443    fn test_keyring_long_password() {
444        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
445        let _ = clear_keyring();
446
447        // Test very long password
448        let long_password = "x".repeat(10000);
449        let result = save_to_keyring(&long_password);
450        assert!(result.is_ok());
451
452        let retrieved = get_from_keyring();
453        assert_eq!(retrieved, Some(long_password));
454
455        let _ = clear_keyring();
456    }
457
458    #[test]
459    fn test_resolve_password_keyring_integration() {
460        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
461        std::env::remove_var("REDDB_KEY");
462        let _ = clear_keyring();
463
464        // Save to keyring
465        let _ = save_to_keyring("keyring_test_pwd");
466
467        // Should resolve from keyring
468        let result = resolve_password(None);
469        assert!(matches!(result, PasswordSource::Keyring(_)));
470        if let PasswordSource::Keyring(pwd) = result {
471            assert_eq!(pwd, "keyring_test_pwd");
472        }
473
474        let _ = clear_keyring();
475    }
476
477    #[test]
478    fn test_resolve_password_none_when_empty() {
479        let _lock = KEYRING_TEST_LOCK.lock().unwrap();
480        std::env::remove_var("REDDB_KEY");
481        let _ = clear_keyring();
482
483        let result = resolve_password(None);
484        assert!(matches!(result, PasswordSource::None));
485    }
486
487    #[test]
488    fn test_get_keyring_path_returns_some() {
489        // Should return a path on most systems
490        let path = get_keyring_path();
491        // This might be None in very restricted environments
492        if let Some(p) = path {
493            assert!(p.to_string_lossy().contains("reddb"));
494            assert!(p.to_string_lossy().contains("keyring.enc"));
495        }
496    }
497}