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 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 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: 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); }
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 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 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 let _ = clear_keyring();
357
358 let result = save_to_keyring("test_keyring_password_12345");
360 assert!(result.is_ok(), "Failed to save to keyring: {:?}", result);
361
362 let retrieved = get_from_keyring();
364 assert!(retrieved.is_some());
365 assert_eq!(retrieved.unwrap(), "test_keyring_password_12345");
366
367 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 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 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 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 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 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 let _ = save_to_keyring("keyring_test_pwd");
466
467 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 let path = get_keyring_path();
491 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}