cipherstash-client 0.36.0

The official CipherStash SDK
Documentation
use cllw_ore::{OpeCllw8VariableV1, OreCllw8VariableV1};
use hex::{FromHex, FromHexError};
use serde::{Deserialize, Serialize};

/// Represents an encrypted term in a STE vector. Carries either a MAC (for
/// bool/null/array/object/root leaves) or a single tagged-plaintext orderable
/// ciphertext (OPE in Compat mode, ORE in Standard mode). Numeric and string
/// plaintexts share the same orderable variant; domain separation is enforced
/// on the **plaintext** bit stream (see
/// [`super::priv_state::ste_plaintext_term::OrderableTerm`]), not by tagging
/// the ciphertext after the fact.
#[derive(Debug, PartialEq, Eq, PartialOrd, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum EncryptedSteVecTerm {
    Compat(EncryptedSteVecTermCompat),
    Standard(EncryptedSteVecTermStandard),
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EncryptedSteVecTermCompat {
    /// HMAC-SHA256 MAC for bool/null/array/object/root leaves.
    ///
    /// Serializes as `"hm"` (same as `Standard::Mac`) — both modes share the
    /// term-side MAC primitive (HMAC); only the orderable term differs (OPE
    /// vs ORE).
    #[serde(with = "hex::serde", rename = "hm")]
    Mac(Mac),

    /// CLLW OPE ciphertext for the single orderable domain (Number ∪ String).
    /// Numeric and string plaintexts produce ciphertexts in disjoint ranges
    /// because each plaintext is tagged at the bit-stream level before OPE
    /// (see [`super::priv_state::ste_plaintext_term::OrderableTerm`]). Lex
    /// byte comparison of the ciphertext is correct under CLLW OPE.
    #[serde(with = "hex::serde", rename = "op")]
    Ope(OpeCllw8VariableV1),
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EncryptedSteVecTermStandard {
    /// HMAC-SHA256 MAC for bool/null/array/object/root leaves.
    #[serde(with = "hex::serde", rename = "hm")]
    Mac(Mac),

    /// CLLW ORE ciphertext for the single orderable domain (Number ∪ String).
    /// Numeric and string plaintexts produce ciphertexts in disjoint ranges
    /// because each plaintext is tagged at the bit-stream level before ORE
    /// (see [`super::priv_state::ste_plaintext_term::OrderableTerm`]). The
    /// derived `PartialOrd`/`Ord` use `OreCllw8VariableV1`'s ORE-aware
    /// comparison (first-differing-byte `y + 1 == x`), not plain lex order.
    #[serde(with = "hex::serde", rename = "oc")]
    Ore(OreCllw8VariableV1),
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Mac(Vec<u8>);

impl AsRef<[u8]> for Mac {
    fn as_ref(&self) -> &[u8] {
        self.0.as_ref()
    }
}

impl FromHex for Mac {
    type Error = FromHexError;

    fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
        Ok(Self(Vec::from_hex(hex.as_ref())?))
    }
}

impl Mac {
    pub(crate) fn new(bytes: Vec<u8>) -> Self {
        Self(bytes)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn compat_mac_serializes_as_hm() {
        let term = EncryptedSteVecTermCompat::Mac(Mac::new(vec![0xab, 0xcd]));
        assert_eq!(serde_json::to_value(&term).unwrap(), json!({"hm": "abcd"}));
    }

    #[test]
    fn compat_ope_serializes_as_op() {
        let term = EncryptedSteVecTermCompat::Ope(OpeCllw8VariableV1::from_bytes(vec![1, 2, 3]));
        assert_eq!(
            serde_json::to_value(&term).unwrap(),
            json!({"op": "010203"})
        );
    }

    #[test]
    fn compat_ope_roundtrips_through_serde() {
        let term = EncryptedSteVecTermCompat::Ope(OpeCllw8VariableV1::from_bytes(vec![0xde, 0xad]));
        let s = serde_json::to_string(&term).unwrap();
        let back: EncryptedSteVecTermCompat = serde_json::from_str(&s).unwrap();
        assert_eq!(back, term);
    }

    #[test]
    fn standard_ore_serializes_as_oc() {
        let term = EncryptedSteVecTermStandard::Ore(OreCllw8VariableV1::from(vec![0x42, 0x43]));
        assert_eq!(serde_json::to_value(&term).unwrap(), json!({"oc": "4243"}));
    }

    #[test]
    fn standard_ore_roundtrips_through_serde() {
        let term = EncryptedSteVecTermStandard::Ore(OreCllw8VariableV1::from(vec![0x99]));
        let s = serde_json::to_string(&term).unwrap();
        let back: EncryptedSteVecTermStandard = serde_json::from_str(&s).unwrap();
        assert_eq!(back, term);
    }

    #[test]
    fn compat_and_standard_mac_share_hm_tag() {
        // Both modes use HMAC-SHA256 for MAC terms; the wire tag is "hm" in
        // both. This is what lets the outer `untagged` Encrypted SteVec term
        // deserialize MACs back into either branch.
        let compat = EncryptedSteVecTermCompat::Mac(Mac::new(vec![1, 2]));
        let standard = EncryptedSteVecTermStandard::Mac(Mac::new(vec![1, 2]));
        assert_eq!(
            serde_json::to_value(&compat).unwrap(),
            serde_json::to_value(&standard).unwrap()
        );
    }
}