Skip to main content

rose_squared_sdk/client/
updates.rs

1// Forward-Secure Add and Backward-Secure Delete.
2//
3// ── Forward Security ──────────────────────────────────────────────────────────
4// A search token issued at time T cannot retrieve entries written AFTER T.
5// This is structurally guaranteed: each add allocates a fresh `total_writes`
6// index.  A token captures the live_indices snapshot at generation time —
7// it has no knowledge of indices > snapshot.
8//
9// ── Backward Security Type-II (locked decision) ──────────────────────────────
10// After deletion of doc D from keyword W:
11//   1.  D's index is removed from live_indices.
12//   2.  The epoch is bumped for W.
13//   3.  All surviving entries are re-written to the EDB under the new epoch.
14//   4.  Any future search token uses the new epoch — it cannot address the
15//       old epoch's tags (they are orphaned on the server forever).
16//
17// Cost: O(|remaining results|) EDB writes per delete.
18// Benefit: unconditional backward privacy — even an adaptive server that
19// stores *all* historical access patterns learns nothing about deleted docs.
20//
21// Type-III would avoid re-writes but requires a more complex "forward pointer"
22// state; Type-II is simpler, auditable, and sufficient for our threat model.
23
24use sha2::{Sha256, Digest};
25use uuid::Uuid;
26
27use crate::crypto::{aead, primitives::{EntryOp, EntryPayload}};
28use crate::crypto::kdf::MasterKeySet;
29use crate::client::{state::KeywordState, trapdoor::TrapdoorEngine};
30use crate::error::VaultError;
31use crate::server::edb::RawEdbEntry;
32
33// Re-export for protocol layer convenience.
34pub use crate::crypto::primitives::Tag;
35
36// ── UpdateEngine ──────────────────────────────────────────────────────────────
37
38pub struct UpdateEngine<'a> {
39    pub trapdoor: TrapdoorEngine<'a>,
40}
41
42impl<'a> UpdateEngine<'a> {
43    pub fn new(keys: &'a MasterKeySet) -> Self {
44        Self { trapdoor: TrapdoorEngine::new(keys) }
45    }
46
47    // ── Add ───────────────────────────────────────────────────────────────────
48
49    /// Prepare one EDB entry to index `doc_id` under `keyword`.
50    ///
51    /// Mutates `state` (increments counters, records the new index).
52    /// Returns the `RawEdbEntry` to be sent to the server.
53    ///
54    /// The caller must persist the updated `state` before (or atomically with)
55    /// sending the entry to the server.  If the state is lost after the server
56    /// write, use `TrapdoorEngine::generate_recovery_probe` to reconstruct it.
57    pub fn prepare_add(
58        &self,
59        keyword: &[u8],
60        doc_id:  Uuid,
61        state:   &mut KeywordState,
62    ) -> Result<RawEdbEntry, VaultError> {
63        let idx = state.next_index();
64        let epoch = state.epoch;
65
66        // Derive the fresh address + key for this entry.
67        let tag     = self.trapdoor.derive_tag(keyword, idx, epoch);
68        let val_key = self.trapdoor.derive_val_key(keyword, idx, epoch);
69
70        // Encrypt the payload.
71        let payload = EntryPayload {
72            doc_id,
73            op:        EntryOp::Add,
74            timestamp: now_ms(),
75        };
76        let plaintext = bincode::serialize(&payload)?;
77        let enc_value = aead::encrypt(&val_key, &plaintext)?;
78
79        // Register the new index in client state.
80        state.record_add(idx, doc_id);
81
82        Ok(RawEdbEntry { tag, value: enc_value })
83    }
84
85    // ── Delete (Backward Security Type-II) ────────────────────────────────────
86
87    /// Remove `doc_id` from `keyword`'s result set.
88    ///
89    /// Returns a batch of EDB entries to:
90    ///   (a) DELETE the old epoch's entries (server removes them by tag).
91    ///   (b) PUT new epoch's entries for all surviving docs.
92    ///
93    /// The batch must be applied atomically on the server side.
94    /// If an entry was not indexed under this keyword, returns an empty Vec.
95    pub fn prepare_delete(
96        &self,
97        keyword: &[u8],
98        doc_id:  Uuid,
99        state:   &mut KeywordState,
100    ) -> Result<DeleteBatch, VaultError> {
101        // Step 1: collect the old tags that need to be removed.
102        let old_epoch = state.epoch;
103        let old_tags: Vec<Tag> = state
104            .live_indices
105            .iter()
106            .map(|&i| self.trapdoor.derive_tag(keyword, i, old_epoch))
107            .collect();
108
109        // Step 2: evict the doc from state.  This also bumps the epoch.
110        let was_indexed = state.evict_doc(doc_id);
111        if !was_indexed {
112            return Ok(DeleteBatch { removes: vec![], adds: vec![] });
113        }
114
115        // Step 3: re-encrypt surviving entries under the new epoch.
116        let new_epoch = state.epoch;
117        let mut new_entries = Vec::with_capacity(state.live_indices.len());
118
119        for &idx in &state.live_indices {
120            // Reconstruct the doc_id for this surviving index.
121            let surviving_doc = Uuid::from_bytes(
122                *state.index_to_doc.get(&idx)
123                    .ok_or(VaultError::DocNotFound(format!("index {idx}")))?
124            );
125
126            let tag     = self.trapdoor.derive_tag(keyword, idx, new_epoch);
127            let val_key = self.trapdoor.derive_val_key(keyword, idx, new_epoch);
128
129            let payload = EntryPayload {
130                doc_id:    surviving_doc,
131                op:        EntryOp::Add,
132                timestamp: now_ms(),
133            };
134            let plaintext = bincode::serialize(&payload)?;
135            let enc_value = aead::encrypt(&val_key, &plaintext)?;
136
137            new_entries.push(RawEdbEntry { tag, value: enc_value });
138        }
139
140        Ok(DeleteBatch {
141            removes: old_tags,
142            adds:    new_entries,
143        })
144    }
145}
146
147// ── DeleteBatch ───────────────────────────────────────────────────────────────
148
149/// Atomic update set produced by a delete operation.
150///
151/// The server must apply `removes` and `adds` in a single transaction.
152/// Partial application would leave the index in an inconsistent state.
153pub struct DeleteBatch {
154    /// Old-epoch tags to erase from the EDB.
155    pub removes: Vec<Tag>,
156    /// New-epoch ciphertexts to insert.
157    pub adds: Vec<RawEdbEntry>,
158}
159
160// ── Helpers ───────────────────────────────────────────────────────────────────
161
162/// Keyword → 32-byte hash used as the state-table key.
163/// SHA-256 hides keyword lengths and content at rest.
164pub fn hash_keyword(keyword: &str) -> [u8; 32] {
165    let mut h = Sha256::new();
166    h.update(keyword.as_bytes());
167    h.finalize().into()
168}
169
170/// Current time in milliseconds since UNIX epoch.
171#[cfg(target_arch = "wasm32")]
172pub fn now_ms() -> u64 {
173    (js_sys::Date::now()) as u64
174}
175
176#[cfg(not(target_arch = "wasm32"))]
177pub fn now_ms() -> u64 {
178    use std::time::{SystemTime, UNIX_EPOCH};
179    SystemTime::now()
180        .duration_since(UNIX_EPOCH)
181        .map(|d| d.as_millis() as u64)
182        .unwrap_or(0)
183}