use core::cmp::Ordering;
use core::fmt;
use std::sync::OnceLock;
use vitaminc_aead::plaintext::{self, SourceError};
use vitaminc_aead::{Encrypt, FixedType, Logical, MapSink, Physical, SeqSink, Sink};
use vitaminc_context::{Context, IntoContext};
use vitaminc_normalize::NormalizeError;
use vitaminc_protected::{Controlled, OpaqueDebug, Protected};
use crate::order::{encode, label, null_plaintext};
use crate::target::{
FitsIn, FixedWidth, NullPolicy, Nulls, OwnTarget, RuntimeNulls, StaticTarget, Supports, Target,
};
use crate::{NullOrder, OrderTarget};
const SALT_LABEL: &[u8] = b"vitaminc/ore/v1";
#[derive(OpaqueDebug)]
pub struct OrderPlaintext {
#[non_sensitive]
target: OrderTarget,
#[non_sensitive]
nulls: Option<NullOrder>,
#[non_sensitive]
domain: &'static str,
bytes: Protected<Vec<u8>>,
}
impl OrderPlaintext {
pub(crate) fn new(
target: OrderTarget,
nulls: Option<NullOrder>,
domain: &'static str,
bytes: Protected<Vec<u8>>,
) -> Self {
debug_assert_eq!(
label(target, nulls),
Some(domain),
"the domain is the space's label"
);
debug_assert!(
target.width().is_none_or(|w| w == bytes.risky_ref().len()),
"{target:?} encodes to its width"
);
debug_assert!(
!bytes.risky_ref().is_empty(),
"an order plaintext is never empty"
);
Self {
target,
nulls,
domain,
bytes,
}
}
pub fn target(&self) -> OrderTarget {
self.target
}
pub fn null_order(&self) -> Option<NullOrder> {
self.nulls
}
pub fn domain(&self) -> &'static str {
self.domain
}
pub fn width(&self) -> Option<usize> {
self.target.width()
}
pub fn bytes(&self) -> &Protected<Vec<u8>> {
&self.bytes
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum OrderError<E> {
#[error("no order term: {0}")]
Term(#[from] NormalizeError),
#[error("a container has no single order term; walk it for one per scalar")]
NotScalar,
#[error("the value could not be read: {0}")]
Source(SourceError),
#[error("{scheme} has no order term for {domain}")]
Unsupported {
scheme: &'static str,
domain: &'static str,
},
#[error("the order scheme failed: {0}")]
Backend(E),
}
pub trait OrderScheme {
const NAME: &'static str;
type Term: AsRef<[u8]>;
type Error: core::error::Error + Send + Sync + 'static;
type Space: zeroize::ZeroizeOnDrop;
fn open(&self, target: OrderTarget, salt: &Context<'_>) -> Result<Self::Space, Self::Error>;
fn encrypt_plaintext(
&self,
space: &Self::Space,
plaintext: &OrderPlaintext,
) -> Result<Self::Term, Self::Error>;
fn compare(a: &Self::Term, b: &Self::Term) -> Option<Ordering>;
}
pub trait OrderEncrypt: OrderScheme {
fn target<T: Target>(&self, target: T) -> OrderSpace<'_, Self, T>
where
Self: Supports<T> + Supports<OrderTarget>,
{
OrderSpace::new(self, target, &().into_context().encode())
}
fn context<'a, C: IntoContext<'a>>(&self, context: C) -> OrderContext<'_, Self> {
OrderContext {
scheme: self,
context: context.into_context().encode().into_owned(),
}
}
fn encrypt<V>(&self, value: V) -> Result<OrderTerm<Self::Term, V::Target>, Self::Error>
where
V: OwnTarget + FitsIn<V::Target>,
Self: Supports<V::Target> + Supports<OrderTarget>,
{
self.target(V::Target::default()).encrypt(value)
}
#[allow(clippy::type_complexity)]
fn encrypt_checked<V: OwnTarget>(
&self,
value: V,
) -> Result<OrderTerm<Self::Term, V::Target>, OrderError<Self::Error>>
where
Self: Supports<V::Target> + Supports<OrderTarget>,
{
self.target(V::Target::default()).encrypt_checked(value)
}
#[deprecated(since = "0.7.0", note = "use `scheme.target(target).context(context)`")]
fn column<'a, C: IntoContext<'a>>(
&self,
target: OrderTarget,
context: C,
) -> OrderSpace<'_, Self, OrderTarget>
where
Self: Supports<OrderTarget>,
{
self.target(target).context(context)
}
}
impl<S: OrderScheme + ?Sized> OrderEncrypt for S {}
fn salt(scheme: &str, domain: &str, context: &Context<'_>) -> Context<'static> {
Context::pae(&[
SALT_LABEL,
scheme.as_bytes(),
domain.as_bytes(),
context.as_bytes(),
])
}
#[derive(OpaqueDebug)]
pub struct OrderContext<'s, S: OrderScheme + ?Sized> {
scheme: &'s S,
#[non_sensitive]
context: Context<'static>,
}
impl<'s, S: OrderScheme + ?Sized> OrderContext<'s, S> {
pub fn target<T: Target>(&self, target: T) -> OrderSpace<'s, S, T>
where
S: Supports<T> + Supports<OrderTarget>,
{
OrderSpace::new(self.scheme, target, &self.context)
}
pub fn encrypt<V>(&self, value: V) -> Result<OrderTerm<S::Term, V::Target>, S::Error>
where
V: OwnTarget + FitsIn<V::Target>,
S: Supports<V::Target> + Supports<OrderTarget>,
{
self.target(V::Target::default()).encrypt(value)
}
#[allow(clippy::type_complexity)]
pub fn encrypt_checked<V: OwnTarget>(
&self,
value: V,
) -> Result<OrderTerm<S::Term, V::Target>, OrderError<S::Error>>
where
S: Supports<V::Target> + Supports<OrderTarget>,
{
self.target(V::Target::default()).encrypt_checked(value)
}
}
#[derive(OpaqueDebug)]
pub struct OrderSpace<'s, S: OrderScheme + ?Sized, T: Target = OrderTarget> {
pub(crate) scheme: &'s S,
#[non_sensitive]
pub(crate) target: T,
#[non_sensitive]
domain: &'static str,
context: Context<'static>,
salt: Context<'static>,
opened: OnceLock<S::Space>,
}
#[deprecated(since = "0.7.0", note = "use `OrderSpace<'s, S, OrderTarget>`")]
pub type OrderColumn<'s, S> = OrderSpace<'s, S, OrderTarget>;
impl<'s, S: Supports<T> + Supports<OrderTarget> + ?Sized, T: Target> OrderSpace<'s, S, T> {
fn new(scheme: &'s S, target: T, context: &Context<'_>) -> Self {
debug_assert!(
!<S as Supports<T>>::supports(target)
|| <S as Supports<OrderTarget>>::supports(target.runtime()),
"{} implements Supports<{:?}> but refuses it at runtime",
S::NAME,
target.runtime()
);
let domain = label(target.runtime(), target.null_order())
.expect("a null policy is only ever set on a fixed-width target");
Self {
scheme,
target,
domain,
context: context.clone().into_owned(),
salt: salt(S::NAME, domain, context),
opened: OnceLock::new(),
}
}
pub fn open(&self) -> Result<(), S::Error> {
self.opened().map(|_| ())
}
pub(crate) fn opened(&self) -> Result<&S::Space, S::Error> {
if let Some(opened) = self.opened.get() {
return Ok(opened);
}
let opened = self.scheme.open(self.target.runtime(), &self.salt)?;
Ok(self.opened.get_or_init(|| opened))
}
pub fn context<'a, C: IntoContext<'a>>(&self, context: C) -> Self {
Self::new(self.scheme, self.target, &context.into_context().encode())
}
pub fn target(&self) -> OrderTarget {
self.target.runtime()
}
pub fn null_order(&self) -> Option<NullOrder> {
self.target.null_order()
}
pub fn domain(&self) -> &'static str {
self.domain
}
pub fn salt(&self) -> &Context<'static> {
&self.salt
}
pub fn encrypt_checked<V: Encrypt>(
&self,
value: V,
) -> Result<OrderTerm<S::Term, T>, OrderError<S::Error>> {
self.supported()?;
let plaintext = value.visit(Scalar::of(self.target))?;
self.encrypt_plaintext(&plaintext)
}
pub fn encrypt_term(
&self,
plaintext: &OrderPlaintext,
) -> Result<OrderTerm<S::Term, T>, OrderError<S::Error>> {
if plaintext.domain() != self.domain {
return Err(OrderError::Term(NormalizeError::Mismatch));
}
self.supported()?;
self.encrypt_plaintext(plaintext)
}
pub fn term(&self, raw: S::Term) -> OrderTerm<S::Term, T> {
OrderTerm {
raw,
target: self.target,
}
}
pub fn compare(
&self,
a: &OrderTerm<S::Term, T>,
b: &OrderTerm<S::Term, T>,
) -> Option<Ordering> {
(same(a.target, self.target) && same(b.target, self.target))
.then(|| S::compare(&a.raw, &b.raw))
.flatten()
}
pub(crate) fn supported(&self) -> Result<(), OrderError<S::Error>> {
if <S as Supports<OrderTarget>>::supports(self.target.runtime()) {
Ok(())
} else {
Err(OrderError::Unsupported {
scheme: S::NAME,
domain: self.domain,
})
}
}
fn encrypt_plaintext(
&self,
plaintext: &OrderPlaintext,
) -> Result<OrderTerm<S::Term, T>, OrderError<S::Error>> {
self.encrypt_opened(plaintext).map_err(OrderError::Backend)
}
fn encrypt_opened(
&self,
plaintext: &OrderPlaintext,
) -> Result<OrderTerm<S::Term, T>, S::Error> {
let opened = self.opened()?;
self.scheme
.encrypt_plaintext(opened, plaintext)
.map(|raw| self.term(raw))
}
}
impl<S: Supports<T> + Supports<OrderTarget> + ?Sized, T: StaticTarget> OrderSpace<'_, S, T> {
pub fn encrypt<V: FitsIn<T>>(&self, value: V) -> Result<OrderTerm<S::Term, T>, S::Error> {
let plaintext = match value.visit(Scalar::of(T::default())) {
Ok(plaintext) => plaintext,
Err(refused) => unreachable!("a `FitsIn` value was refused: {refused}"),
};
self.encrypt_opened(&plaintext)
}
}
pub(crate) fn same<T: Target>(a: T, b: T) -> bool {
a.runtime() == b.runtime() && a.null_order() == b.null_order()
}
impl<'s, S: Supports<T> + Supports<OrderTarget> + ?Sized, T: FixedWidth> OrderSpace<'s, S, T> {
pub fn nulls<P: NullPolicy>(&self, _: P) -> OrderSpace<'s, S, Nulls<T, P>> {
OrderSpace::new(self.scheme, Nulls::new(self.target), &self.context)
}
}
impl<'s, S: Supports<OrderTarget> + ?Sized> OrderSpace<'s, S, OrderTarget> {
pub fn nulls(
&self,
nulls: NullOrder,
) -> Result<OrderSpace<'s, S, RuntimeNulls>, NormalizeError> {
if self.target.domain_with_nulls(nulls).is_none() {
return Err(NormalizeError::NoDomain);
}
Ok(OrderSpace::<'s, S, RuntimeNulls>::new(
self.scheme,
RuntimeNulls::new(self.target, nulls),
&self.context,
))
}
}
impl<S: Supports<T> + Supports<OrderTarget> + ?Sized, T: Target> OrderSpace<'_, S, T> {
pub(crate) fn sentinel(&self) -> OrderPlaintext {
self.target
.null_order()
.and_then(|order| null_plaintext(self.target.runtime(), order))
.expect("a null policy is only ever set on a fixed-width target")
}
}
impl<S: Supports<T> + Supports<OrderTarget> + ?Sized, T: FixedWidth, P: NullPolicy>
OrderSpace<'_, S, Nulls<T, P>>
{
#[allow(clippy::type_complexity)]
pub fn encrypt_null(&self) -> Result<OrderTerm<S::Term, Nulls<T, P>>, S::Error> {
self.encrypt_opened(&self.sentinel())
}
}
impl<S: Supports<OrderTarget> + ?Sized> OrderSpace<'_, S, RuntimeNulls> {
#[allow(clippy::type_complexity)]
pub fn encrypt_null(&self) -> Result<OrderTerm<S::Term, RuntimeNulls>, OrderError<S::Error>> {
self.supported()?;
self.encrypt_plaintext(&self.sentinel())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderTerm<R, T: Target> {
raw: R,
pub(crate) target: T,
}
impl<R, T: Target> OrderTerm<R, T> {
pub fn target(&self) -> OrderTarget {
self.target.runtime()
}
pub fn null_order(&self) -> Option<NullOrder> {
self.target.null_order()
}
pub fn raw(&self) -> &R {
&self.raw
}
pub fn into_raw(self) -> R {
self.raw
}
}
impl<R: AsRef<[u8]>, T: Target> OrderTerm<R, T> {
pub fn as_bytes(&self) -> &[u8] {
self.raw.as_ref()
}
}
impl<R: AsRef<[u8]>, T: Target> AsRef<[u8]> for OrderTerm<R, T> {
fn as_ref(&self) -> &[u8] {
self.raw.as_ref()
}
}
pub trait OrderKeyInit: OrderScheme + Sized {
type Key: Controlled;
type KeyError: core::error::Error + Send + Sync + 'static;
fn new(key: Self::Key) -> Self;
fn try_from_bytes(key: Protected<Vec<u8>>) -> Result<Self, Self::KeyError>;
}
#[derive(Clone, Copy)]
pub(crate) struct Scalar {
target: OrderTarget,
nulls: Option<NullOrder>,
}
impl Scalar {
pub(crate) fn of<T: Target>(target: T) -> Self {
Self {
target: target.runtime(),
nulls: target.null_order(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum ScalarError {
#[error(transparent)]
Term(NormalizeError),
#[error("not a scalar")]
NotScalar,
#[error(transparent)]
Source(SourceError),
}
impl plaintext::Error for ScalarError {
fn source(error: SourceError) -> Self {
Self::Source(error)
}
}
impl<E> From<ScalarError> for OrderError<E> {
fn from(error: ScalarError) -> Self {
match error {
ScalarError::Term(e) => Self::Term(e),
ScalarError::NotScalar => Self::NotScalar,
ScalarError::Source(e) => Self::Source(e),
}
}
}
impl Sink for Scalar {
type Ok = OrderPlaintext;
type Error = ScalarError;
type Seq = Never;
type Map = Never;
fn scalar(
self,
ty: Option<Logical>,
value: Physical<'_>,
) -> Result<OrderPlaintext, ScalarError> {
encode(self.target, self.nulls, ty, value).map_err(ScalarError::Term)
}
fn typed_null(self, _: FixedType) -> Result<OrderPlaintext, ScalarError> {
self.scalar(None, Physical::Null)
}
fn seq(self, _: Option<usize>) -> Result<Never, ScalarError> {
Err(ScalarError::NotScalar)
}
fn map(self, _: Option<usize>) -> Result<Never, ScalarError> {
Err(ScalarError::NotScalar)
}
fn passthrough<P: Encrypt + Clone>(self, _: P) -> Result<OrderPlaintext, ScalarError> {
Err(ScalarError::NotScalar)
}
}
pub(crate) enum Never {}
impl SeqSink for Never {
type Ok = OrderPlaintext;
type Error = ScalarError;
type Element<'s>
= Scalar
where
Self: 's;
fn element(&mut self) -> Result<Scalar, ScalarError> {
match *self {}
}
fn end(self) -> Result<OrderPlaintext, ScalarError> {
match self {}
}
}
impl MapSink for Never {
type Ok = OrderPlaintext;
type Error = ScalarError;
type Entry<'s>
= Scalar
where
Self: 's;
fn entry(&mut self, _: &str) -> Result<Scalar, ScalarError> {
match *self {}
}
fn end(self) -> Result<OrderPlaintext, ScalarError> {
match self {}
}
}
impl fmt::Display for OrderPlaintext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.domain())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{domain, order};
use vitaminc_aead::Int;
#[test]
fn every_target_has_the_width_its_encoding_has() {
for target in OrderTarget::ALL {
let variable = matches!(target, OrderTarget::Text | OrderTarget::Bytes);
assert_eq!(target.width().is_none(), variable, "{target:?}");
}
let p = order(OrderTarget::I32, None, Physical::Int(Int::I8(1))).expect("term");
assert_eq!(p.width(), Some(4));
assert_eq!(p.bytes().risky_ref().len(), 4);
assert_eq!(p.target(), OrderTarget::I32);
assert_eq!(
order(OrderTarget::Text, None, Physical::Text("a"))
.expect("term")
.width(),
None
);
}
#[test]
fn the_salt_binds_label_scheme_domain_and_context() {
let context = ("tenant", 1u64).into_context().encode();
let salted = salt("mock", domain::INT16, &context);
assert_eq!(
salted,
Context::pae(&[
b"vitaminc/ore/v1",
b"mock",
domain::INT16.as_bytes(),
context.as_bytes()
])
);
assert_ne!(salted, salt("other", domain::INT16, &context));
assert_ne!(salted, salt("mock", domain::UINT16, &context));
assert_ne!(
salted,
salt(
"mock",
domain::INT16,
&("tenant", 2u64).into_context().encode()
)
);
assert_ne!(salted, salt("mock", domain::INT16_NULLS_FIRST, &context));
assert_ne!(salted, salt("mock", domain::INT16_NULLS_LAST, &context));
}
#[test]
fn debug_and_display_show_the_domain_and_mask_the_bytes() {
let p = order(OrderTarget::Text, None, Physical::Text("secret")).expect("term");
let shown = format!("{p:?}");
assert!(shown.contains("Text") && shown.contains("***"), "{shown}");
assert!(!shown.contains("secret"), "{shown}");
assert_eq!(p.to_string(), domain::TEXT);
}
#[test]
fn errors_say_what_failed_without_plaintext() {
type E = OrderError<&'static str>;
assert_eq!(
E::Term(NormalizeError::NoDomain).to_string(),
"no order term: no domain for this kind of value"
);
assert_eq!(
E::Term(NormalizeError::Mismatch).to_string(),
"no order term: the value is not of the kind the index declares"
);
assert_eq!(
E::Term(NormalizeError::OutOfRange).to_string(),
"no order term: the value does not fit the type the index declares"
);
assert_eq!(
E::NotScalar.to_string(),
"a container has no single order term; walk it for one per scalar"
);
assert_eq!(
E::Source(SourceError::msg("getter threw")).to_string(),
"the value could not be read: getter threw"
);
assert_eq!(
E::Unsupported {
scheme: "block-ore",
domain: domain::TEXT
}
.to_string(),
"block-ore has no order term for vitaminc/ore/value/text-folded/unicode-16/v1"
);
assert_eq!(
E::Backend("down").to_string(),
"the order scheme failed: down"
);
let from: E = NormalizeError::Invalid.into();
assert!(matches!(from, E::Term(NormalizeError::Invalid)));
}
#[test]
fn the_scalar_sink_encodes_one_scalar_as_its_target_and_refuses_a_container() {
let p = 7i16.visit(Scalar::of(OrderTarget::I64)).expect("scalar");
assert_eq!(p.domain(), domain::INT64);
assert!(matches!(
vec![1u16, 2].visit(Scalar::of(OrderTarget::U16)),
Err(ScalarError::NotScalar)
));
assert!(matches!(
std::collections::BTreeMap::from([("k", 1u16)]).visit(Scalar::of(OrderTarget::U16)),
Err(ScalarError::NotScalar)
));
assert!(matches!(
true.visit(Scalar::of(OrderTarget::U16)),
Err(ScalarError::Term(NormalizeError::NoDomain))
));
}
#[test]
fn the_scalar_sink_gives_a_typed_null_no_term_without_a_policy_and_the_sentinel_with_one() {
let plain = Scalar::of(OrderTarget::U32);
assert!(
matches!(
None::<u32>.visit(plain),
Err(ScalarError::Term(NormalizeError::NoDomain))
),
"a typed null must have no order term in a plain space"
);
assert!(
matches!(
None::<u32>.visit(Scalar::of(OrderTarget::Text)),
Err(ScalarError::Term(NormalizeError::NoDomain))
),
"the target does not give a typed null a term either"
);
for (nulls, fill, label) in [
(NullOrder::First, 0x00, domain::UINT32_NULLS_FIRST),
(NullOrder::Last, 0xFF, domain::UINT32_NULLS_LAST),
] {
let placed = Scalar::of(RuntimeNulls::new(OrderTarget::U32, nulls));
let typed = None::<u32>
.visit(placed)
.expect("a typed null is the sentinel");
assert_eq!(
typed.domain(),
label,
"the sentinel is under the policy's label"
);
assert_eq!(
typed.null_order(),
Some(nulls),
"the plaintext carries its policy"
);
assert_eq!(
typed.bytes().risky_ref(),
&[fill; 4],
"a typed null is the sentinel's bytes"
);
let other = None::<i16>.visit(placed).expect("sentinel");
let raw = vitaminc_aead::Raw(None, Physical::Null)
.visit(placed)
.expect("sentinel");
assert_eq!(other.bytes().risky_ref(), typed.bytes().risky_ref());
assert_eq!(raw.bytes().risky_ref(), typed.bytes().risky_ref());
}
}
}