rose-squared-sdk 0.1.0

Privacy-preserving encrypted search SDK implementing the SWiSSSE protocol with forward/backward security and volume-hiding, compilable to WebAssembly
Documentation
// Trapdoor Generation — the core of RO(SE)².
//
// The O(1) server-side search complexity arises because the CLIENT computes
// the exact EDB addresses (tags) for every live result and sends them in the
// search token.  The server does one direct-address lookup per tag — no scan,
// no linked-list traversal.
//
// PRF instantiation (locked):
//   HMAC-SHA256 used as a PRF.
//   Input domain separation via distinct ASCII prefixes ("tag:" / "val:").
//   This satisfies the PRF assumption under standard HMAC security.
//
// Robustness (RO(SE)²):
//   After a client crash the local state may be lost.  `generate_recovery_probe`
//   issues a bounded scan across counter values [0, max_writes) to rediscover
//   live entries from the EDB without revealing the keyword to the server.

use hmac::{Hmac, Mac};
use sha2::Sha256;

use crate::crypto::kdf::MasterKeySet;
use crate::crypto::primitives::{Epoch, SearchToken, Tag, LAMBDA};
use crate::client::state::KeywordState;

type HmacSha256 = Hmac<Sha256>;

// ── TrapdoorEngine ────────────────────────────────────────────────────────────

/// Stateless engine that derives tags and entry keys from the MasterKeySet.
///
/// All methods take keyword bytes + counters and return deterministic output.
/// No state is mutated here — state mutation lives in `UpdateEngine`.
pub struct TrapdoorEngine<'a> {
    pub keys: &'a MasterKeySet,
}

impl<'a> TrapdoorEngine<'a> {
    pub fn new(keys: &'a MasterKeySet) -> Self {
        Self { keys }
    }

    // ── Tag derivation ────────────────────────────────────────────────────────

    /// Derive the EDB address for keyword `w` at index `i` in epoch `e`.
    ///
    ///   tag = HMAC-SHA256(K_tag,  "tag:" || w || LE64(i) || LE64(e))
    ///
    /// The epoch binds the tag to a specific deletion era.  When the epoch
    /// is bumped on delete, all old tags become orphaned — the server has the
    /// ciphertext but no token will ever request it again (Backward Security).
    pub fn derive_tag(&self, keyword: &[u8], index: u64, epoch: Epoch) -> Tag {
        let mut mac = HmacSha256::new_from_slice(self.keys.k_tag.as_bytes())
            .expect("HMAC accepts any key size");
        mac.update(b"tag:");
        mac.update(keyword);
        mac.update(&index.to_le_bytes());
        mac.update(&epoch.to_le_bytes());
        Tag(mac.finalize().into_bytes().into())
    }

    // ── Per-entry value key ───────────────────────────────────────────────────

    /// Derive the AES-256-GCM key for the entry at (keyword, index, epoch).
    ///
    ///   val_key = HMAC-SHA256(K_val, "val:" || w || LE64(i) || LE64(e))
    ///
    /// Each entry has an independent key, so compromise of one entry key
    /// reveals nothing about other entries.
    pub fn derive_val_key(&self, keyword: &[u8], index: u64, epoch: Epoch) -> [u8; LAMBDA] {
        let mut mac = HmacSha256::new_from_slice(self.keys.k_val.as_bytes())
            .expect("HMAC accepts any key size");
        mac.update(b"val:");
        mac.update(keyword);
        mac.update(&index.to_le_bytes());
        mac.update(&epoch.to_le_bytes());
        mac.finalize().into_bytes().into()
    }

    // ── Search token generation ───────────────────────────────────────────────

    /// Generate a `SearchToken` for keyword `w` given its current `state`.
    ///
    /// The token contains exactly one (tag, key) pair per live entry.
    /// Complexity: O(|result|) on the client, O(1) per lookup on the server.
    ///
    /// This token MUST be single-use.  Re-using a token allows the server to
    /// link two queries to the same keyword (breaks search-pattern hiding).
    pub fn generate_search_token(
        &self,
        keyword: &[u8],
        state:   &KeywordState,
    ) -> SearchToken {
        let pairs = state
            .live_indices
            .iter()
            .map(|&i| {
                let tag = self.derive_tag(keyword, i, state.epoch);
                let key = self.derive_val_key(keyword, i, state.epoch);
                (tag, key)
            })
            .collect();

        SearchToken { pairs }
    }

    // ── Robustness: crash-recovery probe ─────────────────────────────────────

    /// Generate probe (tag, key) pairs for indices [0, max_writes) at `epoch`.
    ///
    /// After a client crash where local state is lost, the client can re-probe
    /// the EDB for all historical indices.  Any tag the server returns indicates
    /// a live entry — the client can reconstruct its KeywordState from the
    /// decrypted payloads.
    ///
    /// Security: the server still sees only pseudorandom tags.  The probe
    /// count is an upper bound on total writes, which the client may store
    /// separately (e.g., in a small unencrypted hint file).
    pub fn generate_recovery_probe(
        &self,
        keyword:       &[u8],
        max_writes:    u64,
        epoch:         Epoch,
    ) -> Vec<(Tag, [u8; LAMBDA])> {
        (0..max_writes)
            .map(|i| {
                let tag = self.derive_tag(keyword, i, epoch);
                let key = self.derive_val_key(keyword, i, epoch);
                (tag, key)
            })
            .collect()
    }
}