rose_squared_sdk/server/edb.rs
1// Encrypted Database (EDB) abstraction layer.
2//
3// The server is FULLY UNTRUSTED. It stores (Tag → EncValue) pairs and
4// executes fetch/put/delete operations on tags it cannot interpret.
5//
6// The `EncryptedStore` trait is the single extension point for storage
7// backends. Implement it for:
8// • IndexedDB (browser WASM, see wasm/js_store.rs)
9// • Redis (server-side proxy for high-throughput deployments)
10// • S3 (blob store with DynamoDB index for tag lookups)
11// • SQLite (local native testing)
12// • HashMap (in-memory mock — shipped here for unit tests)
13//
14// Design: storage-agnostic trait with async methods.
15// In WASM, async fns resolve to JS Promises automatically via wasm-bindgen.
16
17use std::collections::HashMap;
18use async_trait::async_trait;
19
20use crate::crypto::primitives::{EncValue, Tag};
21use crate::error::VaultError;
22
23// ── Raw EDB entry (wire type) ─────────────────────────────────────────────────
24
25/// A single (tag, ciphertext) pair ready to be written to the EDB.
26pub struct RawEdbEntry {
27 pub tag: Tag,
28 pub value: EncValue,
29}
30
31// ── Storage trait ─────────────────────────────────────────────────────────────
32
33/// Implement this trait for any key-value store that will back the EDB.
34///
35/// All inputs and outputs are opaque byte arrays — the store never sees
36/// plaintext keywords, document IDs, or user data.
37#[async_trait(?Send)] // ?Send because WASM is single-threaded
38pub trait EncryptedStore {
39 /// Fetch the encrypted value stored at `tag`, if any.
40 async fn get(&self, tag: &Tag) -> Result<Option<EncValue>, VaultError>;
41
42 /// Store a single (tag, value) pair. Overwrites any existing entry.
43 async fn put(&self, tag: Tag, value: EncValue) -> Result<(), VaultError>;
44
45 /// Remove the entry at `tag`. No-op if tag does not exist.
46 async fn delete(&self, tag: &Tag) -> Result<(), VaultError>;
47
48 /// Fetch multiple tags in a single round-trip.
49 ///
50 /// The default implementation issues sequential GETs.
51 /// Backends should override this with a real batch read (e.g., Redis MGET).
52 ///
53 /// Returns a Vec aligned with `tags`: `None` for any tag not present.
54 async fn get_batch(&self, tags: &[Tag]) -> Result<Vec<Option<EncValue>>, VaultError> {
55 let mut out = Vec::with_capacity(tags.len());
56 for tag in tags {
57 out.push(self.get(tag).await?);
58 }
59 Ok(out)
60 }
61
62 /// Write multiple entries and delete a set of old tags atomically.
63 ///
64 /// Used by the delete protocol (Backward Security Type-II) where we must
65 /// atomically retire old-epoch entries and write new-epoch entries.
66 ///
67 /// Default: sequential puts then deletes (not truly atomic — override for
68 /// production stores that support transactions).
69 async fn atomic_update(
70 &self,
71 puts: Vec<RawEdbEntry>,
72 removes: Vec<Tag>,
73 ) -> Result<(), VaultError> {
74 for entry in puts {
75 self.put(entry.tag, entry.value).await?;
76 }
77 for tag in removes {
78 self.delete(&tag).await?;
79 }
80 Ok(())
81 }
82
83 // ── SWiSSSE: volume-hiding batch write ────────────────────────────────────
84
85 /// Write exactly `target_count` entries, padding with dummy entries if needed.
86 ///
87 /// This is the key SWiSSSE primitive: every write to the EDB has the same
88 /// observable volume (number of entries written), suppressing the volume
89 /// leakage that lets a passive server distinguish large vs. small updates.
90 ///
91 /// Dummy entries are (random_tag, random_ciphertext) pairs that are
92 /// cryptographically indistinguishable from real entries.
93 async fn padded_put_batch(
94 &self,
95 real_entries: Vec<RawEdbEntry>,
96 target_count: usize,
97 ) -> Result<(), VaultError> {
98 use rand::RngCore;
99 use crate::crypto::primitives::LAMBDA;
100
101 if real_entries.len() > target_count {
102 return Err(VaultError::VolumeLimitExceeded { max: target_count });
103 }
104
105 let pad_count = target_count - real_entries.len();
106 let mut rng = rand::thread_rng();
107
108 // Write real entries.
109 for entry in real_entries {
110 self.put(entry.tag, entry.value).await?;
111 }
112
113 // Write dummy entries: both tag and ciphertext are uniformly random.
114 // The server cannot distinguish these from real writes.
115 for _ in 0..pad_count {
116 let mut dummy_tag = [0u8; LAMBDA];
117 let mut dummy_val = vec![0u8; 60]; // matches real entry size
118 rng.fill_bytes(&mut dummy_tag);
119 rng.fill_bytes(&mut dummy_val);
120 self.put(Tag(dummy_tag), EncValue(dummy_val)).await?;
121 }
122
123 Ok(())
124 }
125}
126
127// ── In-memory mock store ──────────────────────────────────────────────────────
128
129/// A `HashMap`-backed `EncryptedStore` for unit tests and local development.
130///
131/// NOT suitable for production — no persistence, no concurrency safety.
132pub struct MockStore {
133 inner: std::sync::Mutex<HashMap<[u8; 32], Vec<u8>>>,
134}
135
136impl MockStore {
137 pub fn new() -> Self {
138 Self { inner: std::sync::Mutex::new(HashMap::new()) }
139 }
140
141 /// Number of entries currently stored. Useful for test assertions.
142 pub fn len(&self) -> usize {
143 self.inner.lock().unwrap().len()
144 }
145}
146
147impl Default for MockStore {
148 fn default() -> Self { Self::new() }
149}
150
151#[async_trait(?Send)]
152impl EncryptedStore for MockStore {
153 async fn get(&self, tag: &Tag) -> Result<Option<EncValue>, VaultError> {
154 let map = self.inner.lock().unwrap();
155 Ok(map.get(&tag.0).map(|v| EncValue(v.clone())))
156 }
157
158 async fn put(&self, tag: Tag, value: EncValue) -> Result<(), VaultError> {
159 let mut map = self.inner.lock().unwrap();
160 map.insert(tag.0, value.0);
161 Ok(())
162 }
163
164 async fn delete(&self, tag: &Tag) -> Result<(), VaultError> {
165 let mut map = self.inner.lock().unwrap();
166 map.remove(&tag.0);
167 Ok(())
168 }
169}