Skip to main content

rose_squared_sdk/protocol/
swissse.rs

1// SWiSSSE: System-Wide Security for Searchable Symmetric Encryption.
2// (PoPETS 2024 — upgrade layer on top of RO(SE)²)
3//
4// ── What SWiSSSE adds ─────────────────────────────────────────────────────────
5// RO(SE)² achieves forward/backward security but still leaks:
6//   (a) Volume leakage — the number of results per keyword.
7//   (b) Access-pattern leakage — which EDB addresses are fetched per query,
8//       allowing a persistent server to link repeated searches.
9//
10// SWiSSSE suppresses both by:
11//   (a) Padding every write batch to a fixed size `N_max`.
12//   (b) Padding every read batch to `N_max` dummy lookups.
13//
14// ── Design decisions (locked) ─────────────────────────────────────────────────
15//   • `N_max` is fixed at vault creation time.  It bounds the maximum number
16//     of distinct documents that can be indexed under any single keyword.
17//   • Dummy reads: the client generates `N_max - |real_results|` fake tags
18//     (random bytes) and includes them in the search token.  The server fetches
19//     all tags uniformly; misses on dummy tags are silently ignored.
20//   • Dummy writes: `padded_put_batch` on the EDB trait fills up to `N_max`.
21//   • Both dummies are cryptographically indistinguishable from real ones —
22//     random tags in a tag space of 2^256 are pseudo-random under PRF security.
23
24use rand::RngCore;
25
26use crate::crypto::primitives::{SearchToken, Tag, LAMBDA};
27use crate::error::VaultError;
28
29// ── VolumeConfig ─────────────────────────────────────────────────────────────
30
31/// Parameters that control the SWiSSSE volume-hiding behaviour.
32///
33/// Set once at vault creation and stored alongside the encrypted state.
34/// Changing `n_max` after vault creation requires a full re-index.
35#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
36pub struct VolumeConfig {
37    /// The padded batch size for both reads and writes.
38    /// Every search fetches exactly `n_max` tags from the server,
39    /// and every write batch sends exactly `n_max` entries.
40    ///
41    /// Choose to be ≥ the 99th-percentile keyword result count.
42    /// Typical value for enterprise docs: 256–1024.
43    pub n_max: usize,
44}
45
46impl Default for VolumeConfig {
47    fn default() -> Self {
48        Self { n_max: 256 }
49    }
50}
51
52// ── Read padding ──────────────────────────────────────────────────────────────
53
54/// Pad a `SearchToken` with dummy (tag, key) pairs up to `n_max`.
55///
56/// The returned token has exactly `n_max` pairs.  The server fetches all
57/// of them; missing entries for dummy tags are silently dropped by
58/// `SearchProtocol::finalize_search` (they return `None` from the EDB).
59///
60/// The dummy keys are random — even if a dummy tag accidentally collides
61/// with a real entry (probability 2^{-256}), decryption will fail the GCM
62/// check and return `Tampered`, which the caller can distinguish and ignore.
63pub fn pad_search_token(
64    token:  SearchToken,
65    config: &VolumeConfig,
66) -> Result<SearchToken, VaultError> {
67    let real_count = token.pairs.len();
68    if real_count > config.n_max {
69        return Err(VaultError::VolumeLimitExceeded { max: config.n_max });
70    }
71
72    let pad_count = config.n_max - real_count;
73    let mut rng   = rand::thread_rng();
74    let mut pairs = token.pairs;
75
76    for _ in 0..pad_count {
77        let mut dummy_tag = [0u8; LAMBDA];
78        let mut dummy_key = [0u8; LAMBDA];
79        rng.fill_bytes(&mut dummy_tag);
80        rng.fill_bytes(&mut dummy_key);
81        pairs.push((Tag(dummy_tag), dummy_key));
82    }
83
84    // Shuffle so real entries are not always at the front.
85    // A deterministic server correlating position across queries would otherwise
86    // learn the result count even with padding.
87    fisher_yates_shuffle(&mut pairs, &mut rng);
88
89    Ok(SearchToken { pairs })
90}
91
92/// Fisher-Yates shuffle for the token pairs.
93fn fisher_yates_shuffle<T>(v: &mut Vec<T>, rng: &mut impl RngCore) {
94    let n = v.len();
95    for i in (1..n).rev() {
96        let j = (rng.next_u64() as usize) % (i + 1);
97        v.swap(i, j);
98    }
99}