use core::cmp::Ordering;
use core::fmt;
use std::sync::OnceLock;
use vitaminc_aead::plaintext::{self, SourceError};
use vitaminc_aead::{Encrypt, Logical, MapSink, Physical, SeqSink, Sink};
use vitaminc_context::{Context, IntoContext};
use vitaminc_normalize::NormalizeError;
use vitaminc_protected::{Controlled, OpaqueDebug, Protected};
use crate::target::{FitsIn, OwnTarget, StaticTarget, Supports, Target};
use crate::{order, OrderTarget};
const SALT_LABEL: &[u8] = b"vitaminc/ore/v1";
#[derive(OpaqueDebug)]
pub struct OrderPlaintext {
#[non_sensitive]
target: OrderTarget,
bytes: Protected<Vec<u8>>,
}
impl OrderPlaintext {
pub(crate) fn new(target: OrderTarget, bytes: Protected<Vec<u8>>) -> Self {
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, bytes }
}
pub fn target(&self) -> OrderTarget {
self.target
}
pub fn domain(&self) -> &'static str {
self.target.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, target: OrderTarget, context: &Context<'_>) -> Context<'static> {
Context::pae(&[
SALT_LABEL,
scheme.as_bytes(),
target.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,
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()
);
Self {
scheme,
target,
salt: salt(S::NAME, target.runtime(), 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 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(self.target()))?;
self.encrypt_plaintext(&plaintext)
}
pub fn encrypt_term(
&self,
plaintext: &OrderPlaintext,
) -> Result<OrderTerm<S::Term, T>, OrderError<S::Error>> {
if plaintext.target() != self.target() {
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> {
let target = self.target();
(a.target() == target && b.target() == 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.target().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(T::TARGET)) {
Ok(plaintext) => plaintext,
Err(refused) => unreachable!("a `FitsIn` value was refused: {refused}"),
};
self.encrypt_opened(&plaintext)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderTerm<R, T: Target> {
raw: R,
target: T,
}
impl<R, T: Target> OrderTerm<R, T> {
pub fn target(&self) -> OrderTarget {
self.target.runtime()
}
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(pub(crate) OrderTarget);
#[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> {
order(self.0, ty, value).map_err(ScalarError::Term)
}
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;
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", OrderTarget::I16, &context);
assert_eq!(
salted,
Context::pae(&[
b"vitaminc/ore/v1",
b"mock",
domain::INT16.as_bytes(),
context.as_bytes()
])
);
assert_ne!(salted, salt("other", OrderTarget::I16, &context));
assert_ne!(salted, salt("mock", OrderTarget::U16, &context));
assert_ne!(
salted,
salt(
"mock",
OrderTarget::I16,
&("tenant", 2u64).into_context().encode()
)
);
}
#[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(OrderTarget::I64)).expect("scalar");
assert_eq!(p.domain(), domain::INT64);
assert!(matches!(
vec![1u16, 2].visit(Scalar(OrderTarget::U16)),
Err(ScalarError::NotScalar)
));
assert!(matches!(
std::collections::BTreeMap::from([("k", 1u16)]).visit(Scalar(OrderTarget::U16)),
Err(ScalarError::NotScalar)
));
assert!(matches!(
true.visit(Scalar(OrderTarget::U16)),
Err(ScalarError::Term(NormalizeError::NoDomain))
));
}
}