Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions packages/ore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ A caller opens a column on a scheme with its target and the context
its terms compare within, and encrypts values there:

```rust
use std::cmp::Ordering;
use vitaminc_ore::{Int, Logical, NormalizeError, OrderEncrypt, OrderError, OrderTarget, Physical, Raw};
# use vitaminc_ore::{Context, OrderPlaintext, OrderScheme};
# /// Keeps the bytes as they are, for the example. Any scheme will do.
Expand All @@ -53,13 +54,17 @@ use vitaminc_ore::{Int, Logical, NormalizeError, OrderEncrypt, OrderError, Order
# 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"));
let three = total.encrypt(3i8)?;
let forty = total.encrypt(40i64)?;
assert!(three < forty);
// The column compares its terms; a term alone has no order.
assert_eq!(total.compare(&three, &forty), Some(Ordering::Less));

// Outside the column's type, or of another kind: refused.
assert!(matches!(
Expand Down Expand Up @@ -137,6 +142,7 @@ is implemented for every scheme and cannot be overridden, so no scheme
can change what its terms are bound to.

```rust
use core::cmp::Ordering;
use core::convert::Infallible;
use vitaminc_ore::{Context, OrderError, OrderPlaintext, OrderScheme};
use vitaminc_protected::Controlled;
Expand All @@ -162,7 +168,20 @@ impl OrderScheme for FourBytes {
term.extend_from_slice(plaintext.bytes().risky_ref());
Ok(term)
}

/// The scheme's own comparison, without a key. `None` for a term that
/// is not this scheme's, or for two terms of different columns: here,
/// a term shorter than its four bytes, or two salts that differ.
fn compare(a: &Vec<u8>, b: &Vec<u8>) -> Option<Ordering> {
let (a_salt, a_bytes) = a.split_at(a.len().checked_sub(4)?);
let (b_salt, b_bytes) = b.split_at(b.len().checked_sub(4)?);
(a_salt == b_salt).then(|| a_bytes.cmp(b_bytes))
}
}

assert_eq!(FourBytes::compare(&b"salt\0\0\0\x01".to_vec(), &b"salt\0\0\0\x02".to_vec()), Some(Ordering::Less));
assert_eq!(FourBytes::compare(&b"salt\0\0\0\x01".to_vec(), &b"SALT\0\0\0\x02".to_vec()), None);
assert_eq!(FourBytes::compare(&vec![], &vec![]), None);
```

The salt is `PAE("vitaminc/ore/v1", scheme, domain, context)`, derived
Expand All @@ -174,7 +193,11 @@ the way a PRF is keyed, so an index layer constructs any scheme the same
way.

A term compares only with terms of the same scheme, key, target and
context, which is what a column is. A column's leakage is the combined
context, which is what a column is, through `column.compare(&a, &b)`
(the scheme's [`OrderScheme::compare`], which needs no key). A term is
plain bytes and records nothing of its column, so it is not `Ord`: a
promise to order any two terms is one no order scheme can keep across
columns, and a caller keeps a column's terms together. A column's leakage is the combined
leakage of every term it holds: an order term reveals order, an equality
term reveals equality, and a column that carries both reveals both.
Comment on lines +200 to 202

Copy link
Copy Markdown
Contributor

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.

Suggested change
columns, and a caller keeps a column's terms together. A column's leakage is the combined
leakage of every term it holds: an order term reveals order, an equality
term reveals equality, and a column that carries both reveals both.
columns, and a caller keeps a column's terms together. A column's
leakage is the combined leakage of every term it holds: an order term
reveals order, an equality term reveals equality, and a column that
carries both reveals both.


Expand Down
47 changes: 42 additions & 5 deletions packages/ore/src/scheme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor This and OrderColumn::compare (line 310) promise an order a caller may sort with, but return Option, so the pattern the tests use, sort_by(|a, b| column.compare(a, b).expect(..)) (tests/conformance.rs:650), panics on one malformed stored term: a FourBytes term under four bytes returns None. Add a fallible helper such as OrderColumn::sort(&self, &mut [S::Term]) -> Result<(), _>, or document that stored terms are validated before sorting.

/// 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
Expand All @@ -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.
Expand All @@ -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"));
Expand Down Expand Up @@ -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
Expand Down
29 changes: 24 additions & 5 deletions packages/ore/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor MockTerm has no key field: encrypt_plaintext appends the key to salt (line 52). Say salt is the column's salt with the mock's key appended.

/// 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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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]> {
Expand Down
66 changes: 56 additions & 10 deletions packages/ore/tests/conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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");
Expand All @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor This repeats the assert above: OrderColumn::compare is S::compare(a, b) (scheme.rs:319), so the two cannot fail independently. Drop it, and the OrderScheme import on line 18 that only it uses.

}
}

#[test]
fn a_mock_term_is_the_plaintext_bytes() {
// So the tests above are about the plaintext layer, not the mock.
Expand Down
Loading