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
9 changes: 5 additions & 4 deletions .cargo/mutants.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ exclude_re = [
# and the port is checked byte for byte against the frozen crate besides.
"replace \\| with \\^ in (orderable|strip_trailing_zeros)$",
# Unreachable, not untested: `Nothing` (vitaminc-aead typed.rs) is an
# uninhabited enum, so no value exists to call `scalar` or `passthrough`
# on and replacing either body with `Ok(())` cannot be observed. Its
# `seq` and `map` mutants are unviable for the same reason.
"replace <impl Sink for Nothing>::(scalar|passthrough) ",
# uninhabited enum, so no value exists to call `scalar`, `typed_null` or
# `passthrough` on and replacing any of those bodies with `Ok(())` cannot
# be observed. Its `seq` and `map` mutants are unviable for the same
# reason.
"replace <impl Sink for Nothing>::(scalar|typed_null|passthrough) ",
# Equivalent mutant: `max` (vitaminc-aead plaintext/depth.rs) picks the
# larger of two depths, and when they are equal `a > b` and `a >= b`
# return the same number, so no input can tell them apart.
Expand Down
96 changes: 96 additions & 0 deletions packages/aead-derive/src/encrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,13 @@ pub(crate) fn derive(input: DeriveInput) -> Result<TokenStream> {
item_check,
visit_check,
} = depth(krate, &input, &shape, attrs.recursive);
let (fixed, ref_fixed) = fixed(krate, &input, &shape, attrs.recursive);

Ok(quote! {
#[automatically_derived]
impl #impl_generics #krate::Encrypt for #name #ty_generics #where_clause {
#depth
#fixed

fn visit<__S>(#receiver, __sink: __S) -> ::core::result::Result<__S::Ok, __S::Error>
where
Expand All @@ -95,6 +97,7 @@ pub(crate) fn derive(input: DeriveInput) -> Result<TokenStream> {
#[automatically_derived]
impl #ref_impl_generics #krate::Encrypt for &'__r #name #ty_generics #ref_where_clause {
#ref_depth
#ref_fixed

fn visit<__S>(self, __sink: __S) -> ::core::result::Result<__S::Ok, __S::Error>
where
Expand Down Expand Up @@ -215,6 +218,37 @@ fn depth(krate: &syn::Path, input: &DeriveInput, shape: &Shape, recursive: bool)
}
}

/// The `FIXED` constants for both impls: what an absent value of the type
/// is sealed as. A newtype is transparent, so it has its inner type's (by
/// value and by reference, as `DEPTH` does). A fieldless enum is always a
/// `u32` leaf. A struct is a map, which has none, and a recursive newtype
/// keeps the default rather than name its own constant in a cycle.
fn fixed(
krate: &syn::Path,
input: &DeriveInput,
shape: &Shape,
recursive: bool,
) -> (TokenStream, TokenStream) {
let decl = |value: TokenStream| quote!(const FIXED: ::core::option::Option<#krate::FixedType> = #value;);
match shape {
Shape::Newtype(_) if recursive || names_itself(&input.ident, shape) => {
(TokenStream::new(), TokenStream::new())
}
Shape::Newtype(field) => {
let ty = &field.ty;
(
decl(quote!(<#ty as #krate::Encrypt>::FIXED)),
decl(quote!(<&'__r #ty as #krate::Encrypt>::FIXED)),
)
}
Shape::Enum(_) => {
let u32 = decl(quote!(#krate::FixedType::plain(#krate::Width::U32)));
(u32.clone(), u32)
}
Shape::Map(_) | Shape::Empty => (TokenStream::new(), TokenStream::new()),
}
}

/// Whether a walked field's type names the type being derived, directly or
/// as `Self`: a recursive type, whose depth depends on the value. Mutual
/// recursion through another type cannot be seen from here; it needs
Expand Down Expand Up @@ -709,6 +743,68 @@ mod tests {
assert_lacks(&out, quote!(::vitaminc_aead::Physical::Text));
}

/// What an absent value of the type is sealed as: a newtype's inner
/// type's, by value and by reference; a `u32` leaf for a fieldless
/// enum; nothing for a struct, which is a map.
#[test]
fn fixed_follows_the_shape() {
let out = expand(parse_quote!(
struct Age(u32);
));
assert_contains(
&out,
quote!(
const FIXED: ::core::option::Option<::vitaminc_aead::FixedType> =
<u32 as ::vitaminc_aead::Encrypt>::FIXED;
),
);
assert_contains(
&out,
quote!(
const FIXED: ::core::option::Option<::vitaminc_aead::FixedType> =
<&'__r u32 as ::vitaminc_aead::Encrypt>::FIXED;
),
);

let out = expand(parse_quote! {
enum Status {
Active,
}
});
assert_contains(
&out,
quote!(
const FIXED: ::core::option::Option<::vitaminc_aead::FixedType> =
::vitaminc_aead::FixedType::plain(::vitaminc_aead::Width::U32);
),
);

let out = expand(parse_quote! {
struct User {
id: u32,
}
});
assert_lacks(&out, quote!(const FIXED));
}

/// A recursive newtype keeps the default `FIXED` rather than name its
/// own constant in a cycle, whether it names itself or says so with
/// `#[aead(recursive)]`.
#[test]
fn a_recursive_newtype_declares_no_fixed() {
for input in [
parse_quote!(
struct Wrapped(Option<Box<Wrapped>>);
),
parse_quote!(
#[aead(recursive)]
struct Even(Option<Box<Odd>>);
),
] {
assert_lacks(&expand(input), quote!(const FIXED));
}
}

/// Serde-style: bounding the parameter rather than each field's type lets a
/// field of type `Vec<T>` pick up `Vec<T>: Encrypt` from the blanket
/// impl. The by-reference impl bounds the parameter by reference, and its
Expand Down
13 changes: 13 additions & 0 deletions packages/aead-derive/src/typed_decrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ pub(crate) fn derive(input: DeriveInput) -> Result<TokenStream> {
<#inner as #krate::typed::Decrypt>::Sink,
#wrap #ty_generics,
>;
fn reads_null(__ty: #krate::FixedType) -> bool {
<#inner as #krate::typed::Decrypt>::reads_null(__ty)
}
fn sink() -> Self::Sink {
#krate::typed::Wrapped::new(<#inner as #krate::typed::Decrypt>::sink())
}
Expand Down Expand Up @@ -211,6 +214,13 @@ fn map_target(
::core::result::Result::Err(#krate::DecodeError::Expected(#expected))
}

fn typed_null(
self,
_: #krate::FixedType,
) -> ::core::result::Result<Self::Ok, Self::Error> {
::core::result::Result::Err(#krate::DecodeError::Expected(#expected))
}

fn seq(
self,
_: ::core::option::Option<usize>,
Expand Down Expand Up @@ -298,6 +308,9 @@ fn enum_target(krate: &syn::Path, input: &DeriveInput, variants: &[VariantInfo])
#[automatically_derived]
impl #impl_generics #krate::FromScalar for #name #ty_generics #where_clause {
const EXPECTED: &'static str = #expected;
// Its own `None`, even with no variant at zero to read.
const FIXED: ::core::option::Option<#krate::FixedType> =
#krate::FixedType::plain(#krate::Width::U32);

fn from_scalar(
__ty: ::core::option::Option<#krate::Logical>,
Expand Down
21 changes: 21 additions & 0 deletions packages/aead-frame/src/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
//! | bool | bool |
//! | null, and JavaScript's `undefined` | unit, and `None` |
//!
//! A typed null ([`tags::TYPED_NULL`](vitaminc_aead::tags::TYPED_NULL)) is
//! refused as malformed. It is how plaintext marks an absent fixed-width
//! value so its sealed length matches a present one, which is no concern of
//! data that is not sealed, and this format never writes one.
//!
//! Each kind reads only as itself: a struct or a map reads an object, never
//! an array by position, and a string reads text, never bytes that happen
//! to be UTF-8, nor bytes text.
Expand Down Expand Up @@ -1490,4 +1495,20 @@ mod tests {
);
}
}

/// A typed null is plaintext's, not this format's: refused wherever it
/// appears, an option included, rather than read as a null.
#[test]
fn a_typed_null_is_refused() {
let null_u32 = [
vitaminc_aead::tags::TYPED_NULL,
vitaminc_aead::tags::UINT32,
0,
0,
0,
];
assert!(from_slice::<Option<u32>>(&null_u32).is_err());
assert!(from_slice::<()>(&null_u32).is_err());
assert!(from_slice::<u32>(&null_u32).is_err());
}
}
9 changes: 8 additions & 1 deletion packages/aead-napi/src/js_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

use napi::bindgen_prelude::{BigInt, Buffer, JsValue, Null, Object, ToNapiValue, Unknown};
use napi::{Env, Error, Status};
use vitaminc_aead::{Encrypt, Float, Frame, Int, Logical, MapSink, Physical, SeqSink, Sink};
use vitaminc_aead::{
Encrypt, FixedType, Float, Frame, Int, Logical, MapSink, Physical, SeqSink, Sink,
};

use crate::error::JsFault;
use crate::js::{
Expand Down Expand Up @@ -124,6 +126,11 @@ impl<'e, O: Out<'e>> Sink for JsBuilder<'e, O> {
Ok(self.out.put(built))
}

/// An absent value is JavaScript's `null`, whatever its type.
fn typed_null(self, _: FixedType) -> Result<O::Ok, JsFault> {
self.scalar(None, Physical::Null)
}

fn seq(self, len: Option<usize>) -> Result<JsSeq<'e, O>, JsFault> {
self.check_depth()?;
Ok(JsSeq {
Expand Down
5 changes: 4 additions & 1 deletion packages/aead/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ let mut map = BTreeMap::new();
map.insert("name".to_string(), "Ada".to_string());
assert!(matches!(encrypt(map, &cipher, ())?, CipherText::Map(entries) if entries[0].0 == "name"));

// An empty container is an authenticated marker; `None` is a null leaf.
// An empty container is an authenticated marker; `None` is a null leaf. A
// `None` of a fixed-width type (an int, a float, a date, a timestamp, a
// decimal, a uuid) is a typed null, padded to the length of a `Some`, so the
// ciphertext's length does not show which values are absent.
assert!(matches!(encrypt(Vec::<u32>::new(), &cipher, ())?, CipherText::EmptySequence(_)));
assert!(matches!(encrypt(None::<u32>, &cipher, ())?, CipherText::Single(_)));
# Ok::<(), Box<dyn std::error::Error>>(())
Expand Down
4 changes: 3 additions & 1 deletion packages/aead/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
/// [`EmptyMap`](CipherText::EmptyMap), since they contain no element
/// ciphertexts to authenticate the supplied AAD. Nested structures are
/// represented recursively. An absent value (`Option::None`, a host null)
/// is a null leaf, a `Single` like any other scalar.
/// is a null leaf, a `Single` like any other scalar: the plain null, or a
/// typed null padded to its type's length (see
/// [`tags::TYPED_NULL`](crate::tags::TYPED_NULL)).
///
/// The two parameters:
///
Expand Down
51 changes: 49 additions & 2 deletions packages/aead/src/encrypt_sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use vitaminc_protected::{Controlled, Protected};
use crate::frame::PassthroughFrame;
use crate::leaf::{self, ScalarError};
use crate::plaintext::{
Encrypt, Error, Logical, MapSink, Physical, SeqSink, Sink, SourceError, Step,
Encrypt, Error, FixedType, Logical, MapSink, Physical, SeqSink, Sink, SourceError, Step,
};
use crate::seal::{
seal_all, seal_all_async, AsyncCipher, Cipher, LeafCount, Node, Pending, SealError,
Expand Down Expand Up @@ -271,6 +271,18 @@ impl<E, O: Slot> EncryptSink<E, O> {
aad: self.aad,
})))
}

/// Plan an absent value of a fixed-width type as one leaf: a typed
/// null, as long as a leaf holding a value of the type.
fn typed_null_leaf(self, ty: FixedType) -> Result<O::Ok, EncryptError<E>> {
shape::scalar(self.depth, None, &Physical::Null, &self.out)?;
let mut plaintext = Protected::new(Vec::with_capacity(leaf::typed_null_len(ty)));
leaf::write_typed_null(ty, plaintext.risky_inner_mut());
Ok(self.out.put(CipherText::Single(Pending {
plaintext,
aad: self.aad,
})))
}
}

fn marker(aad: Context<'static>) -> Pending {
Expand All @@ -290,6 +302,10 @@ impl<E, O: Slot> Sink for EncryptSink<E, O> {
self.leaf(ty, v)
}

fn typed_null(self, ty: FixedType) -> Result<O::Ok, Self::Error> {
self.typed_null_leaf(ty)
}

fn seq(self, len: Option<usize>) -> Result<Self::Seq, Self::Error> {
shape::container(self.depth, &self.out)?;
Ok(EncryptSeq {
Expand Down Expand Up @@ -473,13 +489,40 @@ mod tests {

#[test]
fn none_is_a_null_leaf_not_a_marker() {
let node = planned(None::<u32>).expect("plans");
let node = planned(None::<String>).expect("plans");
assert!(matches!(node, CipherText::Single(_)));
let leaves = leaves_with_aad(node, &"ctx".into_aad());
assert_eq!(leaves[0].0.plaintext.risky_ref(), &[0x00]);
assert_eq!(leaves[0].1, "ctx".into_aad());
}

#[test]
fn a_fixed_width_none_is_a_typed_null_as_long_as_a_some() {
let node = planned(None::<u32>).expect("plans");
assert!(matches!(node, CipherText::Single(_)));
let leaves = leaves_with_aad(node, &"ctx".into_aad());
assert_eq!(leaves[0].0.plaintext.risky_ref(), &[0x15, 0x06, 0, 0, 0]);
assert_eq!(leaves[0].1, "ctx".into_aad());
let some = leaves_with_aad(planned(Some(34u32)).expect("plans"), &"ctx".into_aad());
assert_eq!(some[0].0.plaintext.risky_ref().len(), 5);
}

#[test]
fn a_typed_null_too_deep_is_refused_like_a_scalar() {
use crate::plaintext::{FixedType, Width};
let sink = EncryptSink::<Refused, Root> {
aad: "ctx".into_aad().into_owned(),
out: Root,
depth: crate::frame::MAX_DEPTH + 1,
_error: PhantomData,
};
let err = sink
.typed_null(FixedType::plain(Width::I8).expect("fixed"))
.err()
.expect("too deep");
assert!(matches!(err, EncryptError::Shape(_)), "{err:?}");
}

#[test]
fn empty_containers_are_markers() {
let node = planned(Vec::<u32>::new()).expect("plans");
Expand Down Expand Up @@ -575,6 +618,10 @@ mod tests {
*self.0 += 1;
Ok(())
}
fn typed_null(self, _: FixedType) -> Result<(), E> {
*self.0 += 1;
Ok(())
}
fn seq(self, _: Option<usize>) -> Result<Self, E> {
Ok(self)
}
Expand Down
Loading
Loading