Skip to main content

vitaminc_ore/
query.rs

1//! The query side of an order scheme: what a caller makes from the value
2//! it is searching for, to compare against a space's stored terms.
3//!
4//! A block ORE scheme (Lewi-Wu) has two halves. The right half is what
5//! a stored term needs to be compared against; the left half is what a
6//! comparison needs from the other side, and a left half alone is a
7//! **query**: smaller than a stored term, made from the value being
8//! searched for, and enough to drive the comparison against every term
9//! of the space. A scheme whose terms are their own queries, as the CLWW
10//! schemes are, implements this with `Query = Term`.
11//!
12//! A caller makes one through the space, as it makes a term:
13//! `space.encrypt_query(value)`, and compares it with
14//! `space.compare_query(&query, &term)`. A query exists for that
15//! comparison and is not persisted or logged; [`OrderQuery`] says why.
16
17use core::cmp::Ordering;
18
19use vitaminc_aead::Encrypt;
20use vitaminc_normalize::NormalizeError;
21use vitaminc_protected::OpaqueDebug;
22
23use crate::scheme::{same, OrderError, OrderPlaintext, OrderScheme, OrderSpace, OrderTerm, Scalar};
24use crate::target::{
25    FitsIn, FixedWidth, NullPolicy, Nulls, RuntimeNulls, StaticTarget, Supports, Target,
26};
27use crate::{NullOrder, OrderTarget};
28
29/// An order scheme that makes queries: what a caller makes from the
30/// value it is searching for, to compare against the space's stored
31/// terms.
32///
33/// Implemented beside [`OrderScheme`] by a scheme that has a query side,
34/// and trivially, with `Query = Term`, by one whose terms are their own
35/// queries. A caller does not call it: it makes queries through
36/// [`OrderSpace::encrypt_query`] and compares them through
37/// [`OrderSpace::compare_query`], so a query is bound to the same salt
38/// as the terms it is compared with.
39///
40/// A query exists for the comparison at hand, and should not be
41/// persisted or logged. Held beside the stored terms, it orders every
42/// one of them against the searched value, for whoever holds both. And
43/// block ORE's queries are deterministic, one value in one space giving
44/// one query, so `==` on two of its queries is equality of the values
45/// searched for, and a log of queries is a log of what was searched.
46///
47/// That is the whole of what a query is guarded against: being kept. It
48/// is not wiped from memory when it drops, and `Query` has no bound that
49/// would make it wipe itself, unlike a scheme's
50/// [`Space`](OrderScheme::Space). A query is made to be sent to the
51/// database, which sees it in order to compare with it, and the process
52/// that made it also holds the plaintext it came from and the scheme's
53/// key. Bytes of a query left in that process's memory give away less
54/// than what sits beside them.
55pub trait OrderQuery: OrderScheme {
56    /// What one query encrypts to. For a scheme whose terms are their own
57    /// queries, [`Term`](OrderScheme::Term). It need not wipe itself; see
58    /// above for why.
59    type Query: AsRef<[u8]>;
60
61    /// Encrypt a sealed plaintext of `space` as a query, with what
62    /// [`open`](OrderScheme::open) made under the space's salt.
63    ///
64    /// The plaintext is of a target the scheme [`Supports`]: the space
65    /// checks before calling. So the only failure is the scheme's own.
66    ///
67    /// # Errors
68    ///
69    /// The scheme's own failure.
70    fn encrypt_query(
71        &self,
72        space: &Self::Space,
73        plaintext: &OrderPlaintext,
74    ) -> Result<Self::Query, Self::Error>;
75
76    /// The order of the value `query` encrypts relative to the value
77    /// `term` encrypts: `Less` when the queried value sorts before the
78    /// stored one. Needs no key, as [`compare`](OrderScheme::compare)
79    /// does not.
80    ///
81    /// `None` says the two cannot be compared: one is not this scheme's,
82    /// or the two were not made to compare. The same contract as
83    /// [`compare`](OrderScheme::compare): across spaces the noise is in
84    /// the direction and never in equality.
85    fn compare_query(query: &Self::Query, term: &Self::Term) -> Option<Ordering>;
86}
87
88/// One query of an order space: the scheme's query, labelled with the
89/// target it was made under, as an [`OrderTerm`] is.
90///
91/// Under a marker from [`target`](crate::target) the label is a
92/// zero-sized type, so a query of one target, or of one null policy,
93/// cannot meet a term of another in [`OrderSpace::compare_query`]: that
94/// does not compile. Under an [`OrderTarget`] the label is stored, and a
95/// query and a term of different targets or policies compare as `None`
96/// instead of as noise.
97///
98/// A query is not a term: it has its own type, so it cannot be compared
99/// where a term is expected, and the compiler refuses
100/// `space.compare(&query, &term)`. Its bytes are bytes, though, as a
101/// term's are: [`as_bytes`](Self::as_bytes) and `AsRef<[u8]>` are there
102/// so a caller can send the query to wherever the terms are, and nothing
103/// stops a caller writing them into a term's place. Under block ORE a
104/// query has no right half, so every later `compare` against those bytes
105/// returns `None`; under a scheme whose `Query` is its `Term` the mistake
106/// is invisible.
107///
108/// `==` compares the bytes. Whether that is equality of the values
109/// searched for depends on the scheme: under block ORE, whose queries are
110/// deterministic, it is. A query is for the comparison at hand and should
111/// not be persisted or logged, as [`OrderQuery`] explains, so `Debug`
112/// shows the target and masks the query.
113#[derive(OpaqueDebug, Clone, PartialEq, Eq)]
114pub struct QueryTerm<R, T: Target> {
115    raw: R,
116    #[non_sensitive]
117    target: T,
118}
119
120impl<R, T: Target> QueryTerm<R, T> {
121    /// The target the query was made under.
122    pub fn target(&self) -> OrderTarget {
123        self.target.runtime()
124    }
125
126    /// Where the space the query was made in places nulls, or `None` for
127    /// a space that gives them no term.
128    pub fn null_order(&self) -> Option<NullOrder> {
129        self.target.null_order()
130    }
131
132    /// The scheme's own query.
133    pub fn raw(&self) -> &R {
134        &self.raw
135    }
136
137    /// The scheme's own query, unlabelled.
138    pub fn into_raw(self) -> R {
139        self.raw
140    }
141}
142
143impl<R: AsRef<[u8]>, T: Target> QueryTerm<R, T> {
144    /// The query's bytes: the scheme's query's.
145    pub fn as_bytes(&self) -> &[u8] {
146        self.raw.as_ref()
147    }
148}
149
150impl<R: AsRef<[u8]>, T: Target> AsRef<[u8]> for QueryTerm<R, T> {
151    fn as_ref(&self) -> &[u8] {
152        self.raw.as_ref()
153    }
154}
155
156impl<S: OrderQuery + Supports<T> + Supports<OrderTarget> + ?Sized, T: Target> OrderSpace<'_, S, T> {
157    /// Encrypt one scalar as a query of this space's target, checking it
158    /// while the program runs, as
159    /// [`encrypt_checked`](Self::encrypt_checked) checks a term.
160    ///
161    /// The query is for the comparison at hand: held beside the stored
162    /// terms it orders every one of them against the value, so it should
163    /// not be persisted or logged (see [`OrderQuery`]).
164    ///
165    /// In a space that places nulls, a null (`None::<i32>`, or a plain
166    /// null) is the sentinel's query, as [`null_query`](OrderSpace::null_query)
167    /// makes it; in one that does not, a null is refused with
168    /// [`NormalizeError::NoDomain`].
169    ///
170    /// # Errors
171    ///
172    /// As [`encrypt_checked`](Self::encrypt_checked).
173    pub fn encrypt_query_checked<V: Encrypt>(
174        &self,
175        value: V,
176    ) -> Result<QueryTerm<S::Query, T>, OrderError<S::Error>> {
177        self.supported()?;
178        let plaintext = value.visit(Scalar::of(self.target))?;
179        self.query_opened(&plaintext).map_err(OrderError::Backend)
180    }
181
182    /// Encrypt a plaintext [`order_terms`](crate::order_terms) collected
183    /// from a whole value as a query.
184    ///
185    /// # Errors
186    ///
187    /// [`OrderError::Term`] with [`NormalizeError::Mismatch`] if it was
188    /// encoded for another space than this one (another target, or
189    /// another null policy), else as
190    /// [`encrypt_query_checked`](Self::encrypt_query_checked).
191    pub fn encrypt_query_term(
192        &self,
193        plaintext: &OrderPlaintext,
194    ) -> Result<QueryTerm<S::Query, T>, OrderError<S::Error>> {
195        if plaintext.domain() != self.domain() {
196            return Err(OrderError::Term(NormalizeError::Mismatch));
197        }
198        self.supported()?;
199        self.query_opened(plaintext).map_err(OrderError::Backend)
200    }
201
202    /// A query the scheme already made in this space, as the space's
203    /// own: it then compares with the space's terms. The caller vouches
204    /// that it was made here; the space cannot tell.
205    pub fn query(&self, raw: S::Query) -> QueryTerm<S::Query, T> {
206        QueryTerm {
207            raw,
208            target: self.target,
209        }
210    }
211
212    /// The order of the value `query` encrypts relative to the value
213    /// `term` encrypts, [`OrderQuery::compare_query`]: `Less` when the
214    /// queried value sorts before the stored one.
215    ///
216    /// Both must be this space's, as [`compare`](Self::compare) asks of
217    /// its terms: a query and a term of different targets or null
218    /// policies, or of another than this space's, are `None`.
219    pub fn compare_query(
220        &self,
221        query: &QueryTerm<S::Query, T>,
222        term: &OrderTerm<S::Term, T>,
223    ) -> Option<Ordering> {
224        (same(query.target, self.target) && same(term.target, self.target))
225            .then(|| S::compare_query(&query.raw, term.raw()))
226            .flatten()
227    }
228
229    /// Open the space if it is not, then make one query of it.
230    fn query_opened(&self, plaintext: &OrderPlaintext) -> Result<QueryTerm<S::Query, T>, S::Error> {
231        let opened = self.opened()?;
232        self.scheme
233            .encrypt_query(opened, plaintext)
234            .map(|raw| self.query(raw))
235    }
236}
237
238impl<S: OrderQuery + Supports<T> + Supports<OrderTarget> + ?Sized, T: StaticTarget>
239    OrderSpace<'_, S, T>
240{
241    /// Encrypt a value that always fits this space's target as a query,
242    /// as [`encrypt`](Self::encrypt) makes a term of one. The compiler
243    /// refuses a value that might not fit, and names
244    /// [`encrypt_query_checked`](Self::encrypt_query_checked) for it.
245    ///
246    /// The query is for the comparison at hand: held beside the stored
247    /// terms it orders every one of them against the value, so it should
248    /// not be persisted or logged. Under block ORE it is deterministic, so
249    /// a log of queries is a log of what was searched (see
250    /// [`OrderQuery`]).
251    ///
252    /// # Errors
253    ///
254    /// The scheme's own failure, and nothing else.
255    pub fn encrypt_query<V: FitsIn<T>>(
256        &self,
257        value: V,
258    ) -> Result<QueryTerm<S::Query, T>, S::Error> {
259        let plaintext = match value.visit(Scalar::of(T::default())) {
260            Ok(plaintext) => plaintext,
261            // As in `encrypt`: `FitsIn` is a sealed table the encoding
262            // never refuses, exercised at its extremes by this crate's
263            // tests.
264            Err(refused) => unreachable!("a `FitsIn` value was refused: {refused}"),
265        };
266        self.query_opened(&plaintext)
267    }
268}
269
270impl<
271        S: OrderQuery + Supports<T> + Supports<OrderTarget> + ?Sized,
272        T: FixedWidth,
273        P: NullPolicy,
274    > OrderSpace<'_, S, Nulls<T, P>>
275{
276    /// The query of a null in this space: its sentinel, as a query. The
277    /// same as [`encrypt_query_checked`](OrderSpace::encrypt_query_checked)
278    /// of any null.
279    ///
280    /// A layer answers `IS NULL` with an equality comparison against it,
281    /// and `IS NOT NULL` with the negation. It is also the bound a range
282    /// needs, because SQL comparisons never match a null: under
283    /// nulls-first, `age < 10` is "above the sentinel and below 10", and
284    /// under nulls-last `age > 10` is "above 10 and below the sentinel".
285    /// Building the predicate is the layer's work.
286    ///
287    /// # Errors
288    ///
289    /// The scheme's own failure, and nothing else: the scheme
290    /// [`Supports`] the target.
291    #[allow(clippy::type_complexity)]
292    pub fn null_query(&self) -> Result<QueryTerm<S::Query, Nulls<T, P>>, S::Error> {
293        self.query_opened(&self.sentinel())
294    }
295}
296
297impl<S: OrderQuery + Supports<OrderTarget> + ?Sized> OrderSpace<'_, S, RuntimeNulls> {
298    /// The query of a null in this space, for a target chosen while the
299    /// program runs: its sentinel, as a query. As
300    /// [`null_query`](OrderSpace::null_query) on a marker target, which
301    /// says how a layer uses it.
302    ///
303    /// # Errors
304    ///
305    /// [`OrderError::Unsupported`] if the scheme does not take this
306    /// target, [`OrderError::Backend`] if the scheme failed.
307    #[allow(clippy::type_complexity)]
308    pub fn null_query(&self) -> Result<QueryTerm<S::Query, RuntimeNulls>, OrderError<S::Error>> {
309        self.supported()?;
310        self.query_opened(&self.sentinel())
311            .map_err(OrderError::Backend)
312    }
313}