Skip to content

fix(stack-encrypt): let a non-Clone plaintext run a target operation - #1068

Merged
coderdan merged 2 commits into
mainfrom
fix/stack-encrypt-target-owned-plaintext
Oct 5, 2026
Merged

coderdan merged 2 commits into
mainfrom
fix/stack-encrypt-target-owned-plaintext

Conversation

@coderdan

@coderdan coderdan commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

stack-encrypt is our Rust library that seals each value under its own data key from ZeroKMS (our key service) and can derive search terms (equality, match, ORE, OPE) beside the ciphertext. Its target layer, the Encryption descriptions in src/target/, required every plaintext to be Clone, because each operation copied the value before encrypting it. A value that is deliberately not Clone, so that its one copy is moved and then wiped from memory, could not be encrypted through that layer at all. The WebAssembly guest's FfiValue is such a value, and the plan builder (#1056) needs it to go through this layer.

This PR lets a non-Clone plaintext run a single operation (a ciphertext alone, or one term alone) with no copy, and moves the Clone requirement to the one place it is unavoidable: running several operations over the same owned value (zip). Existing code, EncryptFrom declarations, records and the derive keep working unchanged.

Breaking change

stack-encrypt 0.2.0 is published on crates.io, and this changes public signatures, so the next release that carries it must be 0.3.0, not 0.2.1. The commit is marked breaking (fix(stack-encrypt)!: with a BREAKING CHANGE: footer), and packages/stack-encrypt/CHANGELOG.md has a new ## [Unreleased] section with these entries:

  • Encryption gains a last type parameter, M: SourceMode = Borrowed. Code that names Encryption<'s, S, T, K, Ctx> still compiles and keeps the borrowed behaviour.
  • ciphertext, equality, matching, ore and ope gain a type parameter M. A turbofish must name it (in matching it comes before O), and a call whose result type does not fix the mode must name one.
  • Added: KeysetCipher::run and target::{SourceMode, ConsumeSource, ShareSource, Borrowed, Owned}.

Changes

  • Source mode (src/target/source.rs, new): an Encryption now has a sixth type parameter M saying how it is handed its plaintext. Borrowed (the default) hands it &S, exactly as before. Owned hands it S by value. Both are sealed marker types, with three small traits: SourceMode (what is handed over), ConsumeSource (get the value: free when owned, a clone when borrowed, so only Borrowed needs S: Clone), ShareSource (hand one value to two operations: free when borrowed, one clone when owned, so only Owned needs S: Clone).
  • Constructors (src/target/operations.rs): ciphertext, equality, ore and ope drop Clone and ask M: ConsumeSource<'s, S> instead. matching only reads its text, so it works in either mode and never copies. zip asks M: ShareSource<'s, S>. In owned mode the last operand takes ownership and the others get a copy, so n operations make n - 1 copies instead of n. project stays borrowed-only: it picks a field out of a borrowed struct.
  • Running an owned description: new KeysetCipher::run(encryption, source, context) runs a description held in a variable. source is &S for a borrowed description and S for an owned one. Before this, the only way to run a description was encrypt_as through a type's EncryptFrom impl.
  • Core (src/target/core.rs, src/sem/mod.rs): encrypt_native and the internal Term impls take the plaintext by value, so the single source.clone() moved out of encrypt_native to the point where a borrow becomes a value (Borrowed: ConsumeSource).
  • Unchanged on purpose: the EncryptFrom impls for StackCipherText, EqualityTerm, OreTerm and OpeTerm keep S: Clone. An EncryptFrom declaration always runs borrowed (encrypt_as(&value, ..)), and consuming a borrowed value means cloning it. A comment above the impls says so. The non-Clone path is the constructors, run with KeysetCipher::run.

Verification

Run at the current head, 639858b, with the pinned Rust 1.94.1:

  • mise x --env test -- cargo nextest run -p stack-encrypt -p stack-encrypt-derive --all-features: 338 passed, 0 failed (includes the trybuild ui suite).
  • mise run test:doc: all pass, including the KeysetCipher::run doctest, which seals a Protected<String> (not Clone).
  • mise run doc (rustdoc, warnings are errors): passes.
  • cargo fmt --all --check: clean. cargo clippy --locked --no-deps --workspace --all-targets --all-features -- -D warnings: clean.
  • cargo mutants --no-shuffle -p stack-auth -p stack-encrypt --in-diff <this PR's diff against main>: 13 mutants, all unviable (they replace generic returns with Default::default()), so none survive.
  • The new ore/ope compile-pass check was shown to bite: putting Clone back on ore's bounds makes trybuild fail with the trait bound S: Clone is not satisfied.

Run at the previous head (849c031), and not re-run, because the follow-up commit adds only tests and one rustdoc sentence:

  • mise x --env test -- cargo nextest run --locked --workspace --all-features: 778 passed, 0 failed.
  • Go WASI guest (languages/golang/stackencrypt/guest, a separate Cargo workspace that depends on stack-encrypt): cargo clippy --locked --target wasm32-wasip1 -- -D warnings, cargo clippy --locked --all-targets -- -D warnings and cargo test --locked all pass, with no guest changes.
  • EQL (packages/eql, also depends on stack-encrypt by path): cargo test --locked -p eql-bindings --features stack-encrypt and cargo test --locked -p eql-encryption-tests pass, with no EQL changes.

New tests:

  • tests/source_mode.rs: a Secret newtype over Protected<String> that is not Clone goes through ciphertext(), equality() and matching() alone in owned mode. The ciphertext decrypts to the text, and each term is byte-identical to the one the existing equality_term / match_terms calls give. With the dynamic feature, a real FfiValue is sealed by value and opens to the same text. A clone-counting plaintext pins the copy counts: 0 for one owned operation, 1 for an owned zip of two, 2 for three, 1 per consuming operation when borrowed, and 0 for a match term in either mode.
  • tests/source_mode.rs, an_owned_plaintext_is_dropped_before_its_key_request_is_sent: a drop-counting plaintext shows that ciphertext, equality and matching each drop the owned value while the description runs, before run returns its Pending, and exactly once. A later change that kept the value alive across the ZeroKMS round trip fails it.
  • tests/ui/pass/owned_without_clone.rs: compile-pass check that generic functions bounded only by the scheme's own trait (Encrypt, PrfValue, AsRef<str>, CllwOreEncrypt, CllwOpeEncrypt; no Clone) can run each of the five single operations.
  • tests/ui/owned_fan_out_needs_clone.rs: compile-fail check that an owned zip over a non-Clone type is refused, and the error names zip and S: Clone, not the operations.

Related

Closes #1077

Refs #1056. This unblocks it: the issue lists the S: Encrypt + Clone bound as a hard prerequisite, because the guest's FfiValue is not Clone. It is also a prerequisite for #1057 and #1059.

Review notes

  • Two trybuild snapshots changed, because Encryption now has a sixth type parameter and rustc prints it:
    • nested_leaf_without_context.stderr: the impl path and the quoted accepting signature now show M.
    • divergent_context_in_target.stderr: the full types in the note: lines are unchanged apart from a trailing , _. But the one-line label is now truncated by rustc to Encryption<'_, _, _, _, ..., _> and no longer shows DeclaredContext / EqualityTerm. The error and its cause are the same.
  • Design chosen: a mode type parameter, M, defaulting to Borrowed. It is a sound version of "the last operand of a zip takes ownership". Each mode decides at compile time how an operation gets the value, so there is no runtime branch that could fail. Because the parameter has a default, every existing Encryption<'s, S, T, K, Ctx> spelling, including the trait signature and the derive's output, still means what it meant.
  • Rejected:
    • A second builder shape alone (build: FnOnce(S, ..) with the entry cloning): encrypt_as(&value) would then need the whole source to be Clone, including a derived record's struct. Every projected field would also need a copy of the struct.
    • Cow<'_, S> through build: Cow requires S: ToOwned, which means Clone, so a non-Clone type cannot be in it at all. A hand-rolled owned-or-borrowed enum compiles, but its borrowed arm has to clone, so the leaves still need Clone, or the arm fails at runtime.
    • A declared operation count: it moves the same question to a count that must agree with the tree. That is more machinery for the same result as a mode.
  • Overlap with stack-encrypt: add the engine pieces the plan builder needs (passthrough, run-by-value, Index trait, per-field types) — 0.3.0 #1056: KeysetCipher::run is the combinator-level half of that issue's "execute-by-value" item, which this PR needed so the owned path could be called at all. The plan-level run(&plan, ..) / using remains stack-encrypt: add the engine pieces the plan builder needs (passthrough, run-by-value, Index trait, per-field types) — 0.3.0 #1056's. It may want a different name, or may build on this one.
  • Not done here, needed later: an owned project. A fields plan over an owned FfiValue object will need to move each field out instead of borrowing it, for example with a combinator that splits an owned value into its fields. project is borrowed-only today.
  • Deferred from review: a zip_matching combinator that would let a non-Clone value carry a ciphertext and a match term without a copy. zip's rustdoc now says that this case still needs S: Clone. The plan builder is the consumer, and its index sets go through indexed() in feat(stack-encrypt): passthrough, Index/Indexes, Encrypted<Terms> and field types for the plan builder #1069, so the shape is decided there.
  • Declined from review: run_owned / run_borrowed. run is called from generic code (feat(stack-encrypt): chained plan builder as the single front door for writes, queries and reads #1071), so the turbofish only appears in tests and doctests.
  • Glossary: packages/stack-encrypt/CONTEXT.md (landing with docs(plans): the plan builder, one front door for stack-encrypt, the derive, the FFI and Go #1052) may want an entry for the source mode (Borrowed / Owned). It is not edited here.

@changeset-bot

changeset-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 889bef9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

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

Click here to learn what changesets are, and how to add one.

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

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

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

caught missed unviable timeout
0 0 13 0

Every mutant in the changed lines was caught by a test.

Comment thread packages/stack-encrypt/src/target/source.rs

@coderdan coderdan left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed e8fe8cb (base main). Comment only.

Should fix

  • packages/stack-encrypt/src/target/source.rs:71 — Borrowed::Source = &'s S ties the source borrow to the description's lifetime, where the old dyn FnOnce(&S, ..) was higher-ranked. Nothing in the tree breaks (encrypt_as, the derive and EQL's TextEq/TextEqQuery impls all infer 's per call, and a description built before its value compiles), but KeysetCipher::run is new and public and #1057's build() row in the plan doc wants a boxed, reusable recipe. A boxed recipe must pin one 's (dyn for<'s> Fn() -> Encryption<'s, ..> is E0582), and then keyset.run(recipe(), &local, ..) inside a loop fails with E0597 (reproduced in the worktree). Decide the shape now while SourceMode is sealed: a GAT type Source<'r> keeps the borrow higher-ranked; otherwise document the constraint on run (its rustdoc at operations.rs:552 says &S). Inline comment has the repro.

Nit

  • Spec gap, already disclosed in the PR body: project stays Borrowed-only, so the plan doc's fields() row ("for the FFI select picks by name from an FfiValue object") still needs each field to be Clone. The scalar case that #1056 names as the hard prerequisite is unblocked; the record case is not, and #1057 should carry that as an explicit dependency.

What I checked and found fine: SourceMode/ConsumeSource/ShareSource are genuinely sealed (sealed::Sealed lives in a private mod of a private module and is not re-exported); zip in Owned mode shares as (source.clone(), source) before either side runs, so self gets the copy, other takes ownership, n operations make n-1 copies (test pins 0/1/2), and the self-then-other build order, hence key-request order, is unchanged; encrypt_native, the Term impls and the four constructors only moved the single clone from encrypt_native into Borrowed::take, no second copy anywhere; matching reads through &S in both modes with no clone; on the error paths (cx.validated() failing, descriptor failure in encrypt_native) the owned value is dropped in the closure so a zeroizing type still wipes; KeysetCipher::run is exactly encrypt_as minus the EncryptFrom lookup — one Ctx through validated(), zip still requires Ctx: Clone and feeds both halves the same value, so ADR-0004 holds and a write and a query cannot spell the context differently; run over FfiValue is a bare ciphertext() operation, not an EncryptFrom<FfiValue> target, so ADR-0004 §6 is not contradicted; EncryptFrom leaf impls keep S: Clone as documented; the two changed trybuild snapshots differ only in the added M / trailing , _ and rustc's ... truncation; the new pass/owned_without_clone.rs is picked up by ui.rs's t.pass("tests/ui/pass/*.rs"); cargo test -p stack-encrypt --all-features for source_mode, the run doctest and the ui suite all pass here; no Linear ids in the diff, commit or PR body; the commit is signed (G); the crate's CHANGELOG is release-plz/git-cliff generated, so no manual entry is needed; --in-diff mutants are all unviable (generic returns), which matches the PR's claim.

coderdan added a commit that referenced this pull request Oct 5, 2026
Decrypt through a plan is open, because StackCipher::decrypt already
exists with another signature. A plan has two starts and takes its
context from exactly one of three sources. The typed verb encrypt_into
and the picker are Rust-only. The derive emits the plan only after its
grammar is narrowed and the plan's widened, so the two stay one grammar.
EQL types are assembled per language from standard outputs, with no
registry and no target name in the data grammar.

The lowering table now names what #1068, #1069 and #1071 shipped.
@coderdan
coderdan added this pull request to stack #1075 October 5, 2026 05:11
@coderdan
coderdan force-pushed the fix/stack-encrypt-target-owned-plaintext branch from e8fe8cb to 849c031 Compare October 5, 2026 05:23
@coderdan

coderdan commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main (014a04a) with no conflicts and no content change; commit hashes changed, nothing else.

@coderdan
coderdan marked this pull request as ready for review October 5, 2026 05:34
@coderdan
coderdan requested a review from a team as a code owner October 5, 2026 05:34
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-05T05:40:24.934627Z 849c031 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-05T05:37:51.973604Z 849c031 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 849c031516

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stack-encrypt/src/target/operations.rs
Comment thread packages/stack-encrypt/src/target/source.rs
Comment thread packages/stack-encrypt/src/target/source.rs
coderdan added a commit that referenced this pull request Oct 5, 2026
…intext

Review of #1068 asked whether Borrowed and Owned should derive OpaqueDebug because they stand for a plaintext. They are uninhabited marker enums: no value of either type exists, so their Debug impl can print nothing. The doc now says so, and names where the plaintext actually lives (the description's closure, which has no Debug, and the Pending output).

@cipherstash-bot cipherstash-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendation: 🟡 merge after changes (1 of 4 review job(s) failed)

One change is needed before merge. stack-encrypt is published on crates.io, and this commit changes the arity of five public constructors. Mark the commit as breaking (! and a BREAKING CHANGE: footer), so release-plz does not publish it as a patch release with no upgrade note. The other four findings can wait: one API limit in Owned mode (a copy for zip with a match term), one usability point on KeysetCipher::run, and two test gaps.

The rest of the change is correct. The source-mode traits are sealed. The one clone moved out of encrypt_native into Borrowed::take, with no second copy. matching reads through &S in both modes. On the error paths, an owned value is dropped inside the closure, so a zeroizing type is still wiped.

How this review was made
Agent Model Review type Result
claude claude-opus-5-5 test-gap 2 found, 2 posted
claude claude-opus-5-5 rust 3 found, 3 posted
codex gpt-5.6-sol test-gap failed
codex gpt-5.6-sol rust 0 found, 0 posted

Synthesis: claude-opus-5-5 merged the findings, removed duplicates and dropped findings it could not confirm in the code. 0 posted finding(s) were raised by two or more models.

Plain language: claude-opus-5-5 read every comment as a new reader would. 3 comment(s) had a problem that stopped the reader acting; it rewrote 3.

Stack: position 1 of 5 (:asterisk: #1068, #1069, #1071, #1073, #1074). *️⃣ marks this pull request.

Context loaded: the description, 3 linked issue(s) and 12 discussion entries.

Comment thread packages/stack-encrypt/src/target/operations.rs
Comment thread packages/stack-encrypt/src/target/operations.rs
Comment thread packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs
Comment thread packages/stack-encrypt/tests/source_mode.rs

@auxesis auxesis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @coderdan.

Approved, in anticipation of making this either a minor or major release, per @cipherstash-bot's feedback.

coderdan added a commit that referenced this pull request Oct 5, 2026
…intext

Review of #1068 asked whether Borrowed and Owned should derive OpaqueDebug because they stand for a plaintext. They are uninhabited marker enums: no value of either type exists, so their Debug impl can print nothing. The doc now says so, and names where the plaintext actually lives (the description's closure, which has no Debug, and the Pending output).
@coderdan
coderdan force-pushed the fix/stack-encrypt-target-owned-plaintext branch from 849c031 to 639858b Compare October 5, 2026 07:28
@coderdan

coderdan commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Review feedback addressed. New head: 639858b (was 849c031).

  • Marked breaking. The original commit is now fix(stack-encrypt)!: let a non-Clone plaintext run a target operation (7b9ca94), with a BREAKING CHANGE: footer naming the new M parameter on Encryption and on the five constructors. packages/stack-encrypt/CHANGELOG.md gains an ## [Unreleased] section with ### Breaking and ### Added entries, so the next release is 0.3.0.
  • Added (639858b, test(stack-encrypt): pin the Owned-mode drop and the Clone-free ore/ope):
    • a drop-counting test showing that ciphertext, equality and matching drop an owned plaintext before run returns its Pending, and exactly once;
    • ore and ope in the compile-pass file, bounded only by CllwOreEncrypt / CllwOpeEncrypt. Checked that it bites: putting Clone back on ore fails trybuild;
    • one sentence in zip's rustdoc: in Owned mode, a match term next to an operation that consumes the value still needs S: Clone.
  • Deferred: zip_matching. The consumer is the plan builder, whose index sets go through indexed() in feat(stack-encrypt): passthrough, Index/Indexes, Encrypted<Terms> and field types for the plan builder #1069, so the shape is decided there when a non-Clone source with a match index arrives.
  • Declined: run_owned / run_borrowed. run is called from generic code (feat(stack-encrypt): chained plan builder as the single front door for writes, queries and reads #1071), so the turbofish only appears in tests.

Checks at the new head: nextest for stack-encrypt and stack-encrypt-derive, 338 passed (includes trybuild). test:doc, doc, fmt and workspace clippy are clean. cargo mutants --in-diff: 13 mutants, all unviable. The PR body's Verification section is updated.

coderdan added a commit that referenced this pull request Oct 5, 2026
…intext

Review of #1068 asked whether Borrowed and Owned should derive OpaqueDebug because they stand for a plaintext. They are uninhabited marker enums: no value of either type exists, so their Debug impl can print nothing. The doc now says so, and names where the plaintext actually lives (the description's closure, which has no Debug, and the Pending output).
Vitamin C's Encrypt, PrfValue and CllwOreEncrypt consume the value they
are given, and every target description was handed its plaintext by
reference. So each operation cloned the plaintext, and ciphertext(),
equality(), ore(), ope() and their EncryptFrom impls all required
S: Clone. A plaintext that is deliberately not Clone, so its one copy is
moved and wiped (the zeroizing FfiValue the WASI guest decodes), could not
enter the target layer at all, which blocks the plan builder (#1056).

An Encryption now carries a mode, M, saying how it is handed the
plaintext. Borrowed (the default) hands it &S, as before: an operation
that consumes clones once, so EncryptFrom declarations, records and the
derive are unchanged. Owned hands it S, through the new
KeysetCipher::run: a single operation consumes it with no copy and needs
no Clone. zip is the one place an owned plaintext needs Clone; the first
side gets a clone and the last takes the value, so n operations make
n - 1 copies instead of n. A match term only reads its text and never
copies in either mode. encrypt_native and the Term impls now take the
plaintext by value, so the clone happens once, where a borrow becomes a
value.

Refs #1056

BREAKING CHANGE: Encryption gains a last type parameter, M: SourceMode
(default Borrowed), and its struct declares S: 's. The five constructors
ciphertext, equality, matching, ore and ope gain a source-mode type
parameter M, so a turbofish that named their generics must name it too
(in matching it comes before O), and a call whose result type does not
fix the mode must name one. zip now asks M: ShareSource, which Borrowed
always meets. Code that names Encryption<'s, S, T, K, Ctx> without a mode
keeps the borrowed behaviour.
Owned mode exists so a zeroizing plaintext is moved once and wiped. A new
test counts drops and asserts that ciphertext, equality and matching let
the value go while the description runs, before run returns its Pending,
so a later change that kept it alive across the ZeroKMS round trip fails.

The compile-pass file now also runs ore and ope over an S bounded only
by CllwOreEncrypt / CllwOpeEncrypt, which do not imply Clone, so Clone
returning to either constructor fails the ui suite. Checked by adding it
back to ore: trybuild reports `S: Clone` is not satisfied.

zip's rustdoc now says that in Owned mode a match term next to a
consuming operation still needs S: Clone; a combinator that avoids the
copy is left until a non-Clone source with a match index arrives.
@coderdan
coderdan force-pushed the fix/stack-encrypt-target-owned-plaintext branch from 639858b to 889bef9 Compare October 5, 2026 16:35
@coderdan
coderdan merged commit 327b4db into main Oct 5, 2026
47 checks passed
@coderdan
coderdan deleted the fix/stack-encrypt-target-owned-plaintext branch October 5, 2026 17:22
coderdan added a commit that referenced this pull request Oct 5, 2026
…intext

Review of #1068 asked whether Borrowed and Owned should derive OpaqueDebug because they stand for a plaintext. They are uninhabited marker enums: no value of either type exists, so their Debug impl can print nothing. The doc now says so, and names where the plaintext actually lives (the description's closure, which has no Debug, and the Pending output).
auxesis pushed a commit that referenced this pull request Oct 6, 2026
Decrypt through a plan is open, because StackCipher::decrypt already
exists with another signature. A plan has two starts and takes its
context from exactly one of three sources. The typed verb encrypt_into
and the picker are Rust-only. The derive emits the plan only after its
grammar is narrowed and the plan's widened, so the two stay one grammar.
EQL types are assembled per language from standard outputs, with no
registry and no target name in the data grammar.

The lowering table now names what #1068, #1069 and #1071 shipped.
@auxesis auxesis added this to the Rust crates live in stack repo milestone Oct 7, 2026
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.

stack-encrypt: a plaintext that is deliberately not Clone cannot be encrypted through the target layer, and every operation copies the plaintext

3 participants