1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use tokio::fs as tokio_fs;
7use tracing::{debug, error, trace, warn};
8
9use crate::{
10 validation::{is_reserved_name, is_valid_name_chars},
11 Collection,
12 Result,
13 SentinelError,
14};
15
16#[derive(Debug)]
51pub struct Store {
52 root_path: PathBuf,
54 signing_key: Option<Arc<sentinel_crypto::SigningKey>>,
56}
57
58impl Store {
59 pub async fn new<P>(root_path: P, passphrase: Option<&str>) -> Result<Self>
103 where
104 P: AsRef<Path>,
105 {
106 trace!("Creating new Store at path: {:?}", root_path.as_ref());
107 let root_path = root_path.as_ref().to_path_buf();
108 tokio_fs::create_dir_all(&root_path).await.map_err(|e| {
109 error!(
110 "Failed to create store root directory {:?}: {}",
111 root_path, e
112 );
113 e
114 })?;
115 debug!(
116 "Store root directory created or already exists: {:?}",
117 root_path
118 );
119 let mut store = Self {
120 root_path,
121 signing_key: None,
122 };
123 if let Some(passphrase) = passphrase {
124 debug!("Passphrase provided, handling signing key");
125 let keys_collection = store.collection(".keys").await?;
126 if let Some(doc) = keys_collection.get("signing_key").await? {
127 debug!("Loading existing signing key from store");
129 let data = doc.data();
130 let encrypted = data["encrypted"].as_str().ok_or_else(|| {
131 error!("Stored signing key document missing 'encrypted' field");
132 SentinelError::StoreCorruption {
133 reason: "stored signing key document missing 'encrypted' field or not a string".to_owned(),
134 }
135 })?;
136 let salt_hex = data["salt"].as_str().ok_or_else(|| {
137 error!("Stored signing key document missing 'salt' field");
138 SentinelError::StoreCorruption {
139 reason: "stored signing key document missing 'salt' field or not a string".to_owned(),
140 }
141 })?;
142 let salt = hex::decode(salt_hex).map_err(|err| {
143 error!("Stored signing key salt is not valid hex: {}", err);
144 SentinelError::StoreCorruption {
145 reason: format!("stored signing key salt is not valid hex ({})", err),
146 }
147 })?;
148 let encryption_key = sentinel_crypto::derive_key_from_passphrase_with_salt(passphrase, &salt)?;
149 let key_bytes = sentinel_crypto::decrypt_data(encrypted, &encryption_key)?;
150 let key_array: [u8; 32] = key_bytes.try_into().map_err(|kb: Vec<u8>| {
151 error!(
152 "Stored signing key has invalid length: {}, expected 32",
153 kb.len()
154 );
155 SentinelError::StoreCorruption {
156 reason: format!(
157 "stored signing key has an invalid length ({}, expected 32)",
158 kb.len()
159 ),
160 }
161 })?;
162 let signing_key = sentinel_crypto::SigningKey::from_bytes(&key_array);
163 store.signing_key = Some(Arc::new(signing_key));
164 debug!("Existing signing key loaded successfully");
165 }
166 else {
167 debug!("Generating new signing key");
169 let (salt, encryption_key) = sentinel_crypto::derive_key_from_passphrase(passphrase)?;
170 let signing_key = sentinel_crypto::SigningKeyManager::generate_key();
171 let key_bytes = signing_key.to_bytes();
172 let encrypted = sentinel_crypto::encrypt_data(&key_bytes, &encryption_key)?;
173 let salt_hex = hex::encode(&salt);
174 keys_collection
175 .insert(
176 "signing_key",
177 serde_json::json!({"encrypted": encrypted, "salt": salt_hex}),
178 )
179 .await?;
180 store.signing_key = Some(Arc::new(signing_key));
181 debug!("New signing key generated and stored");
182 }
183 }
184 trace!("Store created successfully");
185 Ok(store)
186 }
187
188 pub async fn collection(&self, name: &str) -> Result<Collection> {
248 trace!("Accessing collection: {}", name);
249 validate_collection_name(name)?;
250 let path = self.root_path.join("data").join(name);
251 tokio_fs::create_dir_all(&path).await.map_err(|e| {
252 error!("Failed to create collection directory {:?}: {}", path, e);
253 e
254 })?;
255 debug!("Collection directory ensured: {:?}", path);
256 trace!("Collection '{}' accessed successfully", name);
257 Ok(Collection {
258 path,
259 signing_key: self.signing_key.clone(),
260 })
261 }
262
263 pub fn set_signing_key(&mut self, key: sentinel_crypto::SigningKey) { self.signing_key = Some(Arc::new(key)); }
264}
265
266fn validate_collection_name(name: &str) -> Result<()> {
306 trace!("Validating collection name: {}", name);
307 if name.is_empty() {
309 debug!("Collection name is empty");
310 return Err(SentinelError::InvalidCollectionName {
311 name: name.to_owned(),
312 });
313 }
314
315 if name.starts_with('.') && name != ".keys" {
317 debug!("Collection name starts with dot and is not .keys: {}", name);
318 return Err(SentinelError::InvalidCollectionName {
319 name: name.to_owned(),
320 });
321 }
322
323 if name.ends_with('.') || name.ends_with(' ') {
325 warn!("Collection name ends with dot or space: {}", name);
326 return Err(SentinelError::InvalidCollectionName {
327 name: name.to_owned(),
328 });
329 }
330
331 if !is_valid_name_chars(name) {
333 debug!("Collection name contains invalid characters: {}", name);
334 return Err(SentinelError::InvalidCollectionName {
335 name: name.to_owned(),
336 });
337 }
338
339 if is_reserved_name(name) {
341 debug!("Collection name is a reserved name: {}", name);
342 return Err(SentinelError::InvalidCollectionName {
343 name: name.to_owned(),
344 });
345 }
346
347 trace!("Collection name '{}' is valid", name);
348 Ok(())
349}
350
351#[cfg(test)]
352mod tests {
353 use tempfile::tempdir;
354
355 use super::*;
356
357 #[tokio::test]
358 async fn test_store_new_creates_directory() {
359 let temp_dir = tempdir().unwrap();
360 let store_path = temp_dir.path().join("store");
361
362 let _store = Store::new(&store_path, None).await.unwrap();
363 assert!(store_path.exists());
364 assert!(store_path.is_dir());
365 }
366
367 #[tokio::test]
368 async fn test_store_new_with_existing_directory() {
369 let temp_dir = tempdir().unwrap();
370 let store_path = temp_dir.path();
371
372 let _store = Store::new(&store_path, None).await.unwrap();
374 assert!(store_path.exists());
375 }
376
377 #[tokio::test]
378 async fn test_store_collection_creates_subdirectory() {
379 let temp_dir = tempdir().unwrap();
380 let store = Store::new(temp_dir.path(), None).await.unwrap();
381
382 let collection = store.collection("users").await.unwrap();
383 assert!(collection.path.exists());
384 assert!(collection.path.is_dir());
385 assert_eq!(collection.name(), "users");
386 }
387
388 #[tokio::test]
389 async fn test_store_collection_with_valid_special_characters() {
390 let temp_dir = tempdir().unwrap();
391 let store = Store::new(temp_dir.path(), None).await.unwrap();
392
393 let collection = store.collection("user_data-123").await.unwrap();
395 assert!(collection.path.exists());
396 assert_eq!(collection.name(), "user_data-123");
397
398 let collection2 = store.collection("test.collection").await.unwrap();
399 assert!(collection2.path.exists());
400 assert_eq!(collection2.name(), "test.collection");
401
402 let collection3 = store.collection("data_2024-v1.0").await.unwrap();
403 assert!(collection3.path.exists());
404 assert_eq!(collection3.name(), "data_2024-v1.0");
405 }
406
407 #[tokio::test]
408 async fn test_store_collection_multiple_calls() {
409 let temp_dir = tempdir().unwrap();
410 let store = Store::new(temp_dir.path(), None).await.unwrap();
411
412 let coll1 = store.collection("users").await.unwrap();
413 let coll2 = store.collection("users").await.unwrap();
414
415 assert_eq!(coll1.name(), coll2.name());
416 assert_eq!(coll1.path, coll2.path);
417 }
418
419 #[tokio::test]
420 async fn test_store_collection_invalid_empty_name() {
421 let temp_dir = tempdir().unwrap();
422 let store = Store::new(temp_dir.path(), None).await.unwrap();
423
424 let result = store.collection("").await;
425 assert!(result.is_err());
426 assert!(matches!(
427 result.unwrap_err(),
428 SentinelError::InvalidCollectionName { .. }
429 ));
430 }
431
432 #[tokio::test]
433 async fn test_store_collection_invalid_path_separator() {
434 let temp_dir = tempdir().unwrap();
435 let store = Store::new(temp_dir.path(), None).await.unwrap();
436
437 let result = store.collection("path/traversal").await;
439 assert!(result.is_err());
440 assert!(matches!(
441 result.unwrap_err(),
442 SentinelError::InvalidCollectionName { .. }
443 ));
444
445 let result = store.collection("path\\traversal").await;
447 assert!(result.is_err());
448 assert!(matches!(
449 result.unwrap_err(),
450 SentinelError::InvalidCollectionName { .. }
451 ));
452 }
453
454 #[tokio::test]
455 async fn test_store_collection_invalid_hidden_name() {
456 let temp_dir = tempdir().unwrap();
457 let store = Store::new(temp_dir.path(), None).await.unwrap();
458
459 let result = store.collection(".hidden").await;
460 assert!(result.is_err());
461 assert!(matches!(
462 result.unwrap_err(),
463 SentinelError::InvalidCollectionName { .. }
464 ));
465 }
466
467 #[tokio::test]
468 async fn test_store_collection_invalid_windows_reserved_names() {
469 let temp_dir = tempdir().unwrap();
470 let store = Store::new(temp_dir.path(), None).await.unwrap();
471
472 let reserved_names = vec!["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
473 for name in reserved_names {
474 let result = store.collection(name).await;
475 assert!(result.is_err(), "Expected '{}' to be invalid", name);
476 assert!(matches!(
477 result.unwrap_err(),
478 SentinelError::InvalidCollectionName { .. }
479 ));
480
481 let result = store.collection(&name.to_lowercase()).await;
483 assert!(
484 result.is_err(),
485 "Expected '{}' to be invalid",
486 name.to_lowercase()
487 );
488 assert!(matches!(
489 result.unwrap_err(),
490 SentinelError::InvalidCollectionName { .. }
491 ));
492 }
493 }
494
495 #[tokio::test]
496 async fn test_store_collection_invalid_control_characters() {
497 let temp_dir = tempdir().unwrap();
498 let store = Store::new(temp_dir.path(), None).await.unwrap();
499
500 let result = store.collection("test\0name").await;
502 assert!(result.is_err());
503 assert!(matches!(
504 result.unwrap_err(),
505 SentinelError::InvalidCollectionName { .. }
506 ));
507
508 let result = store.collection("test\x01name").await;
510 assert!(result.is_err());
511 assert!(matches!(
512 result.unwrap_err(),
513 SentinelError::InvalidCollectionName { .. }
514 ));
515 }
516
517 #[tokio::test]
518 async fn test_store_collection_invalid_special_characters() {
519 let temp_dir = tempdir().unwrap();
520 let store = Store::new(temp_dir.path(), None).await.unwrap();
521
522 let invalid_chars = vec!["<", ">", ":", "\"", "|", "?", "*"];
523 for ch in invalid_chars {
524 let name = format!("test{}name", ch);
525 let result = store.collection(&name).await;
526 assert!(result.is_err(), "Expected name with '{}' to be invalid", ch);
527 assert!(matches!(
528 result.unwrap_err(),
529 SentinelError::InvalidCollectionName { .. }
530 ));
531 }
532 }
533
534 #[tokio::test]
535 async fn test_store_collection_invalid_trailing_dot_or_space() {
536 let temp_dir = tempdir().unwrap();
537 let store = Store::new(temp_dir.path(), None).await.unwrap();
538
539 let result = store.collection("test.").await;
541 assert!(result.is_err());
542 assert!(matches!(
543 result.unwrap_err(),
544 SentinelError::InvalidCollectionName { .. }
545 ));
546
547 let result = store.collection("test ").await;
549 assert!(result.is_err());
550 assert!(matches!(
551 result.unwrap_err(),
552 SentinelError::InvalidCollectionName { .. }
553 ));
554 }
555
556 #[tokio::test]
557 async fn test_store_collection_valid_edge_cases() {
558 let temp_dir = tempdir().unwrap();
559 let store = Store::new(temp_dir.path(), None).await.unwrap();
560
561 let collection = store.collection("a").await.unwrap();
563 assert_eq!(collection.name(), "a");
564
565 let collection = store.collection("123").await.unwrap();
567 assert_eq!(collection.name(), "123");
568
569 let long_name = "a".repeat(255);
571 let collection = store.collection(&long_name).await.unwrap();
572 assert_eq!(collection.name(), long_name);
573 }
574
575 #[tokio::test]
576 async fn test_store_new_with_passphrase() {
577 let temp_dir = tempdir().unwrap();
578 let store = Store::new(temp_dir.path(), Some("test_passphrase"))
579 .await
580 .unwrap();
581 assert!(store.signing_key.is_some());
583 }
584
585 #[tokio::test]
586 async fn test_store_new_with_corrupted_keys() {
587 let temp_dir = tempdir().unwrap();
588 let _store = Store::new(temp_dir.path(), Some("test_passphrase"))
590 .await
591 .unwrap();
592
593 let store2 = Store::new(temp_dir.path(), None).await.unwrap();
595 let keys_coll = store2.collection(".keys").await.unwrap();
596 let corrupted_data = serde_json::json!({
598 "salt": "invalid_salt",
599 });
601 keys_coll
602 .insert("signing_key", corrupted_data)
603 .await
604 .unwrap();
605
606 let result = Store::new(temp_dir.path(), Some("test_passphrase")).await;
608 assert!(result.is_err());
609 }
610
611 #[tokio::test]
612 async fn test_store_new_with_invalid_salt_hex() {
613 let temp_dir = tempdir().unwrap();
614 let _store = Store::new(temp_dir.path(), Some("test_passphrase"))
616 .await
617 .unwrap();
618
619 let store2 = Store::new(temp_dir.path(), None).await.unwrap();
621 let keys_coll = store2.collection(".keys").await.unwrap();
622 let doc = keys_coll.get("signing_key").await.unwrap().unwrap();
623 let mut data = doc.data().clone();
624 data["salt"] = serde_json::Value::String("invalid_hex".to_string());
625 keys_coll.insert("signing_key", data).await.unwrap();
626
627 let result = Store::new(temp_dir.path(), Some("test_passphrase")).await;
629 assert!(result.is_err());
630 }
631
632 #[tokio::test]
633 async fn test_store_new_with_invalid_encrypted_length() {
634 let temp_dir = tempdir().unwrap();
635 let _store = Store::new(temp_dir.path(), Some("test_passphrase"))
637 .await
638 .unwrap();
639
640 let store2 = Store::new(temp_dir.path(), None).await.unwrap();
642 let keys_coll = store2.collection(".keys").await.unwrap();
643 let doc = keys_coll.get("signing_key").await.unwrap().unwrap();
644 let mut data = doc.data().clone();
645 data["encrypted"] = serde_json::Value::String(hex::encode(&[0u8; 10])); keys_coll.insert("signing_key", data).await.unwrap();
647
648 let result = Store::new(temp_dir.path(), Some("test_passphrase")).await;
650 assert!(result.is_err());
651 }
652}