reddb_server/storage/
keyring.rs1use 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#[derive(Debug, Clone)]
23pub enum PasswordSource {
24 Flag(String),
26 EnvVar(String),
28 Keyring(String),
30 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
58pub fn resolve_password(flag_password: Option<&str>) -> PasswordSource {
64 if let Some(pwd) = flag_password {
66 if !pwd.is_empty() {
67 return PasswordSource::Flag(pwd.to_string());
68 }
69 }
70
71 if let Some(pwd) = crate::utils::env_with_file_fallback("REDDB_KEY")
73 .or_else(|| crate::utils::env_with_file_fallback("REDBLUE_DB_KEY"))
74 {
75 return PasswordSource::EnvVar(pwd);
76 }
77
78 if let Some(pwd) = get_from_keyring() {
80 return PasswordSource::Keyring(pwd);
81 }
82
83 PasswordSource::None
85}
86
87pub 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 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
113pub fn save_to_keyring(password: &str) -> Result<(), String> {
115 let keyring_path = get_keyring_path().ok_or("Failed to determine keyring path")?;
116
117 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 let nonce = generate_nonce();
127
128 let ciphertext_and_tag = aes256_gcm_encrypt(&key, &nonce, &[], password.as_bytes());
130
131 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 #[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
153pub 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
164pub fn has_keyring_password() -> bool {
166 get_from_keyring().is_some()
167}
168
169fn get_keyring_path() -> Option<PathBuf> {
171 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 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 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
198fn derive_keyring_key() -> [u8; 32] {
200 let mut identity = String::new();
201
202 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 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 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 identity.push_str(":reddb-keyring-v1");
227
228 sha256(identity.as_bytes())
229}
230
231fn 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::ffi::OsString;
243 use std::path::PathBuf;
244 use std::sync::{Mutex, MutexGuard};
245
246 static KEYRING_TEST_LOCK: std::sync::LazyLock<Mutex<()>> =
254 std::sync::LazyLock::new(|| Mutex::new(()));
255
256 struct KeyringTestGuard {
257 _lock: MutexGuard<'static, ()>,
258 dir: PathBuf,
259 previous_config_home: Option<OsString>,
260 }
261
262 impl Drop for KeyringTestGuard {
263 fn drop(&mut self) {
264 if let Some(value) = &self.previous_config_home {
265 std::env::set_var("XDG_CONFIG_HOME", value);
266 } else {
267 std::env::remove_var("XDG_CONFIG_HOME");
268 }
269 let _ = std::fs::remove_dir_all(&self.dir);
270 }
271 }
272
273 fn keyring_test_guard() -> KeyringTestGuard {
274 let lock = KEYRING_TEST_LOCK.lock().unwrap();
275 let dir = std::env::temp_dir().join(format!(
276 "reddb-keyring-test-{}-{}",
277 std::process::id(),
278 crate::utils::now_unix_nanos()
279 ));
280 std::fs::create_dir_all(&dir).unwrap();
281 let previous_config_home = std::env::var_os("XDG_CONFIG_HOME");
282 std::env::set_var("XDG_CONFIG_HOME", &dir);
283 KeyringTestGuard {
284 _lock: lock,
285 dir,
286 previous_config_home,
287 }
288 }
289
290 #[test]
291 fn test_password_source_is_encrypted() {
292 assert!(PasswordSource::Flag("test".to_string()).is_encrypted());
293 assert!(PasswordSource::EnvVar("test".to_string()).is_encrypted());
294 assert!(PasswordSource::Keyring("test".to_string()).is_encrypted());
295 assert!(!PasswordSource::None.is_encrypted());
296 }
297
298 #[test]
299 fn test_password_source_name() {
300 assert_eq!(PasswordSource::Flag("".to_string()).source_name(), "flag");
301 assert_eq!(PasswordSource::EnvVar("".to_string()).source_name(), "env");
302 assert_eq!(
303 PasswordSource::Keyring("".to_string()).source_name(),
304 "keyring"
305 );
306 assert_eq!(PasswordSource::None.source_name(), "none");
307 }
308
309 #[test]
310 fn test_password_source_password() {
311 assert_eq!(
312 PasswordSource::Flag("mypass".to_string()).password(),
313 Some("mypass")
314 );
315 assert_eq!(
316 PasswordSource::EnvVar("envpass".to_string()).password(),
317 Some("envpass")
318 );
319 assert_eq!(
320 PasswordSource::Keyring("ringpass".to_string()).password(),
321 Some("ringpass")
322 );
323 assert_eq!(PasswordSource::None.password(), None);
324 }
325
326 #[test]
327 fn test_derive_keyring_key_deterministic() {
328 let key1 = derive_keyring_key();
329 let key2 = derive_keyring_key();
330 assert_eq!(key1, key2);
331 assert_eq!(key1.len(), 32);
332 }
333
334 #[test]
335 fn test_derive_keyring_key_length() {
336 let key = derive_keyring_key();
337 assert_eq!(key.len(), 32); }
339
340 #[test]
341 fn test_generate_nonce_uniqueness() {
342 let nonce1 = generate_nonce();
343 let nonce2 = generate_nonce();
344 assert_ne!(nonce1, nonce2);
345 assert_eq!(nonce1.len(), 12);
346 assert_eq!(nonce2.len(), 12);
347 }
348
349 #[test]
350 fn test_resolve_password_flag_priority() {
351 let result = resolve_password(Some("flag_password"));
353 assert!(matches!(result, PasswordSource::Flag(_)));
354 if let PasswordSource::Flag(pwd) = result {
355 assert_eq!(pwd, "flag_password");
356 }
357 }
358
359 #[test]
360 fn test_resolve_password_empty_flag() {
361 let _guard = keyring_test_guard();
362 std::env::remove_var("REDDB_KEY");
363 let _ = clear_keyring();
364
365 let result = resolve_password(Some(""));
367 assert!(!matches!(result, PasswordSource::Flag(_)));
368 }
369
370 #[test]
371 fn test_resolve_password_env_var() {
372 let _guard = keyring_test_guard();
373 let _ = clear_keyring();
374
375 std::env::set_var("REDDB_KEY", "env_test_password");
376 let result = resolve_password(None);
377 std::env::remove_var("REDDB_KEY");
378
379 assert!(matches!(result, PasswordSource::EnvVar(_)));
380 if let PasswordSource::EnvVar(pwd) = result {
381 assert_eq!(pwd, "env_test_password");
382 }
383 }
384
385 #[test]
386 fn test_resolve_password_flag_overrides_env() {
387 std::env::set_var("REDDB_KEY", "env_password");
388 let result = resolve_password(Some("flag_password"));
389 std::env::remove_var("REDDB_KEY");
390
391 assert!(matches!(result, PasswordSource::Flag(_)));
392 }
393
394 #[test]
395 fn test_keyring_save_and_retrieve() {
396 let _guard = keyring_test_guard();
397
398 let _ = clear_keyring();
400
401 let result = save_to_keyring("test_keyring_password_12345");
403 assert!(result.is_ok(), "Failed to save to keyring: {:?}", result);
404
405 let retrieved = get_from_keyring();
407 assert!(retrieved.is_some());
408 assert_eq!(retrieved.unwrap(), "test_keyring_password_12345");
409
410 let _ = clear_keyring();
412 }
413
414 #[test]
415 fn test_keyring_has_password() {
416 let _guard = keyring_test_guard();
417
418 let _ = clear_keyring();
419 assert!(!has_keyring_password());
420
421 let _ = save_to_keyring("check_password");
422 assert!(has_keyring_password());
423
424 let _ = clear_keyring();
425 assert!(!has_keyring_password());
426 }
427
428 #[test]
429 fn test_clear_keyring_nonexistent() {
430 let _guard = keyring_test_guard();
431
432 let _ = clear_keyring();
434 let result = clear_keyring();
435 assert!(result.is_ok());
436 }
437
438 #[test]
439 fn test_keyring_special_characters() {
440 let _guard = keyring_test_guard();
441 let _ = clear_keyring();
442
443 let special_password = "p@$$w0rd!#%&*()[]{}|;':\",./<>?`~";
445 let result = save_to_keyring(special_password);
446 assert!(result.is_ok());
447
448 let retrieved = get_from_keyring();
449 assert_eq!(retrieved, Some(special_password.to_string()));
450
451 let _ = clear_keyring();
452 }
453
454 #[test]
455 fn test_keyring_unicode_password() {
456 let _guard = keyring_test_guard();
457 let _ = clear_keyring();
458
459 let unicode_password = "пароль🔒密码パスワード";
461 let result = save_to_keyring(unicode_password);
462 assert!(result.is_ok());
463
464 let retrieved = get_from_keyring();
465 assert_eq!(retrieved, Some(unicode_password.to_string()));
466
467 let _ = clear_keyring();
468 }
469
470 #[test]
471 fn test_keyring_empty_password() {
472 let _guard = keyring_test_guard();
473 let _ = clear_keyring();
474
475 let result = save_to_keyring("");
477 assert!(result.is_ok());
478
479 let retrieved = get_from_keyring();
480 assert_eq!(retrieved, Some("".to_string()));
481
482 let _ = clear_keyring();
483 }
484
485 #[test]
486 fn test_keyring_long_password() {
487 let _guard = keyring_test_guard();
488 let _ = clear_keyring();
489
490 let long_password = "x".repeat(10000);
492 let result = save_to_keyring(&long_password);
493 assert!(result.is_ok());
494
495 let retrieved = get_from_keyring();
496 assert_eq!(retrieved, Some(long_password));
497
498 let _ = clear_keyring();
499 }
500
501 #[test]
502 fn test_resolve_password_keyring_integration() {
503 let _guard = keyring_test_guard();
504 std::env::remove_var("REDDB_KEY");
505 let _ = clear_keyring();
506
507 let _ = save_to_keyring("keyring_test_pwd");
509
510 let result = resolve_password(None);
512 assert!(matches!(result, PasswordSource::Keyring(_)));
513 if let PasswordSource::Keyring(pwd) = result {
514 assert_eq!(pwd, "keyring_test_pwd");
515 }
516
517 let _ = clear_keyring();
518 }
519
520 #[test]
521 fn test_resolve_password_none_when_empty() {
522 let _guard = keyring_test_guard();
523 std::env::remove_var("REDDB_KEY");
524 let _ = clear_keyring();
525
526 let result = resolve_password(None);
527 assert!(matches!(result, PasswordSource::None));
528 }
529
530 #[test]
531 fn test_get_keyring_path_returns_some() {
532 let path = get_keyring_path();
534 if let Some(p) = path {
536 assert!(p.to_string_lossy().contains("reddb"));
537 assert!(p.to_string_lossy().contains("keyring.enc"));
538 }
539 }
540}