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}