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::sync::Mutex;
243
244 static KEYRING_TEST_LOCK: std::sync::LazyLock<Mutex<()>> = std::sync::LazyLock::new(|| {
253 let dir = std::env::temp_dir().join(format!("reddb-keyring-test-{}", std::process::id()));
254 let _ = std::fs::create_dir_all(&dir);
255 std::env::set_var("XDG_CONFIG_HOME", dir);
256 Mutex::new(())
257 });
258
259 #[test]
260 fn test_password_source_is_encrypted() {
261 assert!(PasswordSource::Flag("test".to_string()).is_encrypted());
262 assert!(PasswordSource::EnvVar("test".to_string()).is_encrypted());
263 assert!(PasswordSource::Keyring("test".to_string()).is_encrypted());
264 assert!(!PasswordSource::None.is_encrypted());
265 }
266
267 #[test]
268 fn test_password_source_name() {
269 assert_eq!(PasswordSource::Flag("".to_string()).source_name(), "flag");
270 assert_eq!(PasswordSource::EnvVar("".to_string()).source_name(), "env");
271 assert_eq!(
272 PasswordSource::Keyring("".to_string()).source_name(),
273 "keyring"
274 );
275 assert_eq!(PasswordSource::None.source_name(), "none");
276 }
277
278 #[test]
279 fn test_password_source_password() {
280 assert_eq!(
281 PasswordSource::Flag("mypass".to_string()).password(),
282 Some("mypass")
283 );
284 assert_eq!(
285 PasswordSource::EnvVar("envpass".to_string()).password(),
286 Some("envpass")
287 );
288 assert_eq!(
289 PasswordSource::Keyring("ringpass".to_string()).password(),
290 Some("ringpass")
291 );
292 assert_eq!(PasswordSource::None.password(), None);
293 }
294
295 #[test]
296 fn test_derive_keyring_key_deterministic() {
297 let key1 = derive_keyring_key();
298 let key2 = derive_keyring_key();
299 assert_eq!(key1, key2);
300 assert_eq!(key1.len(), 32);
301 }
302
303 #[test]
304 fn test_derive_keyring_key_length() {
305 let key = derive_keyring_key();
306 assert_eq!(key.len(), 32); }
308
309 #[test]
310 fn test_generate_nonce_uniqueness() {
311 let nonce1 = generate_nonce();
312 let nonce2 = generate_nonce();
313 assert_ne!(nonce1, nonce2);
314 assert_eq!(nonce1.len(), 12);
315 assert_eq!(nonce2.len(), 12);
316 }
317
318 #[test]
319 fn test_resolve_password_flag_priority() {
320 let result = resolve_password(Some("flag_password"));
322 assert!(matches!(result, PasswordSource::Flag(_)));
323 if let PasswordSource::Flag(pwd) = result {
324 assert_eq!(pwd, "flag_password");
325 }
326 }
327
328 #[test]
329 fn test_resolve_password_empty_flag() {
330 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
331 std::env::remove_var("REDDB_KEY");
332 let _ = clear_keyring();
333
334 let result = resolve_password(Some(""));
336 assert!(!matches!(result, PasswordSource::Flag(_)));
337 }
338
339 #[test]
340 fn test_resolve_password_env_var() {
341 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
342 let _ = clear_keyring();
343
344 std::env::set_var("REDDB_KEY", "env_test_password");
345 let result = resolve_password(None);
346 std::env::remove_var("REDDB_KEY");
347
348 assert!(matches!(result, PasswordSource::EnvVar(_)));
349 if let PasswordSource::EnvVar(pwd) = result {
350 assert_eq!(pwd, "env_test_password");
351 }
352 }
353
354 #[test]
355 fn test_resolve_password_flag_overrides_env() {
356 std::env::set_var("REDDB_KEY", "env_password");
357 let result = resolve_password(Some("flag_password"));
358 std::env::remove_var("REDDB_KEY");
359
360 assert!(matches!(result, PasswordSource::Flag(_)));
361 }
362
363 #[test]
364 fn test_keyring_save_and_retrieve() {
365 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
366
367 let _ = clear_keyring();
369
370 let result = save_to_keyring("test_keyring_password_12345");
372 assert!(result.is_ok(), "Failed to save to keyring: {:?}", result);
373
374 let retrieved = get_from_keyring();
376 assert!(retrieved.is_some());
377 assert_eq!(retrieved.unwrap(), "test_keyring_password_12345");
378
379 let _ = clear_keyring();
381 }
382
383 #[test]
384 fn test_keyring_has_password() {
385 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
386
387 let _ = clear_keyring();
388 assert!(!has_keyring_password());
389
390 let _ = save_to_keyring("check_password");
391 assert!(has_keyring_password());
392
393 let _ = clear_keyring();
394 assert!(!has_keyring_password());
395 }
396
397 #[test]
398 fn test_clear_keyring_nonexistent() {
399 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
400
401 let _ = clear_keyring();
403 let result = clear_keyring();
404 assert!(result.is_ok());
405 }
406
407 #[test]
408 fn test_keyring_special_characters() {
409 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
410 let _ = clear_keyring();
411
412 let special_password = "p@$$w0rd!#%&*()[]{}|;':\",./<>?`~";
414 let result = save_to_keyring(special_password);
415 assert!(result.is_ok());
416
417 let retrieved = get_from_keyring();
418 assert_eq!(retrieved, Some(special_password.to_string()));
419
420 let _ = clear_keyring();
421 }
422
423 #[test]
424 fn test_keyring_unicode_password() {
425 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
426 let _ = clear_keyring();
427
428 let unicode_password = "пароль🔒密码パスワード";
430 let result = save_to_keyring(unicode_password);
431 assert!(result.is_ok());
432
433 let retrieved = get_from_keyring();
434 assert_eq!(retrieved, Some(unicode_password.to_string()));
435
436 let _ = clear_keyring();
437 }
438
439 #[test]
440 fn test_keyring_empty_password() {
441 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
442 let _ = clear_keyring();
443
444 let result = save_to_keyring("");
446 assert!(result.is_ok());
447
448 let retrieved = get_from_keyring();
449 assert_eq!(retrieved, Some("".to_string()));
450
451 let _ = clear_keyring();
452 }
453
454 #[test]
455 fn test_keyring_long_password() {
456 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
457 let _ = clear_keyring();
458
459 let long_password = "x".repeat(10000);
461 let result = save_to_keyring(&long_password);
462 assert!(result.is_ok());
463
464 let retrieved = get_from_keyring();
465 assert_eq!(retrieved, Some(long_password));
466
467 let _ = clear_keyring();
468 }
469
470 #[test]
471 fn test_resolve_password_keyring_integration() {
472 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
473 std::env::remove_var("REDDB_KEY");
474 let _ = clear_keyring();
475
476 let _ = save_to_keyring("keyring_test_pwd");
478
479 let result = resolve_password(None);
481 assert!(matches!(result, PasswordSource::Keyring(_)));
482 if let PasswordSource::Keyring(pwd) = result {
483 assert_eq!(pwd, "keyring_test_pwd");
484 }
485
486 let _ = clear_keyring();
487 }
488
489 #[test]
490 fn test_resolve_password_none_when_empty() {
491 let _lock = KEYRING_TEST_LOCK.lock().unwrap();
492 std::env::remove_var("REDDB_KEY");
493 let _ = clear_keyring();
494
495 let result = resolve_password(None);
496 assert!(matches!(result, PasswordSource::None));
497 }
498
499 #[test]
500 fn test_get_keyring_path_returns_some() {
501 let path = get_keyring_path();
503 if let Some(p) = path {
505 assert!(p.to_string_lossy().contains("reddb"));
506 assert!(p.to_string_lossy().contains("keyring.enc"));
507 }
508 }
509}