use std::borrow::Cow;
use crate::plaintext::kinds::id;
use crate::{ContextPiece, IntoContext, Kind, KnownLogical, UnknownLogicalId};
const LEAF_TYPE_DOMAIN: &[u8] = b"vitaminc/aead/leaf-type/v2";
#[derive(Clone, Debug)]
pub struct LeafTypeAad<'a> {
base: ContextPiece<'a>,
kind: Kind,
id: u8,
}
impl<'a> LeafTypeAad<'a> {
pub fn physical(base: impl IntoContext<'a>, kind: Kind) -> Self {
Self::bind(base, kind, id::NONE)
}
pub fn logical(base: impl IntoContext<'a>, logical: KnownLogical) -> Self {
Self::bind(base, logical.kind(), logical.id())
}
pub fn unknown(base: impl IntoContext<'a>, kind: Kind, logical: UnknownLogicalId) -> Self {
Self::bind(base, kind, logical.get())
}
fn bind(base: impl IntoContext<'a>, kind: Kind, id: u8) -> Self {
Self {
base: base.into_context(),
kind,
id,
}
}
}
impl<'a> IntoContext<'a> for LeafTypeAad<'a> {
fn into_context(self) -> ContextPiece<'a> {
ContextPiece::List(vec![
ContextPiece::Bytes(Cow::Borrowed(LEAF_TYPE_DOMAIN)),
self.base,
ContextPiece::U8(self.kind.to_wire()),
ContextPiece::U8(self.id),
])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plaintext::record::{record, Event, Owned, Record};
use crate::plaintext::{Int, Logical, Physical, Raw};
use crate::seal::mock::{Leaf, Mock, Refused};
use crate::{decrypt, decrypt_into, encrypt, Context, DecryptError, IntoAad, Node};
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[test]
fn pins_the_context_bytes_with_no_logical_type() {
let bound = LeafTypeAad::physical("ctx", Kind::Int).into_aad();
let expected = Context::pae(&[
LEAF_TYPE_DOMAIN.into_aad().as_bytes(),
"ctx".into_aad().as_bytes(),
Kind::Int.to_wire().into_aad().as_bytes(),
0u8.into_aad().as_bytes(),
]);
assert_eq!(bound, expected);
assert_eq!(
hex(bound.as_bytes()),
concat!(
"0400000000000000",
"6c00000000000000",
"0300000000000000",
"1900000000000000766974616d696e632f636f6e746578742f76616c75652f7631",
"1900000000000000766974616d696e632f636f6e746578742f62797465732f7631",
"1a00000000000000766974616d696e632f616561642f6c6561662d747970652f7632",
"5400000000000000",
"0300000000000000",
"1900000000000000766974616d696e632f636f6e746578742f76616c75652f7631",
"1800000000000000766974616d696e632f636f6e746578742f757466382f7631",
"0300000000000000637478",
"5300000000000000",
"0300000000000000",
"1900000000000000766974616d696e632f636f6e746578742f76616c75652f7631",
"1900000000000000766974616d696e632f636f6e746578742f75382d6c652f7631",
"010000000000000002",
"5300000000000000",
"0300000000000000",
"1900000000000000766974616d696e632f636f6e746578742f76616c75652f7631",
"1900000000000000766974616d696e632f636f6e746578742f75382d6c652f7631",
"010000000000000000",
)
);
}
#[test]
fn pins_the_context_bytes_with_a_logical_type() {
let bound = LeafTypeAad::logical("ctx", KnownLogical::Date).into_aad();
let expected = Context::pae(&[
LEAF_TYPE_DOMAIN.into_aad().as_bytes(),
"ctx".into_aad().as_bytes(),
Kind::Int.to_wire().into_aad().as_bytes(),
id::DATE.into_aad().as_bytes(),
]);
assert_eq!(bound, expected);
assert_eq!(
hex(bound.as_bytes()),
concat!(
"0400000000000000",
"6c00000000000000",
"0300000000000000",
"1900000000000000766974616d696e632f636f6e746578742f76616c75652f7631",
"1900000000000000766974616d696e632f636f6e746578742f62797465732f7631",
"1a00000000000000766974616d696e632f616561642f6c6561662d747970652f7632",
"5400000000000000",
"0300000000000000",
"1900000000000000766974616d696e632f636f6e746578742f76616c75652f7631",
"1800000000000000766974616d696e632f636f6e746578742f757466382f7631",
"0300000000000000637478",
"5300000000000000",
"0300000000000000",
"1900000000000000766974616d696e632f636f6e746578742f76616c75652f7631",
"1900000000000000766974616d696e632f636f6e746578742f75382d6c652f7631",
"010000000000000002",
"5300000000000000",
"0300000000000000",
"1900000000000000766974616d696e632f636f6e746578742f76616c75652f7631",
"1900000000000000766974616d696e632f636f6e746578742f75382d6c652f7631",
"010000000000000002",
)
);
}
#[test]
fn keeps_the_base_parts_intact() {
let parts = LeafTypeAad::physical(("schema", 7u64), Kind::Text).into_context();
assert_eq!(
parts.to_string(),
"(0x766974616d696e632f616561642f6c6561662d747970652f7632, (\"schema\", 7u64), 4u8, 0u8)"
);
assert_eq!(parts.leaves().count(), 5);
}
#[test]
fn differs_from_a_map_entry_and_from_a_bare_list() {
let leaf = LeafTypeAad::physical("ctx", Kind::Int).into_aad();
assert_ne!(leaf, "ctx".into_aad().for_map_entry("\x02"));
let bare = ContextPiece::List(vec![
"ctx".into_context(),
ContextPiece::U8(Kind::Int.to_wire()),
ContextPiece::U8(0),
]);
assert_ne!(leaf, bare.into_aad());
}
fn parts(kind: Kind, id: u8) -> Context<'static> {
Context::pae(&[
LEAF_TYPE_DOMAIN.into_aad().as_bytes(),
"ctx".into_aad().as_bytes(),
kind.to_wire().into_aad().as_bytes(),
id.into_aad().as_bytes(),
])
}
const ALL_KINDS: [Kind; 6] = [
Kind::Null,
Kind::Bool,
Kind::Int,
Kind::Float,
Kind::Text,
Kind::Bytes,
];
#[test]
fn every_kind_and_logical_id_binds_its_four_parts() {
for kind in ALL_KINDS {
assert_eq!(
LeafTypeAad::physical("ctx", kind).into_aad(),
parts(kind, id::NONE),
"{kind:?}"
);
}
let known = [
(KnownLogical::Undefined, Kind::Null, id::UNDEFINED),
(KnownLogical::Date, Kind::Int, id::DATE),
(KnownLogical::Timestamp, Kind::Int, id::TIMESTAMP),
(KnownLogical::Decimal, Kind::Int, id::DECIMAL),
(KnownLogical::Uuid, Kind::Bytes, id::UUID),
];
assert_eq!(
known.map(|(_, _, id)| id),
core::array::from_fn(|i| i as u8 + 1)
);
assert_eq!(usize::from(id::LAST_KNOWN), known.len());
for (logical, kind, id) in known {
assert_eq!(
LeafTypeAad::logical("ctx", logical).into_aad(),
parts(kind, id),
"{logical:?}"
);
}
for id in id::LAST_KNOWN + 1..=u8::MAX {
let unknown = UnknownLogicalId::new(id).expect("unknown");
for kind in ALL_KINDS {
assert_eq!(
LeafTypeAad::unknown("ctx", kind, unknown).into_aad(),
parts(kind, id),
"{kind:?} with id {id}"
);
}
}
}
#[test]
fn is_sensitive_to_the_kind_and_the_id() {
let int = LeafTypeAad::physical("ctx", Kind::Int).into_aad();
assert_ne!(int, LeafTypeAad::physical("ctx", Kind::Text).into_aad());
assert_ne!(
int,
LeafTypeAad::logical("ctx", KnownLogical::Date).into_aad()
);
assert_ne!(
LeafTypeAad::logical("ctx", KnownLogical::Date).into_aad(),
LeafTypeAad::logical("ctx", KnownLogical::Decimal).into_aad()
);
let unknown = |id| UnknownLogicalId::new(id).expect("unknown");
assert_ne!(
int,
LeafTypeAad::unknown("ctx", Kind::Int, unknown(200)).into_aad()
);
assert_ne!(
LeafTypeAad::unknown("ctx", Kind::Int, unknown(200)).into_aad(),
LeafTypeAad::unknown("ctx", Kind::Int, unknown(201)).into_aad()
);
assert_ne!(
LeafTypeAad::unknown("ctx", Kind::Int, unknown(200)).into_aad(),
LeafTypeAad::unknown("ctx", Kind::Text, unknown(200)).into_aad()
);
}
#[test]
fn every_kind_binds_differently() {
for (i, a) in ALL_KINDS.iter().enumerate() {
for b in &ALL_KINDS[i + 1..] {
assert_ne!(
LeafTypeAad::physical("ctx", *a).into_aad(),
LeafTypeAad::physical("ctx", *b).into_aad(),
"{a:?} and {b:?} bind the same"
);
}
}
let flag = LeafTypeAad::physical("flags", Kind::Bool);
let sealed = encrypt(true, &Mock, flag.clone()).expect("seals");
assert!(decrypt::<bool, _, _>(&Mock, sealed, flag).expect("opens"));
}
#[test]
fn a_logical_type_binds_over_its_own_kind() {
assert_eq!(KnownLogical::Uuid.kind(), Kind::Bytes);
assert_eq!(
LeafTypeAad::logical("ctx", KnownLogical::Uuid).into_aad(),
parts(Kind::Bytes, id::UUID)
);
assert_ne!(
LeafTypeAad::logical("ctx", KnownLogical::Uuid).into_aad(),
parts(Kind::Text, id::UUID)
);
}
#[test]
fn debug_shows_the_binding() {
let bound = LeafTypeAad::logical("ctx", KnownLogical::Date);
let shown = format!("{bound:?}");
assert!(shown.contains("Int") && shown.contains("id: 2"), "{shown}");
}
fn date() -> LeafTypeAad<'static> {
LeafTypeAad::logical("schema", KnownLogical::Date)
}
fn int() -> LeafTypeAad<'static> {
LeafTypeAad::physical("schema", Kind::Int)
}
fn opens<T: crate::typed::Decrypt>(
sealed: Node<Leaf>,
aad: LeafTypeAad<'_>,
) -> Result<T, DecryptError<crate::DecodeError, Refused>> {
decrypt(&Mock, sealed, aad)
}
#[test]
fn a_date_does_not_open_as_an_int_nor_an_int_as_a_date() {
let days = || Raw(Some(Logical::Date), Physical::Int(Int::I32(19_000)));
let sealed = encrypt(days(), &Mock, date()).expect("seals");
assert!(
matches!(
opens::<i32>(sealed, int()),
Err(DecryptError::Cipher(Refused))
),
"a date opened under the int binding"
);
let sealed = encrypt(19_000i32, &Mock, int()).expect("seals");
assert!(
matches!(
opens::<i32>(sealed, date()),
Err(DecryptError::Cipher(Refused))
),
"an int opened under the date binding"
);
let sealed = encrypt(19_000i32, &Mock, int()).expect("seals");
assert_eq!(opens::<i32>(sealed, int()).expect("opens"), 19_000);
let sealed = encrypt(days(), &Mock, date()).expect("seals");
let mut log = Vec::new();
decrypt_into(&Mock, sealed, date(), Record(&mut log)).expect("opens");
assert_eq!(log, record(days()).expect("records"));
}
#[test]
fn the_width_is_not_bound() {
let sealed = encrypt(19_000i64, &Mock, int()).expect("seals");
assert_eq!(opens::<i32>(sealed, int()).expect("opens"), 19_000);
}
#[test]
fn decimals_of_different_scales_open_under_one_binding() {
let column = || LeafTypeAad::logical("price", KnownLogical::Decimal);
for (scale, mantissa) in [(2u8, 150i128), (1, 15), (0, 2)] {
let value = || {
Raw(
Some(Logical::Decimal { scale }),
Physical::Int(Int::I128(mantissa)),
)
};
let sealed = encrypt(value(), &Mock, column()).expect("seals");
let mut log = Vec::new();
decrypt_into(&Mock, sealed, column(), Record(&mut log)).expect("opens");
assert_eq!(
log,
[Event::Scalar(
Some(Logical::Decimal { scale }),
Owned::Int(Int::I128(mantissa))
)],
"scale {scale} did not round-trip"
);
}
}
#[test]
fn the_parameter_is_still_validated_after_opening() {
let column = || LeafTypeAad::logical("price", KnownLogical::Decimal);
let mut plaintext = vec![crate::tags::DECIMAL];
plaintext.extend_from_slice(&(29u32 << 16).to_le_bytes());
plaintext.extend_from_slice(&[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
let leaf = crate::seal::Cipher::seal(
&Mock,
vitaminc_protected::Protected::new(plaintext),
&column().into_aad(),
)
.expect("seals");
let mut log = Vec::new();
assert!(
matches!(
decrypt_into(&Mock, Node::Single(leaf), column(), Record(&mut log)),
Err(DecryptError::Malformed)
),
"an out-of-range scale was read"
);
}
#[test]
fn a_ciphertext_bound_under_the_old_label_does_not_open() {
let old = || {
ContextPiece::List(vec![
ContextPiece::Bytes(Cow::Borrowed(b"vitaminc/aead-value/leaf-type/v1")),
"schema".into_context(),
ContextPiece::U8(crate::tags::INT32),
])
};
let sealed = encrypt(19_000i32, &Mock, old()).expect("seals");
assert!(
matches!(
opens::<i32>(sealed, int()),
Err(DecryptError::Cipher(Refused))
),
"a v1-bound ciphertext opened under v2"
);
}
}