diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index 0ad2ac34d..59e404604 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Breaking + +- **A target description carries a source mode.** `Encryption` gains a + last type parameter, `M: SourceMode = Borrowed`, saying how it is handed + its plaintext. Code that names `Encryption<'s, S, T, K, Ctx>` still + compiles and means the borrowed mode it always ran in. +- `ciphertext`, `equality`, `matching`, `ore` and `ope` gain a source-mode + type parameter, `M`. A turbofish must name it: `ciphertext::()`, + and in `matching` it comes before `O` (`matching::()`). A call + whose result type does not fix the mode must name it; `Borrowed` is the + old behaviour. In return `ciphertext`, `equality`, `ore` and `ope` no + longer ask `S: Clone` themselves: only borrowed mode does. + +### Added + +- `KeysetCipher::run`: run a description held in a variable over a value, + under a context, without an `EncryptFrom` declaration. +- `target::{SourceMode, ConsumeSource, ShareSource, Borrowed, Owned}`. In + `Owned` mode a description is handed the plaintext by value, so a single + operation consumes it with no copy and a plaintext that is not `Clone` + (a zeroizing FFI value) can be sealed or indexed. The traits are sealed. + ## [0.2.0] - 2026-10-04 ### Breaking diff --git a/packages/stack-encrypt/src/sem/mod.rs b/packages/stack-encrypt/src/sem/mod.rs index f8044b222..9e00a8d74 100644 --- a/packages/stack-encrypt/src/sem/mod.rs +++ b/packages/stack-encrypt/src/sem/mod.rs @@ -341,13 +341,13 @@ where /// carries no requests. impl<'c, S, K, T> Term> for EqualityTerm where - S: PrfValue + Clone, + S: PrfValue, T: IntoPrfContext<'c>, { // Derived locally: no data key, no descriptor. fn encrypt_from<'a>( - source: &S, + source: S, cipher: &'a KeysetCipher<'_, K>, context: NonEmpty, ) -> Pending<'a, Self, K> @@ -355,7 +355,7 @@ where Self: 'a, { let context = context.into_prf_context().into_owned(); - let term = equality(cipher.prf(), source.clone(), context).map_err(Error::from); + let term = equality(cipher.prf(), source, context).map_err(Error::from); Pending::ready(cipher, term) } } @@ -660,7 +660,7 @@ where // Derived locally: no data key, no descriptor. fn encrypt_from<'a>( - source: &S, + source: S, cipher: &'a KeysetCipher<'_, K>, context: NonEmpty, ) -> Pending<'a, Self, K> @@ -942,14 +942,14 @@ where /// carries no requests. impl<'c, S, K, T> Term> for OreTerm where - S: CllwOreEncrypt + Clone + Send + 'static, + S: CllwOreEncrypt + Send + 'static, S::Output: Send + 'static, T: IntoPrfContext<'c>, { // Derived locally: no data key, no descriptor. fn encrypt_from<'a>( - source: &S, + source: S, cipher: &'a KeysetCipher<'_, K>, context: NonEmpty, ) -> Pending<'a, Self, K> @@ -957,7 +957,7 @@ where Self: 'a, { let context = context.into_prf_context().into_owned(); - let term = ore(cipher.prf(), source.clone(), context).map_err(Error::from); + let term = ore(cipher.prf(), source, context).map_err(Error::from); Pending::ready(cipher, term) } } @@ -967,14 +967,14 @@ where /// carries no requests. impl<'c, S, K, T> Term> for OpeTerm where - S: CllwOpeEncrypt + Clone + Send + 'static, + S: CllwOpeEncrypt + Send + 'static, S::Output: Send + 'static, T: IntoPrfContext<'c>, { // Derived locally: no data key, no descriptor. fn encrypt_from<'a>( - source: &S, + source: S, cipher: &'a KeysetCipher<'_, K>, context: NonEmpty, ) -> Pending<'a, Self, K> @@ -982,7 +982,7 @@ where Self: 'a, { let context = context.into_prf_context().into_owned(); - let term = ope(cipher.prf(), source.clone(), context).map_err(Error::from); + let term = ope(cipher.prf(), source, context).map_err(Error::from); Pending::ready(cipher, term) } } diff --git a/packages/stack-encrypt/src/target/core.rs b/packages/stack-encrypt/src/target/core.rs index 5817a8a55..7b76831bd 100644 --- a/packages/stack-encrypt/src/target/core.rs +++ b/packages/stack-encrypt/src/target/core.rs @@ -6,9 +6,13 @@ use vitaminc_aead::{CipherText, Decrypt, Encrypt, IntoAad, IntoContext}; use vitaminc_protected::NonEmpty; /// Internal term operation. It is deliberately inaccessible to target authors. +/// +/// `S` is what the operation is handed, by value: the PRF and ORE schemes +/// consume their input, so a term that needs the plaintext takes it, and one +/// that only reads it (a match term) is handed a reference as its `S`. pub(crate) trait Term: Sized { fn encrypt_from<'a>( - source: &S, + source: S, cipher: &'a KeysetCipher<'_, K>, context: Ctx, ) -> Pending<'a, Self, K> @@ -16,8 +20,12 @@ pub(crate) trait Term: Sized { Self: 'a; } -pub(crate) fn encrypt_native<'a, 'c, S: Encrypt + Clone, K, T: IntoContext<'c>>( - source: &S, +/// Seal `source` into the native tree. It is consumed, as Vitamin C's +/// `Encrypt` consumes it: a caller holding only a borrow clones before it +/// gets here (see [`ConsumeSource`](super::ConsumeSource)), and one holding +/// the value hands it over without a copy. +pub(crate) fn encrypt_native<'a, 'c, S: Encrypt, K, T: IntoContext<'c>>( + source: S, cipher: &'a KeysetCipher<'_, K>, context: NonEmpty, ) -> Pending<'a, StackCipherText, K> { @@ -27,7 +35,7 @@ pub(crate) fn encrypt_native<'a, 'c, S: Encrypt + Clone, K, T: IntoContext<'c>>( return Pending::failed(cipher, error); } let aad = context.into_aad().into_owned(); - match source.clone().encrypt_with_aad(cipher, aad) { + match source.encrypt_with_aad(cipher, aad) { Ok(tree) => seal_pending(cipher, tree, descriptor), Err(_) => Pending::ready(cipher, Err(Error::Aead)), } diff --git a/packages/stack-encrypt/src/target/mod.rs b/packages/stack-encrypt/src/target/mod.rs index 29b8b9736..038b92c79 100644 --- a/packages/stack-encrypt/src/target/mod.rs +++ b/packages/stack-encrypt/src/target/mod.rs @@ -60,6 +60,13 @@ //! New cryptographic operations belong in core; output adapters cannot install an //! execution callback. The separate cipher-directed API remains public. //! +//! Those schemes consume the plaintext. A description is handed it by +//! reference by default, and an operation clones it before consuming it, so a +//! declaration's plaintext is `Clone`. A plaintext that must not be copied +//! runs in [`Owned`] mode through [`KeysetCipher::run`](crate::KeysetCipher::run): +//! a single operation takes the value with no copy, and only +//! [`Encryption::zip`] asks for `Clone`. See [`SourceMode`]. +//! //! # Collections and authentication //! //! `Vec` describes independently encrypted rows under the same context. @@ -83,6 +90,7 @@ pub(crate) mod core; mod operations; mod pending; mod request; +mod source; pub mod transcode; pub(crate) use self::core::{decipher_pending, seal_pending}; @@ -93,4 +101,5 @@ pub use operations::{ }; pub use pending::{CipherScope, Pending, PendingFuture}; pub use request::{Request, Responses}; +pub use source::{Borrowed, ConsumeSource, Owned, ShareSource, SourceMode}; pub use stack_encrypt_derive::{DecryptInto, EncryptFrom}; diff --git a/packages/stack-encrypt/src/target/operations.rs b/packages/stack-encrypt/src/target/operations.rs index ff98af2b3..fc9bca95c 100644 --- a/packages/stack-encrypt/src/target/operations.rs +++ b/packages/stack-encrypt/src/target/operations.rs @@ -11,6 +11,7 @@ //! by name, with [`Encryption::under`] or [`Encryption::extend`]. use super::context::{AeadContext, CallerContext, DeclaredContext, Extends}; use super::core::{encrypt_native, open_native, Term}; +use super::source::{Borrowed, ConsumeSource, ShareSource, SourceMode}; use super::{CipherScope, Pending}; use crate::{Error, IntoContext, KeysetCipher, NonEmpty, StackCipher, StackCipherText}; use stack_kms::MaybeSend; @@ -53,12 +54,27 @@ pub trait DecryptInto

: Sized { fn decryption(self, context: Self::Context) -> Decryption; } +// What the description is handed is `M::Source`: `&'s S` in the default +// `Borrowed` mode, `S` itself in `Owned` mode (see `SourceMode`). #[cfg(not(target_arch = "wasm32"))] -type Build<'s, S, T, K, Ctx> = - Box FnOnce(&S, &'a KeysetCipher<'k, K>, Ctx) -> Pending<'a, T, K> + Send + 's>; +type Build<'s, S, T, K, Ctx, M> = Box< + dyn for<'a, 'k> FnOnce( + >::Source, + &'a KeysetCipher<'k, K>, + Ctx, + ) -> Pending<'a, T, K> + + Send + + 's, +>; #[cfg(target_arch = "wasm32")] -type Build<'s, S, T, K, Ctx> = - Box FnOnce(&S, &'a KeysetCipher<'k, K>, Ctx) -> Pending<'a, T, K> + 's>; +type Build<'s, S, T, K, Ctx, M> = Box< + dyn for<'a, 'k> FnOnce( + >::Source, + &'a KeysetCipher<'k, K>, + Ctx, + ) -> Pending<'a, T, K> + + 's, +>; #[cfg(not(target_arch = "wasm32"))] type Open = Box FnOnce(&'a StackCipher) -> Pending<'a, T, K> + Send>; #[cfg(target_arch = "wasm32")] @@ -89,9 +105,16 @@ type Open = Box FnOnce(&'a StackCipher) -> Pending<'a, T, K /// only ways to change the context a subtree runs under, and only `under` /// discharges the requirement into a [`DeclaredContext`], which is what /// `()` may satisfy. +/// +/// `M` is how the description is handed its plaintext ([`SourceMode`]): +/// by reference in the default [`Borrowed`] mode, which every +/// [`EncryptFrom`] declaration runs in, or by value in +/// [`Owned`](super::Owned) mode, run with [`KeysetCipher::run`]. In owned mode +/// a single operation consumes the plaintext without copying it, so `S` need +/// not be `Clone`; [`zip`](Self::zip) is the one place that asks for it. #[must_use = "an encryption description does nothing until a keyset cipher executes it"] -pub struct Encryption<'s, S, T, K, Ctx> { - build: Build<'s, S, T, K, Ctx>, +pub struct Encryption<'s, S: 's, T, K, Ctx, M: SourceMode<'s, S> = Borrowed> { + build: Build<'s, S, T, K, Ctx, M>, } /// A composable description of how `T` is recovered from a stored target. /// @@ -109,7 +132,7 @@ enum Opening { Failed(Error), Open(Open), } -impl fmt::Debug for Encryption<'_, S, T, K, Ctx> { +impl<'s, S: 's, T, K, Ctx, M: SourceMode<'s, S>> fmt::Debug for Encryption<'s, S, T, K, Ctx, M> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Encryption").finish_non_exhaustive() } @@ -124,7 +147,9 @@ impl fmt::Debug for Decryption { } } -impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { +impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's, M: SourceMode<'s, S>> + Encryption<'s, S, T, K, Ctx, M> +{ /// A description whose output is already known — metadata a record /// carries, or a declaration rejected before any key request. pub fn ready(result: Result) -> Self @@ -144,7 +169,7 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { } /// Build the destination from the completed output. `f` sees ciphertext /// and terms, never the plaintext. - pub fn map(self, f: F) -> Encryption<'s, S, U, K, Ctx> + pub fn map(self, f: F) -> Encryption<'s, S, U, K, Ctx, M> where F: FnOnce(T) -> U + MaybeSend + 'static, { @@ -154,7 +179,7 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { } /// [`map`](Self::map) for a conversion that can fail, such as reading /// native output into a destination that does not accept every shape. - pub fn try_map(self, f: F) -> Encryption<'s, S, U, K, Ctx> + pub fn try_map(self, f: F) -> Encryption<'s, S, U, K, Ctx, M> where F: FnOnce(T) -> Result + MaybeSend + 'static, { @@ -165,7 +190,9 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { /// Drive the destination's [`Visitor`](super::transcode::Visitor) from /// this operation's native output, moving leaves and markers across /// without an intermediate tree. - pub fn transcode(self) -> Encryption<'s, S, U, K, Ctx> + pub fn transcode( + self, + ) -> Encryption<'s, S, U, K, Ctx, M> where T: super::transcode::Reader, { @@ -180,16 +207,26 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { /// [`extend`](Self::extend) before it got here — that is how a record /// composes fields with different contexts — and `zip` cannot tell that /// from a target's two halves (ADR-0004, decision 1). + /// + /// Both sides also receive the same plaintext. In the default + /// [`Borrowed`] mode that is the same reference, and costs nothing. In + /// [`Owned`](super::Owned) mode this side gets a clone and `other` takes + /// ownership, so a chain of `n` operations makes `n - 1` copies rather + /// than `n`, and only here does an owned plaintext need to be `Clone`. + /// In Owned mode, a match term next to an operation that consumes the + /// value still needs `S: Clone`. pub fn zip( self, - other: Encryption<'s, S, U, K, Ctx>, - ) -> Encryption<'s, S, (T, U), K, Ctx> + other: Encryption<'s, S, U, K, Ctx, M>, + ) -> Encryption<'s, S, (T, U), K, Ctx, M> where Ctx: Clone, + M: ShareSource<'s, S>, { Encryption { build: Box::new(move |source, cipher, cx| { - (self.build)(source, cipher, cx.clone()).zip((other.build)(source, cipher, cx)) + let (mine, theirs) = M::share(source); + (self.build)(mine, cipher, cx.clone()).zip((other.build)(theirs, cipher, cx)) }), } } @@ -203,7 +240,7 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { /// record storing its own context declares `NonEmpty` while its /// operations want a `CallerContext` — and this adapts the one to the /// other once, at the root. - pub fn accepting(self) -> Encryption<'s, S, T, K, C2> + pub fn accepting(self) -> Encryption<'s, S, T, K, C2, M> where C2: Into + 's, { @@ -212,7 +249,7 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { /// Need a different context, derived from the one supplied by `derive` /// at the root of this subtree. The one place a context changes on its /// way down; every public way of doing so is a closure handed here. - fn needing(self, derive: F) -> Encryption<'s, S, T, K, C2> + fn needing(self, derive: F) -> Encryption<'s, S, T, K, C2, M> where C2: 's, F: FnOnce(C2) -> Ctx + MaybeSend + 's, @@ -233,7 +270,7 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { pub fn under( self, own: NonEmpty + MaybeSend + 's>, - ) -> Encryption<'s, S, T, K, DeclaredContext> + ) -> Encryption<'s, S, T, K, DeclaredContext, M> where Ctx: From, { @@ -251,13 +288,16 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { pub fn extend( self, own: NonEmpty + MaybeSend + 's>, - ) -> Encryption<'s, S, T, K, C> + ) -> Encryption<'s, S, T, K, C, M> where C: Extends + 's, Ctx: From, { self.needing(move |cx: C| cx.extend(own).into()) } +} + +impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { /// Lift a description of a field to a description of the struct that /// holds it, which is how a `struct = T` derive composes its fields. /// @@ -265,6 +305,10 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { /// nothing, so it cannot reach a cipher, and what it returns is encrypted /// under `S`'s own Vitamin C contract. It is a place to pick a field, not /// to re-encode one. + /// + /// A field is picked out of a borrowed struct, so this is a + /// [`Borrowed`]-mode combinator: a field whose operations consume it is + /// cloned, as it always was, and the struct itself never is. pub fn project( self, select: for<'borrow> fn(&'borrow P) -> &'borrow S, @@ -275,8 +319,10 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's> Encryption<'s, S, T, K, Ctx> { } } -impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's + Clone + MaybeSend + 'static> - Encryption<'s, S, T, K, Ctx> +impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's + Clone + MaybeSend + 'static, M> + Encryption<'s, S, T, K, Ctx, M> +where + M: SourceMode<'s, S>, { /// Build the output from the completed operations *and* the context they /// ran under. @@ -284,7 +330,7 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's + Clone + MaybeSend + 'static> /// For a record that stores its own context in a field /// (`#[stash(context_field)]`): the context is supplied when the /// description runs, so the field it populates is filled there too. - pub fn map_with_context(self, f: F) -> Encryption<'s, S, U, K, Ctx> + pub fn map_with_context(self, f: F) -> Encryption<'s, S, U, K, Ctx, M> where F: FnOnce(T, Ctx) -> U + MaybeSend + 'static, { @@ -306,12 +352,16 @@ impl<'s, S: 's, T: 'static, K: 'static, Ctx: 's + Clone + MaybeSend + 'static> /// `AeadContext` where a term needs a [`CallerContext`]. Beside a term, /// [`accepting`](Encryption::accepting) lets it take the term's context — /// the AEAD half of the same value — so the two zip under one context. -pub fn ciphertext<'s, S: crate::Encrypt + Clone + 's, K: 'static>( -) -> Encryption<'s, S, StackCipherText, K, AeadContext> { +/// +/// `Encrypt` consumes the plaintext. In [`Owned`](super::Owned) mode it is +/// handed over, so `S` need not be `Clone`; in the default [`Borrowed`] mode +/// it is cloned once, which is what `M: ConsumeSource<'s, S>` asks. +pub fn ciphertext<'s, S: crate::Encrypt + 's, K: 'static, M: ConsumeSource<'s, S>>( +) -> Encryption<'s, S, StackCipherText, K, AeadContext, M> { Encryption { build: Box::new( move |source, cipher, cx: AeadContext| match cx.validated() { - Ok(ctx) => encrypt_native(source, cipher, ctx), + Ok(ctx) => encrypt_native(M::take(source), cipher, ctx), Err(e) => Pending::failed(cipher, e), }, ), @@ -319,21 +369,45 @@ pub fn ciphertext<'s, S: crate::Encrypt + Clone + 's, K: 'static>( } /// A term operation: `$function` produces `$output` from any `S` satisfying /// the bounds, under the [`CallerContext`] the tree hands it. +/// +/// `consume` terms (equality, ORE, OPE) take the plaintext by value, as their +/// scheme does, so they ask the same `M: ConsumeSource<'s, S>` as +/// [`ciphertext`]. A `view` term (match) only reads it, so it works in any +/// mode and never clones. macro_rules! term_operation { ( $(#[$doc:meta])* - $function:ident, $output:ty, [$($generics:tt)*], [$($bounds:tt)*] + $function:ident, $output:ty, consume, [$($generics:tt)*], [$($bounds:tt)*] + ) => { + $(#[$doc])* + pub fn $function<'s, S, K: 'static, M: ConsumeSource<'s, S>, $($generics)*>( + ) -> Encryption<'s, S, $output, K, CallerContext, M> + where + S: 's, + $($bounds)* + { + Encryption { + build: Box::new(move |source, cipher, cx: CallerContext| match cx.validated() { + Ok(ctx) => <$output as Term>::encrypt_from(M::take(source), cipher, ctx), + Err(e) => Pending::failed(cipher, e), + }), + } + } + }; + ( + $(#[$doc:meta])* + $function:ident, $output:ty, view, [$($generics:tt)*], [$($bounds:tt)*] ) => { $(#[$doc])* - pub fn $function<'s, S, K: 'static, $($generics)*>( - ) -> Encryption<'s, S, $output, K, CallerContext> + pub fn $function<'s, S, K: 'static, M: SourceMode<'s, S>, $($generics)*>( + ) -> Encryption<'s, S, $output, K, CallerContext, M> where S: 's, $($bounds)* { Encryption { build: Box::new(move |source, cipher, cx: CallerContext| match cx.validated() { - Ok(ctx) => <$output as Term>::encrypt_from(source, cipher, ctx), + Ok(ctx) => <$output as Term<&S, K, _>>::encrypt_from(M::view(&source), cipher, ctx), Err(e) => Pending::failed(cipher, e), }), } @@ -343,25 +417,25 @@ macro_rules! term_operation { term_operation!( /// The equality term of `S` under the context the tree hands it. Requires /// only `S`'s PRF contract, not recoverable encryption. - equality, crate::sem::EqualityTerm, [], [S: vitaminc_prf::PrfValue + Clone] + equality, crate::sem::EqualityTerm, consume, [], [S: vitaminc_prf::PrfValue] ); term_operation!( /// The match term of any text `S` under the context the tree hands it, /// tokenised and hashed as `O` declares. - matching, crate::sem::MatchTerm, [O: crate::sem::MatchConfig + 'static], [S: AsRef] + matching, crate::sem::MatchTerm, view, [O: crate::sem::MatchConfig + 'static], [S: AsRef] ); term_operation!( /// The order-revealing term of `S` under the context the tree hands it. /// The bounds are the leaf's own: they say which `S` the CLLW ORE scheme /// can order. - ore, crate::sem::OreTerm, [], - [S: cllw_ore::CllwOreEncrypt + Clone + Send + 'static, S::Output: Send + 'static] + ore, crate::sem::OreTerm, consume, [], + [S: cllw_ore::CllwOreEncrypt + Send + 'static, S::Output: Send + 'static] ); term_operation!( /// The order-preserving term of `S` under the context the tree hands it, /// with the same bounds as [`ore`]. - ope, crate::sem::OpeTerm, [], - [S: cllw_ore::CllwOpeEncrypt + Clone + Send + 'static, S::Output: Send + 'static] + ope, crate::sem::OpeTerm, consume, [], + [S: cllw_ore::CllwOpeEncrypt + Send + 'static, S::Output: Send + 'static] ); impl Decryption { @@ -475,6 +549,50 @@ impl KeysetCipher<'_, K> { { (T::encryption().build)(source, self, context) } + /// Run a description held in a variable over `source`, under `context`. + /// + /// `source` is what the description's mode hands its operations: `&S` + /// for a [`Borrowed`]-mode description (the default, and what + /// [`encrypt_as`](Self::encrypt_as) runs for a type), or `S` itself for + /// an [`Owned`](super::Owned)-mode one. Owned mode is how a plaintext + /// that is not `Clone` reaches an operation: a single operation consumes + /// it without a copy. + /// + /// ``` + /// # async fn example() -> Result<(), stack_encrypt::Error> { + /// use stack_encrypt::kms::FakeDataKeySource; + /// use stack_encrypt::target::{self, AeadContext, Owned}; + /// use stack_encrypt::{nonempty, StackCipher, StackCipherText}; + /// use vitaminc_protected::Protected; + /// + /// let cipher = StackCipher::builder().kms(FakeDataKeySource::new()).init().await?; + /// let keyset = cipher.default_keyset(); + /// // `Protected` is deliberately not `Clone`: it is moved in, and + /// // the one copy is wiped once it is sealed. + /// let card = Protected::new(String::from("4111 1111 1111 1111")); + /// let sealed: StackCipherText = keyset + /// .run( + /// target::ciphertext::<_, _, Owned>(), + /// card, + /// AeadContext::from(nonempty!("cards/number")), + /// ) + /// .await?; + /// # let _ = sealed; + /// # Ok(()) + /// # } + /// # tokio_test_block_on(example()).unwrap(); + /// # fn tokio_test_block_on(f: F) -> F::Output { + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(f) + /// # } + /// ``` + pub fn run<'a, 's, S: 's, T: 'static, Ctx, M: SourceMode<'s, S>>( + &'a self, + encryption: Encryption<'s, S, T, K, Ctx, M>, + source: M::Source, + context: Ctx, + ) -> Pending<'a, T, K> { + (encryption.build)(source, self, context) + } /// Recover `P` from `source`, as its declaration describes. A leaf sealed /// under another keyset is refused ([`Error::ForeignKeyset`]) before any /// key is retrieved. @@ -586,6 +704,12 @@ pub trait DecryptFrom: Sized + 'static { } impl DecryptFrom for T {} +// The leaf declarations below keep `Clone` on purpose. An `EncryptFrom` +// declaration runs in the `Borrowed` mode (`encrypt_as` hands it `&S`), and an +// operation that consumes a borrowed plaintext must clone it: the bound is +// `Borrowed: ConsumeSource<'s, S>`, spelled out. The constructors themselves +// (`ciphertext`, `equality`, `ore`, `ope`) ask only for the scheme's own +// capability, and run a non-`Clone` plaintext in `Owned` mode. impl EncryptFrom for StackCipherText { type Context = AeadContext; fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> diff --git a/packages/stack-encrypt/src/target/source.rs b/packages/stack-encrypt/src/target/source.rs new file mode 100644 index 000000000..4c9579e29 --- /dev/null +++ b/packages/stack-encrypt/src/target/source.rs @@ -0,0 +1,102 @@ +//! How an [`Encryption`](super::Encryption) receives its plaintext. The +//! explanation is on [`SourceMode`], which is what the public docs show; this +//! module is private and its items are re-exported from `target`. + +/// A description's plaintext is handed to it by reference: the default mode, +/// and the one every [`EncryptFrom`](super::EncryptFrom) declaration runs in. +/// An operation that consumes the plaintext clones it. +#[derive(Debug)] +pub enum Borrowed {} + +/// A description's plaintext is handed to it by value. A single operation +/// consumes it without a copy; only fan-out over it needs `Clone`. +#[derive(Debug)] +pub enum Owned {} + +mod sealed { + pub trait Sealed {} + impl Sealed for super::Borrowed {} + impl Sealed for super::Owned {} +} + +/// How a description receives its plaintext: the `M` parameter of +/// [`Encryption`](super::Encryption). +/// +/// Vitamin C's `Encrypt`, `PrfValue` and `CllwOreEncrypt` consume the value +/// they are given, so an operation needs the plaintext by value. A +/// description can be handed it two ways, and because the mode is a type +/// parameter the difference is settled at compile time: +/// +/// - [`Borrowed`], the default: the description is handed `&S`. This is what +/// [`encrypt_as`](crate::KeysetCipher::encrypt_as) does for every +/// [`EncryptFrom`](super::EncryptFrom) declaration, and what lets a record +/// pick its fields out of a borrowed struct. An operation that consumes the +/// plaintext clones it first ([`ConsumeSource`]), so here, and only here, +/// `S` must be `Clone`. +/// - [`Owned`]: the description is handed `S` itself, through +/// [`KeysetCipher::run`](crate::KeysetCipher::run). A single operation +/// consumes it with no copy, so a plaintext that is deliberately not +/// `Clone` (one that is moved and wiped, such as a zeroizing FFI value) can +/// be sealed or indexed. Running two operations over one value +/// ([`zip`](super::Encryption::zip)) hands a copy to every side but the +/// last, which takes ownership ([`ShareSource`]); that, and only that, +/// needs `Clone`. +/// +/// A match term only reads its text, so it runs in either mode without a +/// copy. The traits are sealed: these two modes are the only ones. +pub trait SourceMode<'s, S: 's>: sealed::Sealed + 's { + /// What the description is handed: `&'s S` or `S`. + type Source; + /// Read the plaintext in place, for an operation that does not consume + /// it (a match term reads text through `AsRef`). + fn view(source: &Self::Source) -> &S; +} + +/// A mode in which an operation can take the plaintext by value: always for +/// [`Owned`]; for [`Borrowed`] when `S: Clone`, by cloning it. +pub trait ConsumeSource<'s, S: 's>: SourceMode<'s, S> { + /// The plaintext, by value. + fn take(source: Self::Source) -> S; +} + +/// A mode in which one plaintext can be handed to two operations: always +/// for [`Borrowed`], where the reference is copied; for [`Owned`] when +/// `S: Clone`, where the first side gets a clone and the second the value. +pub trait ShareSource<'s, S: 's>: SourceMode<'s, S> { + /// The plaintext for the first side, and for the second. + fn share(source: Self::Source) -> (Self::Source, Self::Source); +} + +impl<'s, S: 's> SourceMode<'s, S> for Borrowed { + type Source = &'s S; + fn view(source: &Self::Source) -> &S { + source + } +} +impl<'s, S: Clone + 's> ConsumeSource<'s, S> for Borrowed { + fn take(source: Self::Source) -> S { + source.clone() + } +} +impl<'s, S: 's> ShareSource<'s, S> for Borrowed { + fn share(source: Self::Source) -> (Self::Source, Self::Source) { + (source, source) + } +} + +impl<'s, S: 's> SourceMode<'s, S> for Owned { + type Source = S; + fn view(source: &Self::Source) -> &S { + source + } +} +impl<'s, S: 's> ConsumeSource<'s, S> for Owned { + fn take(source: Self::Source) -> S { + source + } +} +impl<'s, S: Clone + 's> ShareSource<'s, S> for Owned { + fn share(source: Self::Source) -> (Self::Source, Self::Source) { + (source.clone(), source) + } +} diff --git a/packages/stack-encrypt/tests/source_mode.rs b/packages/stack-encrypt/tests/source_mode.rs new file mode 100644 index 000000000..1b2d0f80f --- /dev/null +++ b/packages/stack-encrypt/tests/source_mode.rs @@ -0,0 +1,441 @@ +//! How a description receives its plaintext (`target::{Borrowed, Owned}`). +//! +//! Vitamin C's `Encrypt` and `PrfValue` consume their input. A description +//! handed a borrow (the default, and what every `EncryptFrom` declaration +//! runs in) clones before an operation consumes; one handed the value +//! (`Owned`) gives it to the operation. These tests hold the two claims that +//! make owned mode worth having: +//! +//! - a plaintext that is **not `Clone`** runs a single operation — a +//! ciphertext alone, or one term alone — and the output is the same as the +//! borrowed path's; +//! - the number of copies is exactly what the mode promises: none for one +//! owned operation, `n - 1` for an owned `zip` of `n`, one per consuming +//! operation when borrowed, and none ever for a match term, which only +//! reads its text. + +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use stack_encrypt::sem::{EqualityTerm, MatchTerm}; +use stack_encrypt::target::{ + ciphertext, equality, matching, AeadContext, Borrowed, CallerContext, Encryption, Owned, +}; +use stack_encrypt::{nonempty, Encrypt, NonEmpty, StackCipherText}; +use stack_kms::FakeDataKeySource; +use vitaminc_aead::{Cipher, IntoAad}; +use vitaminc_prf::{IntoPrfContext, Prf, PrfValue, PrfVisitor}; +use vitaminc_protected::{Controlled, Protected}; + +mod common; +use common::stack_cipher; + +const NUMBER: &str = "4111 1111 1111 1111"; + +fn context() -> NonEmpty<&'static str> { + nonempty!("cards/number") +} +fn aead() -> AeadContext { + AeadContext::from(context()) +} +fn caller() -> CallerContext { + CallerContext::from(context()) +} + +/// A secret that is deliberately not `Clone`: it is moved, never copied, and +/// `Protected` wipes the one copy when it drops. It encrypts, derives terms +/// and reads as text exactly as the `String` it holds does. +struct Secret(Protected); + +impl Secret { + fn new(text: &str) -> Self { + Self(Protected::new(text.to_string())) + } +} +impl Encrypt for Secret { + fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result + where + C: Cipher, + A: IntoAad<'a>, + { + self.0.encrypt_with_aad(cipher, aad) + } +} +impl PrfValue for Secret { + fn prf_visit_with_context<'a, P, V, C>(self, prf: &P, context: C, visitor: V) -> P::Ok + where + P: Prf, + V: PrfVisitor, + C: IntoPrfContext<'a>, + { + self.0.prf_visit_with_context(prf, context, visitor) + } +} +impl AsRef for Secret { + fn as_ref(&self) -> &str { + self.0.risky_ref() + } +} + +/// A `String` that counts its clones, so a test can say how many copies of +/// the plaintext a description made. +struct Counted { + text: String, + clones: Arc, +} + +impl Counted { + fn new(text: &str) -> (Self, Arc) { + let clones = Arc::new(AtomicUsize::new(0)); + let value = Self { + text: text.to_string(), + clones: clones.clone(), + }; + (value, clones) + } +} +impl Clone for Counted { + fn clone(&self) -> Self { + let _ = self.clones.fetch_add(1, Ordering::SeqCst); + Self { + text: self.text.clone(), + clones: self.clones.clone(), + } + } +} +impl Encrypt for Counted { + fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result + where + C: Cipher, + A: IntoAad<'a>, + { + self.text.encrypt_with_aad(cipher, aad) + } +} +impl PrfValue for Counted { + fn prf_visit_with_context<'a, P, V, C>(self, prf: &P, context: C, visitor: V) -> P::Ok + where + P: Prf, + V: PrfVisitor, + C: IntoPrfContext<'a>, + { + self.text.prf_visit_with_context(prf, context, visitor) + } +} +impl AsRef for Counted { + fn as_ref(&self) -> &str { + &self.text + } +} + +// --- A plaintext that is not `Clone` ---------------------------------------- + +#[tokio::test] +async fn a_plaintext_that_is_not_clone_seals_through_ciphertext_alone() { + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let sealed: StackCipherText = keyset + .run(ciphertext::<_, _, Owned>(), Secret::new(NUMBER), aead()) + .await + .unwrap(); + + let opened: String = cipher.decrypt_as(sealed, aead()).await.unwrap(); + assert_eq!( + opened, NUMBER, + "an owned, non-Clone plaintext should seal to a ciphertext that opens to its text" + ); +} + +#[tokio::test] +async fn a_plaintext_that_is_not_clone_derives_an_equality_term_alone() { + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let term: EqualityTerm = keyset + .run(equality::<_, _, Owned>(), Secret::new(NUMBER), caller()) + .await + .unwrap(); + + let expected = keyset.equality_term(NUMBER, context()).await.unwrap(); + assert_eq!( + term, expected, + "an owned equality term should be byte-identical to the same text's term" + ); +} + +#[tokio::test] +async fn a_plaintext_that_is_not_clone_derives_a_match_term_alone() { + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let term: MatchTerm = keyset + .run(matching::<_, _, Owned, _>(), Secret::new(NUMBER), caller()) + .await + .unwrap(); + + let expected: MatchTerm = keyset.match_terms(NUMBER, context()).await.unwrap(); + assert_eq!( + term, expected, + "an owned match term should be byte-identical to the same text's term" + ); +} + +/// The case the owned mode exists for: the dynamic value an FFI binding +/// decodes is zeroized on drop and is not `Clone`. +#[cfg(feature = "dynamic")] +#[tokio::test] +async fn a_zeroizing_ffi_value_seals_through_ciphertext_alone() { + use vitaminc_aead_value::FfiValue; + + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let sealed: StackCipherText = keyset + .run( + ciphertext::<_, _, Owned>(), + FfiValue::String(NUMBER.into()), + aead(), + ) + .await + .unwrap(); + + let opened: FfiValue = cipher.decrypt_as(sealed, aead()).await.unwrap(); + let FfiValue::String(text) = opened else { + panic!("an FfiValue string should open to a string"); + }; + assert_eq!( + text.risky_ref(), + NUMBER.as_bytes(), + "an FfiValue should seal by value and open to the same text" + ); +} + +// --- How many copies each mode makes ---------------------------------------- + +#[tokio::test] +async fn one_owned_operation_makes_no_copy() { + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let (value, clones) = Counted::new(NUMBER); + let _: StackCipherText = keyset + .run(ciphertext::<_, _, Owned>(), value, aead()) + .await + .unwrap(); + assert_eq!(clones.load(Ordering::SeqCst), 0, "an owned ciphertext"); + + let (value, clones) = Counted::new(NUMBER); + let _: EqualityTerm = keyset + .run(equality::<_, _, Owned>(), value, caller()) + .await + .unwrap(); + assert_eq!(clones.load(Ordering::SeqCst), 0, "an owned equality term"); +} + +/// A ciphertext beside an equality term, as a target composes them. +fn sealed_and_indexed<'s, S, M>( +) -> Encryption<'s, S, (StackCipherText, EqualityTerm), FakeDataKeySource, CallerContext, M> +where + S: Encrypt + PrfValue + 's, + M: stack_encrypt::target::ConsumeSource<'s, S> + stack_encrypt::target::ShareSource<'s, S>, +{ + ciphertext().accepting().zip(equality()) +} + +#[tokio::test] +async fn an_owned_zip_copies_for_every_side_but_the_last() { + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let (value, clones) = Counted::new(NUMBER); + let (sealed, term) = keyset + .run(sealed_and_indexed::<_, Owned>(), value, caller()) + .await + .unwrap(); + assert_eq!( + clones.load(Ordering::SeqCst), + 1, + "two owned operations should share one copy: the last takes the value" + ); + + // And what each side made is what it would have made alone. + let opened: String = cipher.decrypt_as(sealed, aead()).await.unwrap(); + assert_eq!(opened, NUMBER, "the zipped ciphertext opens to the text"); + let expected = keyset.equality_term(NUMBER, context()).await.unwrap(); + assert_eq!(term, expected, "the zipped term is the text's term"); + + let (value, clones) = Counted::new(NUMBER); + let three = sealed_and_indexed::<_, Owned>().zip(equality::<_, _, Owned>()); + let (_, again) = keyset.run(three, value, caller()).await.unwrap(); + assert_eq!( + clones.load(Ordering::SeqCst), + 2, + "three owned operations should make two copies" + ); + assert_eq!(again, expected, "the third side's term is the text's term"); +} + +#[tokio::test] +async fn a_borrowed_operation_copies_once_for_each_operation_that_consumes() { + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let (value, clones) = Counted::new(NUMBER); + let sealed: StackCipherText = keyset + .run(ciphertext::<_, _, Borrowed>(), &value, aead()) + .await + .unwrap(); + assert_eq!(clones.load(Ordering::SeqCst), 1, "a borrowed ciphertext"); + let opened: String = cipher.decrypt_as(sealed, aead()).await.unwrap(); + assert_eq!(opened, NUMBER, "the borrowed ciphertext opens to the text"); + + let (value, clones) = Counted::new(NUMBER); + let (_, term) = keyset + .run(sealed_and_indexed::<_, Borrowed>(), &value, caller()) + .await + .unwrap(); + assert_eq!( + clones.load(Ordering::SeqCst), + 2, + "a borrowed ciphertext beside a borrowed term copies once for each" + ); + let expected = keyset.equality_term(NUMBER, context()).await.unwrap(); + assert_eq!(term, expected, "the borrowed term is the text's term"); +} + +#[tokio::test] +async fn a_match_term_never_copies_its_text() { + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let (value, clones) = Counted::new(NUMBER); + let _: MatchTerm = keyset + .run(matching::<_, _, Borrowed, _>(), &value, caller()) + .await + .unwrap(); + let _: MatchTerm = keyset + .run(matching::<_, _, Owned, _>(), value, caller()) + .await + .unwrap(); + assert_eq!( + clones.load(Ordering::SeqCst), + 0, + "a match term reads its text in either mode" + ); +} + +// --- When an owned plaintext is dropped ------------------------------------- + +/// A plaintext that counts its drops, so a test can say when an owned value +/// is let go. Its operations take the text out of it, as a zeroizing type +/// would hand its bytes over, and the husk drops at the end of the call. +struct Dropped { + text: String, + drops: Arc, +} + +impl Dropped { + fn new(text: &str) -> (Self, Arc) { + let drops = Arc::new(AtomicUsize::new(0)); + let value = Self { + text: text.to_string(), + drops: drops.clone(), + }; + (value, drops) + } +} +impl Drop for Dropped { + fn drop(&mut self) { + let _ = self.drops.fetch_add(1, Ordering::SeqCst); + } +} +impl Encrypt for Dropped { + fn encrypt_with_aad<'a, C, A>(mut self, cipher: C, aad: A) -> Result + where + C: Cipher, + A: IntoAad<'a>, + { + std::mem::take(&mut self.text).encrypt_with_aad(cipher, aad) + } +} +impl PrfValue for Dropped { + fn prf_visit_with_context<'a, P, V, C>( + mut self, + prf: &P, + context: C, + visitor: V, + ) -> P::Ok + where + P: Prf, + V: PrfVisitor, + C: IntoPrfContext<'a>, + { + std::mem::take(&mut self.text).prf_visit_with_context(prf, context, visitor) + } +} +impl AsRef for Dropped { + fn as_ref(&self) -> &str { + &self.text + } +} + +/// Owned mode exists so a zeroizing plaintext moves once and is wiped. Each +/// operation must let it go while the description runs, before `run` +/// returns its `Pending` — not keep it alive across the key request. +#[tokio::test] +async fn an_owned_plaintext_is_dropped_before_its_key_request_is_sent() { + let cipher = stack_cipher().await; + let keyset = cipher.default_keyset(); + + let (value, drops) = Dropped::new(NUMBER); + let pending = keyset.run(ciphertext::<_, _, Owned>(), value, aead()); + assert_eq!( + drops.load(Ordering::SeqCst), + 1, + "ciphertext: dropped before the request is sent" + ); + let sealed: StackCipherText = pending.await.unwrap(); + assert_eq!( + drops.load(Ordering::SeqCst), + 1, + "ciphertext: dropped exactly once" + ); + let opened: String = cipher.decrypt_as(sealed, aead()).await.unwrap(); + assert_eq!( + opened, NUMBER, + "the text was sealed before the husk dropped" + ); + + let (value, drops) = Dropped::new(NUMBER); + let pending = keyset.run(equality::<_, _, Owned>(), value, caller()); + assert_eq!( + drops.load(Ordering::SeqCst), + 1, + "equality: dropped before run returns" + ); + let term: EqualityTerm = pending.await.unwrap(); + assert_eq!( + drops.load(Ordering::SeqCst), + 1, + "equality: dropped exactly once" + ); + let expected = keyset.equality_term(NUMBER, context()).await.unwrap(); + assert_eq!(term, expected, "the term is the text's term"); + + let (value, drops) = Dropped::new(NUMBER); + let pending = keyset.run(matching::<_, _, Owned, _>(), value, caller()); + assert_eq!( + drops.load(Ordering::SeqCst), + 1, + "matching: dropped before run returns" + ); + let term: MatchTerm = pending.await.unwrap(); + assert_eq!( + drops.load(Ordering::SeqCst), + 1, + "matching: dropped exactly once" + ); + let expected: MatchTerm = keyset.match_terms(NUMBER, context()).await.unwrap(); + assert_eq!(term, expected, "the term is the text's term"); +} diff --git a/packages/stack-encrypt/tests/ui/divergent_context_in_target.stderr b/packages/stack-encrypt/tests/ui/divergent_context_in_target.stderr index fc60d6a2d..b532d4ea4 100644 --- a/packages/stack-encrypt/tests/ui/divergent_context_in_target.stderr +++ b/packages/stack-encrypt/tests/ui/divergent_context_in_target.stderr @@ -2,12 +2,12 @@ error[E0308]: mismatched types --> tests/ui/divergent_context_in_target.rs:30:18 | 30 | .zip(equality()) - | --- ^^^^^^^^^^ expected `Encryption<'_, _, _, _, DeclaredContext>`, found `Encryption<'_, _, EqualityTerm, _, ...>` + | --- ^^^^^^^^^^ expected `Encryption<'_, _, _, _, ..., _>`, found `Encryption<'_, _, ..., _, ..., _>` | | | arguments to this method are incorrect | - = note: expected struct `Encryption<'_, _, _, _, DeclaredContext>` - found struct `Encryption<'_, _, EqualityTerm, _, CallerContext>` + = note: expected struct `Encryption<'_, _, _, _, DeclaredContext, _>` + found struct `Encryption<'_, _, EqualityTerm, _, CallerContext, _>` note: method defined here --> src/target/operations.rs | diff --git a/packages/stack-encrypt/tests/ui/nested_leaf_without_context.stderr b/packages/stack-encrypt/tests/ui/nested_leaf_without_context.stderr index e6f1c201d..5bd1addde 100644 --- a/packages/stack-encrypt/tests/ui/nested_leaf_without_context.stderr +++ b/packages/stack-encrypt/tests/ui/nested_leaf_without_context.stderr @@ -15,14 +15,14 @@ error[E0277]: the trait bound `AeadContext: From` is not satisf `AeadContext` implements `From` and $N others = note: required for `DeclaredContext` to implement `Into` -note: required by a bound in `Encryption::<'s, S, T, K, Ctx>::accepting` +note: required by a bound in `Encryption::<'s, S, T, K, Ctx, M>::accepting` --> src/target/operations.rs | - | pub fn accepting(self) -> Encryption<'s, S, T, K, C2> + | pub fn accepting(self) -> Encryption<'s, S, T, K, C2, M> | --------- required by a bound in this associated function | where | C2: Into + 's, - | ^^^^^^^^^ required by this bound in `Encryption::<'s, S, T, K, Ctx>::accepting` + | ^^^^^^^^^ required by this bound in `Encryption::<'s, S, T, K, Ctx, M>::accepting` error[E0277]: the trait bound `AeadContext: From` is not satisfied --> tests/ui/nested_leaf_without_context.rs:15:12 diff --git a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs new file mode 100644 index 000000000..8c808dca1 --- /dev/null +++ b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs @@ -0,0 +1,19 @@ +//! Owned mode hands the plaintext to an operation by value, so a single +//! operation needs no `Clone`. Two operations over one owned value are the +//! one place a copy is unavoidable — the first side gets a clone, the last +//! the value — and the refusal names `zip`, not the operations. +use stack_encrypt::target::{ciphertext, equality, CallerContext, Owned}; +use stack_encrypt::{nonempty, Encrypt, KeysetCipher}; +use stack_kms::FakeDataKeySource; + +fn seal_and_index( + keyset: &KeysetCipher<'_, FakeDataKeySource>, + value: S, +) { + let both = ciphertext::<_, _, Owned>() + .accepting::() + .zip(equality::<_, _, Owned>()); + let _ = keyset.run(both, value, CallerContext::from(nonempty!("column"))); +} + +fn main() {} diff --git a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr new file mode 100644 index 000000000..aca006b38 --- /dev/null +++ b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr @@ -0,0 +1,19 @@ +error[E0277]: the trait bound `S: Clone` is not satisfied + --> tests/ui/owned_fan_out_needs_clone.rs:15:10 + | +15 | .zip(equality::<_, _, Owned>()); + | ^^^ the trait `Clone` is not implemented for `S` + | + = note: required for `stack_encrypt::target::Owned` to implement `ShareSource<'_, S>` +note: required by a bound in `Encryption::<'s, S, T, K, Ctx, M>::zip` + --> src/target/operations.rs + | + | pub fn zip( + | --- required by a bound in this associated function +... + | M: ShareSource<'s, S>, + | ^^^^^^^^^^^^^^^^^^ required by this bound in `Encryption::<'s, S, T, K, Ctx, M>::zip` +help: consider further restricting type parameter `S` with trait `Clone` + | + 9 | fn seal_and_index( + | +++++++++++++++++++ diff --git a/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs b/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs new file mode 100644 index 000000000..ea6331265 --- /dev/null +++ b/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs @@ -0,0 +1,55 @@ +//! A single operation in owned mode asks nothing of the plaintext beyond the +//! operation's own capability: no `Clone`, so a value that is moved and +//! wiped can be sealed or indexed. +use stack_encrypt::sem::{CllwOpeEncrypt, CllwOreEncrypt, EqualityTerm, MatchTerm, OpeTerm, OreTerm}; +use stack_encrypt::target::{ + ciphertext, equality, matching, ope, ore, CallerContext, Owned, Pending, +}; +use stack_encrypt::{nonempty, Encrypt, KeysetCipher, StackCipherText}; +use stack_kms::FakeDataKeySource; + +fn seal<'a, S: Encrypt>( + keyset: &'a KeysetCipher<'_, FakeDataKeySource>, + value: S, +) -> Pending<'a, StackCipherText, FakeDataKeySource> { + keyset.run(ciphertext::<_, _, Owned>(), value, nonempty!("column").into()) +} +fn index<'a, S: vitaminc_prf::PrfValue>( + keyset: &'a KeysetCipher<'_, FakeDataKeySource>, + value: S, +) -> Pending<'a, EqualityTerm, FakeDataKeySource> { + keyset.run(equality::<_, _, Owned>(), value, CallerContext::from(nonempty!("column"))) +} +fn search<'a, S: AsRef>( + keyset: &'a KeysetCipher<'_, FakeDataKeySource>, + value: S, +) -> Pending<'a, MatchTerm, FakeDataKeySource> { + keyset.run(matching::<_, _, Owned, _>(), value, CallerContext::from(nonempty!("column"))) +} +// Bounded only by the scheme's own trait: `CllwOreEncrypt` and +// `CllwOpeEncrypt` do not imply `Clone`, so `Clone` returning to either +// constructor's bounds fails this file. +fn order<'a, S>( + keyset: &'a KeysetCipher<'_, FakeDataKeySource>, + value: S, +) -> Pending<'a, OreTerm, FakeDataKeySource> +where + S: CllwOreEncrypt + Send + 'static, + S::Output: Send + 'static, +{ + keyset.run(ore::<_, _, Owned>(), value, CallerContext::from(nonempty!("column"))) +} +fn order_preserving<'a, S>( + keyset: &'a KeysetCipher<'_, FakeDataKeySource>, + value: S, +) -> Pending<'a, OpeTerm, FakeDataKeySource> +where + S: CllwOpeEncrypt + Send + 'static, + S::Output: Send + 'static, +{ + keyset.run(ope::<_, _, Owned>(), value, CallerContext::from(nonempty!("column"))) +} +fn main() { + let _ = (seal::, index::, search::); + let _ = (order::, order_preserving::); +}