Skip to content

feat(stack-encrypt): add owned scalar ciphertext API - #1404

Draft
coderdan wants to merge 1 commit into
nextfrom
feat/rust-scalar-ciphertext
Draft

coderdan wants to merge 1 commit into
nextfrom
feat/rust-scalar-ciphertext

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

The direct Rust API previously required an AAD argument and returned a structural node even for one integer. This implements the first scalar slice of the API redesign in #1365:

let encrypted = keyset.encrypt(42i32).await?;
let value: i32 = keyset.decrypt(encrypted).await?;

let encrypted: Ciphertext = 42i32
    .encrypt_into_with_context(&keyset, ("users", "age"))
    .await?;
let value = i32::decrypt_from_with_context(
    encrypted, &keyset, ("users", "age"),
).await?;

EncryptTarget and DecryptSource declare operations; the owned EncryptInto and destination-directed DecryptFrom helpers execute them through the existing batched engine. RequiredContext, OptionalContext, and NoCallerContext control method availability explicitly: Default never grants permission to omit context. Encryption guards the owned source with Zeroizing, plans synchronously through a borrow, and wipes the source before returning the pending request. This imposes no blanket Clone bound and also covers discarded requests, planning errors and context-conversion unwinding. Returned plaintext belongs to the caller; this is not a claim of complete compiler-copy erasure or constant-time serialization.

Ciphertext exposes canonical byte encoding/decoding and provider-tag access over the unchanged SealedValue v1 envelope. Cross-opening tests exercise both old-writer/new-reader and new-writer/old-reader paths. No Go/dynamic grammar, stored encoding, SQL, or cryptographic derivation changes.

Migration and scope

  • Direct structural calls become encrypt_node / decrypt_node. Existing plans and derives retain their declaration engine; their borrowed conversion helpers move to target::legacy. Existing consumers and tests are updated explicitly.
  • Built-in scalar support covers bool, fixed-width signed integers, u16–u128, f32/f64 and owned String. Collections and borrowed plaintext are rejected. u8 is deferred because the current Vitamin C Rust visitors conflate byte buffers with their element specialization; this PR does not add implicit Vec blob behavior.
  • Blob adapters, collection targets, derive migration, broader context roles, typed text encodings, shared definitions, prepared JSON reads and database adapters remain subsequent slices. Custom declarations can already project a scalar from a non-Clone owned source and declare required or fixed context.
  • Release-plz versions the Rust change from the breaking conventional commit. The empty Changeset records that no npm release is required.

Validation

  • Stack Encrypt all-feature nextest suite: 1,008 passed, one existing skip; final scalar suite: 10 passed.
  • Compile-fail tests cover required context despite Default, forbidden context arguments, collections/borrows and ownership transfer. Existing snapshots refreshed for legacy import paths and rechecked without overwrite.
  • EQL text_eq/v4_types: 23 passed, seven existing PostgreSQL tests skipped; includes frozen Go rows opening in Rust.
  • 52 doctests; rustdoc with warnings denied; runnable credential-free scalar example.
  • All-target/all-feature Clippy with warnings denied and cargo fmt.
  • Go encryption guest, including EQL, checks for wasm32-wasip1 with its locked dependencies.

No live ZeroKMS or PostgreSQL tests were run. Based on latest next at ffeed7a.

BREAKING CHANGE: keyset encrypt/decrypt use opaque Ciphertext and separate
context methods. Structural calls move to encrypt_node/decrypt_node;
borrowed conversion helpers move to target::legacy.

Declare owned conversions with EncryptTarget/DecryptSource and explicit
context capabilities. Guard plaintext during planning and preserve the
existing sealed-leaf bytes and Go/dynamic execution paths.
@changeset-bot

changeset-bot Bot commented Oct 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9a44074

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Mutation testing (cargo-mutants, --in-diff, stack-auth + stack-encrypt)

caught missed unviable timeout
7 3 24 0

Surviving mutants: add a test that fails on each before merging

packages/stack-encrypt/src/scalar.rs:55:9: replace <impl std::fmt::Debug for Ciphertext>::fmt -> std::fmt::Result with Ok(Default::default())
packages/stack-encrypt/src/target/input.rs:61:9: replace OptionalContext<C>::into_inner -> Option<C> with None
packages/stack-encrypt/src/target/input.rs:114:9: replace <impl From<NoCallerContext> for super::DeclaredContext>::from -> Self with Default::default()

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant