cipherstash-client 0.42.0

The official CipherStash SDK
Documentation
use super::{EncryptedSteVecTerm, TokenizedSelector};
use crate::zerokms::encrypted_record;
use serde::{Deserialize, Serialize};

/// One SteVec wire entry: `{s, c}` plus an optional orderable term.
///
/// - **Path entries** carry the node's ciphertext in `c` and, for
///   number/string nodes only, the orderable term (`op` in Compat mode, `oc`
///   in Standard mode).
/// - **Value entries** (value-inclusive selectors — the exact-match half of
///   the design) and non-orderable path entries carry no term at all; their
///   `c` is the encryption of the value-entry sentinel for value entries, or
///   the node's ciphertext for path entries. Exact matching is selector
///   presence, so no per-value MAC term (`hm`) exists on any entry.
///
/// `c` is the **raw AEAD output only** (base85 on the wire): the key
/// material lives once in the document's [`crate::zerokms::KeyHeader`], and
/// the entry's nonce is derived from its selector
/// ([`TokenizedSelector::aead_nonce`]) rather than stored. An entry is
/// therefore not self-decryptable — decryption reassembles header + `c` +
/// selector nonce.
#[cfg_attr(test, derive(PartialEq, Eq))]
#[derive(Debug, Serialize, Deserialize)]
pub struct EncryptedEntry<const N: usize> {
    #[serde(rename = "s")]
    pub tokenized_selector: TokenizedSelector<N>,
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub term: Option<EncryptedSteVecTerm>,
    #[serde(rename = "c", with = "encrypted_record::formats::base85")]
    pub ciphertext: Vec<u8>,
    pub parent_is_array: bool,
}

impl<const N: usize> EncryptedEntry<N> {
    pub(crate) fn new(
        tokenized_selector: TokenizedSelector<N>,
        term: Option<EncryptedSteVecTerm>,
        ciphertext: Vec<u8>,
        parent_is_array: bool,
    ) -> Self {
        Self {
            tokenized_selector,
            term,
            ciphertext,
            parent_is_array,
        }
    }
}

#[cfg(test)]
use super::QueryEntry;

impl<const N: usize> EncryptedEntry<N> {
    #[cfg(test)]
    pub fn contains(&self, q: &QueryEntry<N>) -> bool {
        // A term-less query entry (a value selector, or a structural node)
        // matches on selector presence alone; a term-carrying one also
        // requires the term to agree — mirroring the DB-side containment
        // semantics.
        self.tokenized_selector == q.0
            && match &q.1 {
                None => true,
                Some(term) => self.term.as_ref() == Some(term),
            }
    }
}

#[cfg(test)]
mod tests {
    use crate::encryption::{
        json_indexer::ste_vec::encrypted_term::EncryptedSteVecTerm, EncryptedEntry,
        TokenizedSelector,
    };
    use serde_json::json;
    use std::vec;

    fn make_encrypted_entry(term: Option<EncryptedSteVecTerm>) -> EncryptedEntry<16> {
        // Raw AEAD output only — the envelope wire format keeps the key
        // material in the document's KeyHeader, not in each entry.
        EncryptedEntry::new(TokenizedSelector([0; 16]), term, vec![1, 2, 3], false)
    }

    /// The wire form of the raw `[1, 2, 3]` ciphertext bytes.
    fn c_base85() -> String {
        base85::encode(&[1, 2, 3])
    }

    mod term_less {
        use super::*;

        // A presence-only entry — a value entry, or the path entry of a
        // bool/null/object/array node. No term key appears on the wire.
        fn expected() -> serde_json::Value {
            json!({
                "parent_is_array": false,
                // Hex-encoded tokenized_selector
                "s": "00000000000000000000000000000000",
                // Base85-encoded raw AEAD ciphertext
                "c": c_base85()
            })
        }

        #[test]
        fn serialize_shape() {
            let entry = make_encrypted_entry(None);
            let serialized = serde_json::to_string(&entry).unwrap();

            assert_eq!(
                serde_json::from_str::<serde_json::Value>(&serialized).unwrap(),
                expected()
            );
        }

        #[test]
        fn deserialize_shape() {
            let serialized = expected();
            let entry: EncryptedEntry<16> = serde_json::from_value(serialized).unwrap();
            assert!(entry.term.is_none());
        }

        /// A legacy entry carrying the retired `hm` key deserializes to a
        /// term-less entry rather than erroring: the flattened
        /// `Option<EncryptedSteVecTerm>` matches neither the `op` nor the `oc`
        /// variant, so the stray key is ignored and `term` is `None`. (The
        /// standalone `EncryptedSteVecTerm` does reject `{"hm": ...}` — see
        /// `encrypted_term::tests::legacy_hm_key_does_not_deserialize_as_a_term`
        /// — but at the *entry* level the `Option` wrapper degrades gracefully,
        /// which is what makes a stray legacy `hm` a tolerated no-op.)
        #[test]
        fn legacy_stray_hm_is_ignored_yielding_a_term_less_entry() {
            let mut with_hm = expected();
            with_hm["hm"] = json!("abcd");
            let entry: EncryptedEntry<16> = serde_json::from_value(with_hm).unwrap();
            assert!(entry.term.is_none());
        }
    }

    mod ore {
        use super::*;
        use cllw_ore::OreCllw8VariableV1;

        // ORE terms collapse to a single variable-width ciphertext field
        // `oc` with no ciphertext-side domain tag byte. Numeric/string
        // disjointness is enforced on the plaintext side by `OrderableTerm`'s
        // type-tag bit; the wire form is just the raw ORE bytes.
        fn expected() -> serde_json::Value {
            json!({
                "parent_is_array": false,
                "s": "00000000000000000000000000000000",
                "oc": "04040404040404040404040404040404040404",
                "c": c_base85()
            })
        }

        #[test]
        fn serialize_shape() {
            let bytes = vec![4; 19];
            let entry = make_encrypted_entry(Some(EncryptedSteVecTerm::Standard {
                oc: OreCllw8VariableV1::from(bytes),
            }));

            let serialized = serde_json::to_string(&entry).unwrap();

            assert_eq!(
                serde_json::from_str::<serde_json::Value>(&serialized).unwrap(),
                expected()
            );
        }

        #[test]
        fn deserialize_shape() {
            let serialized = expected();
            let entry: EncryptedEntry<16> = serde_json::from_value(serialized).unwrap();
            assert!(entry.term.is_some());
        }
    }

    mod ope {
        use super::*;
        use cllw_ore::OpeCllw8VariableV1;

        fn expected() -> serde_json::Value {
            json!({
                "parent_is_array": false,
                "s": "00000000000000000000000000000000",
                "op": "07070707070707070707",
                "c": c_base85()
            })
        }

        #[test]
        fn serialize_shape() {
            let bytes = vec![7; 10];
            let entry = make_encrypted_entry(Some(EncryptedSteVecTerm::Compat {
                op: OpeCllw8VariableV1::from_bytes(bytes),
            }));

            let serialized = serde_json::to_string(&entry).unwrap();

            assert_eq!(
                serde_json::from_str::<serde_json::Value>(&serialized).unwrap(),
                expected()
            );
        }

        #[test]
        fn deserialize_shape() {
            let serialized = expected();
            let entry: EncryptedEntry<16> = serde_json::from_value(serialized).unwrap();
            assert!(entry.term.is_some());
        }
    }
}