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
24 changes: 24 additions & 0 deletions packages/stack-encrypt/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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::<S, K, M>()`,
and in `matching` it comes before `O` (`matching::<S, K, M, O>()`). 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
Expand Down
20 changes: 10 additions & 10 deletions packages/stack-encrypt/src/sem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,21 +341,21 @@ where
/// carries no requests.
impl<'c, S, K, T> Term<S, K, NonEmpty<T>> 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<T>,
) -> Pending<'a, Self, K>
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)
}
}
Expand Down Expand Up @@ -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<T>,
) -> Pending<'a, Self, K>
Expand Down Expand Up @@ -942,22 +942,22 @@ where
/// carries no requests.
impl<'c, S, K, T> Term<S, K, NonEmpty<T>> for OreTerm<S>
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<T>,
) -> Pending<'a, Self, K>
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)
}
}
Expand All @@ -967,22 +967,22 @@ where
/// carries no requests.
impl<'c, S, K, T> Term<S, K, NonEmpty<T>> for OpeTerm<S>
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<T>,
) -> Pending<'a, Self, K>
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)
}
}
Expand Down
16 changes: 12 additions & 4 deletions packages/stack-encrypt/src/target/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@ 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<S, K, Ctx>: Sized {
fn encrypt_from<'a>(
source: &S,
source: S,
cipher: &'a KeysetCipher<'_, K>,
context: Ctx,
) -> Pending<'a, Self, K>
where
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<T>,
) -> Pending<'a, StackCipherText, K> {
Expand All @@ -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)),
}
Expand Down
9 changes: 9 additions & 0 deletions packages/stack-encrypt/src/target/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Target>` describes independently encrypted rows under the same context.
Expand All @@ -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};
Expand All @@ -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};
Loading
Loading