Skip to main content

rose_squared_sdk/
vault.rs

1// PrivacyVault — The high-level API.
2//
3// This is the only type frontend developers need to interact with.
4// It composes all lower-level modules behind a clean interface:
5//
6//   vault.add_document(keywords, doc_id)
7//   vault.search(keyword)             → Vec<Uuid>
8//   vault.delete_document(keyword, doc_id)
9//   vault.export_state()              → Vec<u8>   (encrypted, safe to persist)
10//   vault.import_state(blob)
11//
12// Thread safety: `PrivacyVault` is NOT `Send` in WASM (single-threaded runtime).
13// In native builds with `std`, wrap in `Arc<Mutex<>>` for multi-threaded use.
14
15use uuid::Uuid;
16
17use crate::client::{
18    state::ClientStateTable,
19    updates::{UpdateEngine, hash_keyword},
20};
21use crate::crypto::kdf::MasterKeySet;
22use crate::error::VaultError;
23use crate::protocol::{
24    search::{SearchProtocol, SearchResult},
25    swissse::VolumeConfig,
26};
27use crate::server::edb::EncryptedStore;
28
29// ── PrivacyVault ──────────────────────────────────────────────────────────────
30
31pub struct PrivacyVault {
32    pub keys:   MasterKeySet,
33    pub state:  ClientStateTable,
34    pub config: VolumeConfig,
35}
36
37impl PrivacyVault {
38    // ── Construction ──────────────────────────────────────────────────────────
39
40    /// Create a new vault from a user password and a 16-byte random salt.
41    ///
42    /// The salt must be generated once and stored alongside the encrypted
43    /// state blob — it is NOT secret, but it must be consistent across sessions.
44    ///
45    /// ```rust
46    /// use rose_squared_sdk::PrivacyVault;
47    /// use rand::RngCore;
48    /// let mut salt = [0u8; 16];
49    /// rand::thread_rng().fill_bytes(&mut salt);
50    /// let vault = PrivacyVault::new("correct-horse-battery-staple", &salt, Default::default());
51    /// ```
52    pub fn new(
53        password: &str,
54        salt:     &[u8; 16],
55        config:   VolumeConfig,
56    ) -> Result<Self, VaultError> {
57        let keys  = MasterKeySet::derive(password, salt)?;
58        let state = ClientStateTable::new();
59        Ok(Self { keys, state, config })
60    }
61
62    /// Restore a vault from a previously exported state blob.
63    pub fn from_exported(
64        password: &str,
65        salt:     &[u8; 16],
66        blob:     &[u8],
67        config:   VolumeConfig,
68    ) -> Result<Self, VaultError> {
69        let keys  = MasterKeySet::derive(password, salt)?;
70        let state = ClientStateTable::import_encrypted(blob, &keys)?;
71        Ok(Self { keys, state, config })
72    }
73
74    // ── Write operations ──────────────────────────────────────────────────────
75
76    /// Index a document under one or more keywords.
77    ///
78    /// Sends one EDB entry per keyword to the store.
79    /// The store never sees the keywords or the document ID in plaintext.
80    ///
81    /// `doc_id` should be your application's stable identifier for the document
82    /// (e.g., the UUID of the file in your encrypted document store).
83    pub async fn add_document<S: EncryptedStore>(
84        &mut self,
85        keywords: &[&str],
86        doc_id:   Uuid,
87        store:    &S,
88    ) -> Result<(), VaultError> {
89        let engine = UpdateEngine::new(&self.keys);
90
91        for &keyword in keywords {
92            let kh    = hash_keyword(keyword);
93            let kw_st = self.state.get_or_create(kh);
94            let entry = engine.prepare_add(keyword.as_bytes(), doc_id, kw_st)?;
95
96            // Use padded write for SWiSSSE volume hiding.
97            store.padded_put_batch(vec![entry], self.config.n_max).await?;
98        }
99
100        Ok(())
101    }
102
103    /// Remove a document from one keyword's result set.
104    ///
105    /// This performs a Backward-Security Type-II delete:
106    ///   • The epoch for this keyword is bumped.
107    ///   • All surviving entries are atomically re-written under the new epoch.
108    ///   • Old epoch entries are deleted.
109    ///
110    /// After this call, any previously issued search tokens for this keyword
111    /// are invalid — they address old-epoch tags which are now gone.
112    pub async fn delete_document<S: EncryptedStore>(
113        &mut self,
114        keyword: &str,
115        doc_id:  Uuid,
116        store:   &S,
117    ) -> Result<(usize, usize), VaultError> {
118        let engine = UpdateEngine::new(&self.keys);
119        let kh     = hash_keyword(keyword);
120        let kw_st  = self.state.get_or_create(kh);
121
122        let batch = engine.prepare_delete(keyword.as_bytes(), doc_id, kw_st)?;
123
124        let removes_count = batch.removes.len();
125        let adds_count = batch.adds.len();
126
127        // Atomic: remove old tags, write new-epoch entries.
128        store.atomic_update(batch.adds, batch.removes).await?;
129
130        Ok((removes_count, adds_count))
131    }
132
133    // ── Search ────────────────────────────────────────────────────────────────
134
135    /// Search for all documents indexed under `keyword`.
136    ///
137    /// The keyword never leaves the client in plaintext.
138    /// The server returns opaque ciphertexts; the client decrypts them here.
139    ///
140    /// Returns document UUIDs sorted newest-first.
141    ///
142    /// With `SWiSSSE` enabled (default), every search fetches exactly
143    /// `n_max` tags from the server, hiding the true result count.
144    pub async fn search<S: EncryptedStore>(
145        &self,
146        keyword: &str,
147        store:   &S,
148    ) -> Result<Vec<Uuid>, VaultError> {
149        let results = self.search_with_metadata(keyword, store).await?;
150        Ok(results.into_iter().map(|r| r.doc_id).collect())
151    }
152
153    /// Search and return full `SearchResult` (doc_id + timestamp).
154    pub async fn search_with_metadata<S: EncryptedStore>(
155        &self,
156        keyword: &str,
157        store:   &S,
158    ) -> Result<Vec<SearchResult>, VaultError> {
159        let proto = SearchProtocol::new(&self.keys, &self.state);
160        
161        let token = match proto.prepare_search(keyword)? {
162            Some(t) => t,
163            None    => {
164                // To hide the fact that a keyword doesn't exist, we SHOULD 
165                // perform a dummy search. For now, we'll return empty.
166                return Ok(vec![]);
167            }
168        };
169
170        // SWiSSSE: Pad the token to n_max tags to hide volume.
171        let padded_token = crate::protocol::swissse::pad_search_token(token, &self.config)?;
172        
173        // Fetch results (batch)
174        let enc_values = proto.fetch_token_results(&padded_token, store).await?;
175        
176        // Decrypt and finalize
177        proto.finalize_search(&padded_token, enc_values)
178    }
179
180    // ── State persistence ─────────────────────────────────────────────────────
181
182    /// Export the encrypted client state as a byte blob.
183    ///
184    /// Store this in IndexedDB, a file, or any persistent medium.
185    /// It is AES-256-GCM encrypted with K_state — safe to store in the cloud.
186    pub fn export_state(&self) -> Result<Vec<u8>, VaultError> {
187        self.state.export_encrypted(&self.keys)
188    }
189}