Repository navigation
feat(ore)!: compare order terms through the scheme and column, not through Ord #464
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| //! is [`order`]) or a salt it could derive differently from another | ||
| //! scheme. | ||
|
|
||
| use core::cmp::Ordering; | ||
| use core::fmt; | ||
|
|
||
| use vitaminc_aead::plaintext::{self, SourceError}; | ||
|
|
@@ -125,8 +126,12 @@ impl<E> OrderError<E> { | |
| /// | ||
| /// Terms compare only between terms of the same scheme, key, target and | ||
| /// context: a column. The scheme keeps its own comparison (CLLW ORE and | ||
| /// block ORE do not compare byte by byte) through its term's `Ord`, and | ||
| /// its own stored bytes through `AsRef<[u8]>`. | ||
| /// block ORE do not compare byte by byte) in [`compare`](Self::compare), | ||
| /// and its own stored bytes through `AsRef<[u8]>`. A term is not `Ord`: | ||
| /// that would promise an order between any two terms, and no term carries | ||
| /// which column it came from, so two terms of different columns would | ||
| /// compare as noise and break every sort and tree that trusted the | ||
| /// promise. | ||
| /// | ||
| /// Implemented by the scheme crates behind a `vitaminc` feature, and by a | ||
| /// mock in this crate's tests. An implementation reads the plaintext's | ||
|
|
@@ -139,7 +144,7 @@ pub trait OrderScheme { | |
| const NAME: &'static str; | ||
|
|
||
| /// What one term encrypts to. | ||
| type Term: Ord + AsRef<[u8]>; | ||
| type Term: AsRef<[u8]>; | ||
|
|
||
| /// The scheme's own failure. [`Infallible`](core::convert::Infallible) | ||
| /// for a scheme that cannot fail once keyed. | ||
|
|
@@ -159,6 +164,20 @@ pub trait OrderScheme { | |
| plaintext: &OrderPlaintext, | ||
| salt: &Context<'_>, | ||
| ) -> Result<Self::Term, OrderError<Self::Error>>; | ||
|
|
||
| /// The order of two terms of one column, as the order of the values | ||
| /// they encrypt. Needs no key: that is what makes the scheme an order | ||
| /// scheme. | ||
| /// | ||
| /// Over the terms of one column this is a total order, so a caller may | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor This and |
||
| /// sort with it. `None` says the two cannot be compared: one is not a | ||
| /// term of this scheme, or the two were not made to compare (block | ||
| /// ORE under different padding). A scheme returns `None` wherever it | ||
| /// can tell, and never falls back to comparing bytes it cannot order. | ||
| /// It cannot tell everything: two well-formed terms of different | ||
| /// columns may compare as noise, so a caller keeps a column's terms | ||
| /// together, as [`OrderColumn::compare`] says. | ||
| fn compare(a: &Self::Term, b: &Self::Term) -> Option<Ordering>; | ||
| } | ||
|
|
||
| /// What a caller does with any [`OrderScheme`]: open a column. Implemented | ||
|
|
@@ -172,6 +191,7 @@ pub trait OrderEncrypt: OrderScheme { | |
| /// # Examples | ||
| /// | ||
| /// ``` | ||
| /// use std::cmp::Ordering; | ||
| /// use vitaminc_ore::{Int, OrderEncrypt, OrderTarget, Physical, Raw}; | ||
| /// # use vitaminc_ore::{Context, OrderError, OrderPlaintext, OrderScheme}; | ||
| /// # /// Keeps the bytes as they are, for the example. Any scheme will do. | ||
|
|
@@ -184,12 +204,17 @@ pub trait OrderEncrypt: OrderScheme { | |
| /// # use vitaminc_protected::Controlled; | ||
| /// # Ok(p.bytes().risky_ref().clone()) | ||
| /// # } | ||
| /// # fn compare(a: &Vec<u8>, b: &Vec<u8>) -> Option<Ordering> { | ||
| /// # Some(a.cmp(b)) | ||
| /// # } | ||
| /// # } | ||
| /// # let scheme = Clear; | ||
| /// | ||
| /// let total = scheme.column(OrderTarget::I32, ("orders", "total")); | ||
| /// // Whatever width a value arrives at, it is encoded as the column's. | ||
| /// assert!(total.encrypt(41i8)? < total.encrypt(42i64)?); | ||
| /// // Whatever width a value arrives at, it is encoded as the column's, | ||
| /// // and the column compares its terms. | ||
| /// let (a, b) = (total.encrypt(41i8)?, total.encrypt(42i64)?); | ||
| /// assert_eq!(total.compare(&a, &b), Some(Ordering::Less)); | ||
| /// | ||
| /// // A typed scalar goes in as `Raw`, into a column of its type. | ||
| /// let placed = scheme.column(OrderTarget::Date, ("orders", "placed")); | ||
|
|
@@ -281,6 +306,18 @@ impl<S: OrderScheme + ?Sized> OrderColumn<'_, S> { | |
| } | ||
| self.scheme.encrypt_plaintext(plaintext, &self.salt) | ||
| } | ||
|
|
||
| /// The order of two of this column's terms, as the order of the values | ||
| /// they encrypt: [`OrderScheme::compare`]. A total order over the | ||
| /// column's terms, so they sort by it. | ||
| /// | ||
| /// Both terms must be this column's. A term records nothing of its | ||
| /// column, so the scheme cannot always tell two columns' terms apart, | ||
| /// and may order them as noise. `None` where it can tell: a term not | ||
| /// of this scheme, or two terms not made to compare. | ||
| pub fn compare(&self, a: &S::Term, b: &S::Term) -> Option<Ordering> { | ||
| S::compare(a, b) | ||
| } | ||
| } | ||
|
|
||
| /// A scheme keyed from secret bytes, as | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,21 +5,28 @@ | |
|
|
||
| #![allow(dead_code)] | ||
|
|
||
| use core::cmp::Ordering; | ||
| use core::convert::Infallible; | ||
|
|
||
| use vitaminc_ore::{Context, OrderError, OrderKeyInit, OrderPlaintext, OrderScheme}; | ||
| use vitaminc_protected::{Controlled, Protected}; | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor |
||
| /// A term from a mock: the salt it was bound under and the plaintext | ||
| /// bytes, which it "encrypts" by keeping. Orders by salt then bytes, so | ||
| /// terms under one salt order as the values do and terms under different | ||
| /// salts never compare equal. | ||
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] | ||
| /// A term from a mock: the salt and key it was bound under and the | ||
| /// plaintext bytes, which it "encrypts" by keeping. Terms under one salt | ||
| /// and key compare as the values do; a mock refuses to compare terms of | ||
| /// different columns, which a real scheme cannot always tell apart. | ||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct MockTerm { | ||
| pub salt: Vec<u8>, | ||
| pub bytes: Vec<u8>, | ||
| } | ||
|
|
||
| /// The mocks' comparison: the values' order within one column, `None` | ||
| /// across columns. | ||
| fn compare(a: &MockTerm, b: &MockTerm) -> Option<Ordering> { | ||
| (a.salt == b.salt).then(|| a.bytes.cmp(&b.bytes)) | ||
| } | ||
|
|
||
| impl AsRef<[u8]> for MockTerm { | ||
| fn as_ref(&self) -> &[u8] { | ||
| &self.bytes | ||
|
|
@@ -48,6 +55,10 @@ impl OrderScheme for Mock { | |
| bytes: plaintext.bytes().risky_ref().clone(), | ||
| }) | ||
| } | ||
|
|
||
| fn compare(a: &MockTerm, b: &MockTerm) -> Option<Ordering> { | ||
| compare(a, b) | ||
| } | ||
| } | ||
|
|
||
| /// Why a mock refused key material. | ||
|
|
@@ -92,6 +103,10 @@ impl<const N: usize> OrderScheme for Fixed<N> { | |
| } | ||
| self.0.encrypt_plaintext(plaintext, salt) | ||
| } | ||
|
|
||
| fn compare(a: &MockTerm, b: &MockTerm) -> Option<Ordering> { | ||
| compare(a, b) | ||
| } | ||
| } | ||
|
|
||
| /// A scheme whose backend fails. | ||
|
|
@@ -113,6 +128,10 @@ impl OrderScheme for Down { | |
| ) -> Result<MockTerm, OrderError<BackendDown>> { | ||
| Err(OrderError::Backend(BackendDown)) | ||
| } | ||
|
|
||
| fn compare(a: &MockTerm, b: &MockTerm) -> Option<Ordering> { | ||
| compare(a, b) | ||
| } | ||
| } | ||
|
|
||
| pub fn key() -> Protected<[u8; 32]> { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ use vitaminc_aead::{Float, Int, Logical, Physical, TimeUnit}; | |
| use vitaminc_normalize::{domain as equality, normalize}; | ||
| use vitaminc_ore::{ | ||
| domain, order, Context, IntoContext, NormalizeError, OrderEncrypt, OrderError, OrderKeyInit, | ||
| OrderTarget, Raw, | ||
| OrderScheme, OrderTarget, Raw, | ||
| }; | ||
| use vitaminc_protected::{Controlled, Protected}; | ||
|
|
||
|
|
@@ -191,7 +191,12 @@ fn every_kind_has_an_order_target_or_is_a_named_exception() { | |
| column.encrypt_term(&below).expect("mock"), | ||
| column.encrypt_term(&above).expect("mock"), | ||
| ); | ||
| assert_eq!(x.cmp(&y), Ordering::Less, "{}", kind.domain); | ||
| assert_eq!( | ||
| column.compare(&x, &y), | ||
| Some(Ordering::Less), | ||
| "{}", | ||
| kind.domain | ||
| ); | ||
| } | ||
| // The exceptions: no order term under any target, and the walk skips | ||
| // them. | ||
|
|
@@ -574,7 +579,13 @@ fn typed_values_encrypt_into_the_column_of_their_type() { | |
| date, | ||
| typed(T::Date, Logical::Date, Physical::Int(Int::I32(19_000))) | ||
| ); | ||
| assert!(dates.encrypt(day.pred_opt().expect("date")).expect("date") < date); | ||
| assert_eq!( | ||
| dates.compare( | ||
| &dates.encrypt(day.pred_opt().expect("date")).expect("date"), | ||
| &date | ||
| ), | ||
| Some(Ordering::Less) | ||
| ); | ||
| let at = Utc | ||
| .timestamp_opt(1_700_000_000, 123_456_789) | ||
| .single() | ||
|
|
@@ -610,11 +621,10 @@ fn typed_values_encrypt_into_the_column_of_their_type() { | |
| fn order_through_a_column_is_value_order(a: i32, b: i16, text_a: String, text_b: String) -> bool { | ||
| let mock = Mock::new(key()); | ||
| let ints = mock.column(T::I64, "col"); | ||
| let int_order = ints | ||
| .encrypt(a) | ||
| .expect("mock") | ||
| .cmp(&ints.encrypt(b).expect("mock")) | ||
| == i64::from(a).cmp(&i64::from(b)); | ||
| let int_order = ints.compare( | ||
| &ints.encrypt(a).expect("mock"), | ||
| &ints.encrypt(b).expect("mock"), | ||
| ) == Some(i64::from(a).cmp(&i64::from(b))); | ||
| // Bytes compare as bytes; text ties where it folds together, so only | ||
| // bytes are checked for order here. | ||
| let bytes = mock.column(T::Bytes, "col"); | ||
|
|
@@ -623,11 +633,47 @@ fn order_through_a_column_is_value_order(a: i32, b: i16, text_a: String, text_b: | |
| .encrypt(Raw(None, Physical::Bytes(s.as_bytes()))) | ||
| .expect("mock") | ||
| }; | ||
| let byte_order = | ||
| under(&text_a).cmp(&under(&text_b)) == text_a.as_bytes().cmp(text_b.as_bytes()); | ||
| let byte_order = bytes.compare(&under(&text_a), &under(&text_b)) | ||
| == Some(text_a.as_bytes().cmp(text_b.as_bytes())); | ||
| int_order && byte_order | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_column_sorts_its_own_terms_and_refuses_another_columns() { | ||
| let mock = Mock::new(key()); | ||
| let column = mock.column(T::I32, "col"); | ||
| let mut terms: Vec<_> = [5i32, -3, 9, 0] | ||
| .into_iter() | ||
| .map(|v| column.encrypt(v).expect("mock")) | ||
| .collect(); | ||
| // The column's comparison is total over its terms, so they sort by it. | ||
| terms.sort_by(|a, b| column.compare(a, b).expect("one column")); | ||
| assert_eq!( | ||
| terms.iter().map(|t| t.bytes.clone()).collect::<Vec<_>>(), | ||
| [-3i32, 0, 5, 9] | ||
| .into_iter() | ||
| .map(|v| (v as u32 ^ 0x8000_0000).to_be_bytes().to_vec()) | ||
| .collect::<Vec<_>>() | ||
| ); | ||
| // Another column's term, under another context, another key or another | ||
| // scheme, has no order against them: the mock can tell, a real scheme | ||
| // often cannot, so a caller never mixes them. | ||
| let own = &terms[0]; | ||
| let other_context = mock.column(T::I32, "other").encrypt(1i32).expect("mock"); | ||
| let other_key = Mock::new(Protected::new([8; 32])) | ||
| .column(T::I32, "col") | ||
| .encrypt(1i32) | ||
| .expect("mock"); | ||
| let other_scheme = Fixed::<4>(Mock::new(key())) | ||
| .column(T::I32, "col") | ||
| .encrypt(1i32) | ||
| .expect("mock"); | ||
| for foreign in [other_context, other_key, other_scheme] { | ||
| assert_eq!(column.compare(own, &foreign), None); | ||
| assert_eq!(Mock::compare(own, &foreign), None); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor This repeats the assert above: |
||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_mock_term_is_the_plaintext_bytes() { | ||
| // So the tests above are about the plaintext layer, not the mock. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor Line 200 runs to 89 columns; the rest of the file wraps at 72.