Skip to main content

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}