use core::cmp::Ordering;
use core::fmt;
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::{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"
);
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),
}
impl<E> OrderError<E> {
pub fn unsupported<S: OrderScheme + ?Sized>(plaintext: &OrderPlaintext) -> Self {
Self::Unsupported {
scheme: S::NAME,
domain: plaintext.domain(),
}
}
}
pub trait OrderScheme {
const NAME: &'static str;
type Term: AsRef<[u8]>;
type Error: core::error::Error + Send + Sync + 'static;
fn encrypt_plaintext(
&self,
plaintext: &OrderPlaintext,
salt: &Context<'_>,
) -> Result<Self::Term, OrderError<Self::Error>>;
fn compare(a: &Self::Term, b: &Self::Term) -> Option<Ordering>;
}
pub trait OrderEncrypt: OrderScheme {
fn column<'a, C: IntoContext<'a>>(
&self,
target: OrderTarget,
context: C,
) -> OrderColumn<'_, Self> {
let context = context.into_context().encode();
OrderColumn {
scheme: self,
target,
salt: salt(Self::NAME, target, &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 OrderColumn<'s, S: OrderScheme + ?Sized> {
scheme: &'s S,
#[non_sensitive]
target: OrderTarget,
salt: Context<'static>,
}
impl<S: OrderScheme + ?Sized> OrderColumn<'_, S> {
pub fn target(&self) -> OrderTarget {
self.target
}
pub fn salt(&self) -> &Context<'static> {
&self.salt
}
pub fn encrypt<V: Encrypt>(&self, value: V) -> Result<S::Term, OrderError<S::Error>> {
let plaintext = value.visit(Scalar(self.target))?;
self.scheme.encrypt_plaintext(&plaintext, &self.salt)
}
pub fn encrypt_term(
&self,
plaintext: &OrderPlaintext,
) -> Result<S::Term, OrderError<S::Error>> {
if plaintext.target() != self.target {
return Err(OrderError::Term(NormalizeError::Mismatch));
}
self.scheme.encrypt_plaintext(plaintext, &self.salt)
}
pub fn compare(&self, a: &S::Term, b: &S::Term) -> Option<Ordering> {
S::compare(a, b)
}
}
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)]
struct Scalar(OrderTarget);
#[derive(Debug, thiserror::Error)]
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)
}
}
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::I8, &context);
assert_eq!(
salted,
Context::pae(&[
b"vitaminc/ore/v1",
b"mock",
domain::INT8.as_bytes(),
context.as_bytes()
])
);
assert_ne!(salted, salt("other", OrderTarget::I8, &context));
assert_ne!(salted, salt("mock", OrderTarget::U8, &context));
assert_ne!(
salted,
salt(
"mock",
OrderTarget::I8,
&("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::U8)),
Err(ScalarError::Term(NormalizeError::NoDomain))
));
}
}