Skip to content

fix(stack-encrypt)!: report a ciphertext that opens as the wrong type as OpenedType, not Aead - #1256

Merged
coderdan merged 3 commits into
nextfrom
fix/decrypt-type-mismatch-not-aead
Oct 18, 2026
Merged

coderdan merged 3 commits into
nextfrom
fix/decrypt-type-mismatch-not-aead

Conversation

@coderdan

@coderdan coderdan commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stack Encrypt is CipherStash's Rust encryption engine, and the Go SDK runs it as a WebAssembly guest. When a program opened an encrypted value as the wrong type (an integer opened as text, say), Stack Encrypt reported Error::Aead. That is the error for a wrong key, a wrong context or a tampered ciphertext, and the Go guest turned it into STATUS_AUTH, which tells the host someone altered the data. Nobody had: the value was just sealed as another type. Now that case is a new error, Error::OpenedType, which names the type the reader expected. Real tampering is still Aead.

Changes

  • packages/stack-encrypt/src/cipher.rs:
    • New variant Error::OpenedType { expected }, with the code stack_encrypt::opened_type, help text and an expected payload.
    • open_keyed maps vitaminc's DecryptError::Sink (every leaf authenticated, but the target refused the value) to it. A cipher failure, Auth and Malformed stay Aead.
    • expected is the vitaminc kind the target wanted when the refusal names one, and otherwise the target's Rust type name. It is never the refusal's own text, which can contain a map key.
  • languages/golang/encrypt/guest/src/status.rs: OpenedType maps to STATUS_ENCODING, the status a typed record field that opens to another kind already gets.
  • packages/stack-encrypt/src/codes.rs: the new variant is added to the every-variant code and payload tests.
  • packages/stack-encrypt/CHANGELOG.md: a Breaking entry under Unreleased.

Verification

  • cargo nextest run -p stack-encrypt --all-features: 671 passed. One existing test expected Aead for a uint32 leaf opened as String; it now expects OpenedType { expected: "string" }. Every other Aead expectation in the suite is a wrong-key, wrong-context or tamper case, and still passes unchanged.
  • New unit test only_a_sink_refusal_is_told_apart_from_aead covers every arm of the mapping. New test a_leaf_or_a_container_of_another_shape_is_opened_type goes through a real open: a u32 leaf opened as String, and a Vec<u32> opened as u32, are both OpenedType.
  • The guest's cargo test --locked --features eql, including the new status assertion: pass.
  • cargo clippy -p stack-encrypt --all-features --all-targets -D warnings, mise run doc and cargo fmt --check: clean.
  • These ran on the commit before its rebase onto next. next's tree is identical to that base, so the code tested is the code here.

Related

Closes #1118

Review notes

  • Why it's safe to tell the two apart: vitaminc's sink runs only once every leaf has authenticated, so OpenedType can't come from an altered leaf, and saying "wrong type" reveals nothing.
  • Containers are OpenedType too, by design. A container's membership and order are deliberately not authenticated. A stored sequence is a set of rows and a stored map a row's columns, which a database returns in any order and a query selects in part. A refusal of a container's shape is a shape mismatch, not tampering. Codex flagged this as P1; that finding doesn't apply. The rationale is written on open_keyed and Error::OpenedType, and a test pins it.
  • Not changed: the seal side, PendingStackCipherText::plan, still maps a value vitaminc refuses to encrypt (a repeated key, say) to Aead. That isn't what this issue is about, so I left it for a separate change.
  • Also already done: the rest of stack-encrypt: a Rust String field reads a data-plan leaf as "\nalice" — the Rust chain and a data plan seal different leaf bytes #1118, the different leaf bytes between the Rust chain and a data plan, was fixed by vitaminc 0.6's tagged leaves. mod given_the_leaf_encoding in dynamic/record.rs pins that.

… as OpenedType, not Aead

A typed decrypt collapsed every failure vitaminc reports into Error::Aead,
the error for a wrong key, a wrong context or a tampered ciphertext. That
included a leaf that authenticated and only held another type than the
target reads: an integer opened as String, say. The Go guest then
reported a value sealed as another type as STATUS_AUTH, i.e. tampering.

vitaminc's DecryptError already tells the sink's refusal apart from the
cipher's, so open_keyed now maps DecryptError::Sink to the new
Error::OpenedType, naming only the type the target reads (never the map
key a DecodeError can carry). Every other failure stays Aead. Telling a
wrong-type reader apart reveals nothing, because the sink runs only after
every leaf has authenticated. The guest maps OpenedType to
STATUS_ENCODING, as it already maps a typed record field that opens to
another kind.

Closes #1118.
@changeset-bot

changeset-bot Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 07fa826

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

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3219a9cc-a11d-46bf-8f37-bdce228aa873

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • 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
3 0 2 0

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

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

chatgpt-codex-connector Bot commented Oct 18, 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-18T06:26:01.476830Z bd0fe7b Draft marked ready
🔒 Security Review ✅ Completed 2026-10-18T06:28:38.806350Z bd0fe7b 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.

@cipherstash-agent cipherstash-agent 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.

Review attestation: automated /code-review medium followed by /cross-check-review-findings on head bd0fe7b. Findings are listed below even when there are none.

Critical / blocking: none.

Findings: none survived cross-check.

Checked against the checked-out tree:

  • open_keyed → opened_error (packages/stack-encrypt/src/cipher.rs): only DecryptError::Sink becomes OpenedType. Cipher, Auth and Malformed stay Aead, so wrong-key, wrong-context and tamper cases are not weakened. A wrong key or context fails authentication before the sink runs, so this adds no oracle.
  • expected is the vitaminc kind or type_name::<T>(). It never carries a map key, and the unit test covers MissingField, unknown_key and duplicate_key.
  • The remaining Error::Aead expectations I inspected are wrong-context cases and still hold: tests/derive.rs:436, :767, and tests/plan_grammar.rs:741, :896. None is a type mismatch.
  • Error is #[non_exhaustive], so the new variant is not a source break for downstream matches. The Go guest status_for_error falls through to STATUS_INTERNAL for unlisted variants, and the PR maps OpenedType explicitly to STATUS_ENCODING.
  • Code and payload tests (codes.rs) and the CHANGELOG Breaking entry are present.

Non-blocking note: the seal-side PendingStackCipherText::plan still maps refusals to Aead. The PR description already calls this out as out of scope.

Source: automated review attestation (Claude Code), static read of the diff and tree. I did not run tests.


Generated by Claude Code

@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: bd0fe7b2b8

ℹ️ 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/cipher.rs
…m Aead

A container's leaves authenticate one by one, but its membership does
not: a map entry's AAD binds only its key, and a sequence element's binds
no index. So an entry removed or repeated, or elements reordered or
dropped, still authenticate, and the target's refusal of the result
(MissingField, DuplicateKey, or Expected on a reshaped container) can be
tampering. open_keyed now reports OpenedType only when the tree is one
leaf and the target refused its kind. Every other failure, every refusal
of a container's value included, stays Aead.

Found by review on #1256.
…part from Aead", and say why

This reverts commit 8e904f8. A container's membership and order are
deliberately not authenticated: a stored sequence is a set of rows and a
stored map a row's columns, and a database returns rows in any order and
a query selects some columns, not all. Authenticating position or presence
would break exactly those reads. So a target refusing a container's shape
is a shape mismatch, OpenedType, not tampering.

The rationale now sits on open_keyed, on Error::OpenedType and in the
CHANGELOG, and a test opens a Vec<u32> as u32 and expects OpenedType, so a
later review that reads the missing membership check as a defect meets
the reason first.
@coderdan
coderdan removed the request for review from a team October 18, 2026 06:53
@coderdan
coderdan merged commit 777f297 into next Oct 18, 2026
47 checks passed
@coderdan
coderdan deleted the fix/decrypt-type-mismatch-not-aead branch October 18, 2026 07:04
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.

2 participants