Skip to content

chore(stack-encrypt)!: move to vitaminc 0.8, clww-ore 0.6 and ore-rs 0.10 - #1327

Draft
coderdan wants to merge 2 commits into
nextfrom
chore/vitaminc-0-8-ore-0-10
Draft

coderdan wants to merge 2 commits into
nextfrom
chore/vitaminc-0-8-ore-0-10

Conversation

@coderdan

@coderdan coderdan commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stack Encrypt is the Rust engine that encrypts values for the Go SDK and the EQL types. It now builds on vitaminc 0.8 and on the new releases of the two order-preserving encryption crates: clww-ore 0.6 (the old cllw-ore, renamed) and ore-rs 0.10. vitaminc is the library that seals each value. Its 0.8 release pads a missing value of a fixed-width type (an integer, a float, a date, a timestamp, a decimal) to the same length as a present one. That lets #1298 store an optional field without showing which rows are empty. This PR only does the upgrade. #1298 builds on it.

Draft until ore.rs is released. clww-ore and ore-rs come from git, pinned to the head of cipherstash/ore.rs#132 (stacked on #131 and #130, tracked in cipherstash/ore.rs#129). When clww-ore 0.6.0 and ore-rs 0.10.0 are on crates.io, the two lines in the root Cargo.toml become version pins. A git dependency also stops stack-encrypt publishing, so this cannot merge before then.

Every stored order term changes. An order term is the index value that lets the database compare or sort encrypted values. Terms of the ore, ope and ore_block indexes are derived differently now:

  • clww-ore fixed its keystream and renamed its schemes, and every term's salt includes the scheme name.
  • ore-rs now has one BlockOre scheme, which derives its keys under a new label.

So a query no longer finds an order term written before this change. Sealed values (ciphertexts) and equality terms do not change. A stored row still opens, and its order terms have to be derived again. Nothing has been released with the old terms on next.

Changes

Dependencies

  • vitaminc goes from 0.6.1 to 0.8.0 everywhere: the root workspace, the EQL workspace, both Go guests and the fuzz crate.
  • cllw-ore =0.5.2-rc.1 becomes clww-ore, and ore-rs 0.9.0 becomes ore-rs on vitaminc 0.8. Both come from git at the ore.rs#132 head, with default features off.
  • Dependabot ignores clww-ore and ore-rs in both cargo entries, and its comments are updated.

Order terms (stack-encrypt/src/sem)

  • ORE and OPE terms are encrypted through ClwwOre and ClwwOpe. Block ORE goes through ore-rs's single BlockOre, which picks its 6-bit or chained engine by the target's width. That replaces our own ore_block_is_bit6.
  • Each opens a vitaminc-ore OrderSpace for the type's target (the old OrderColumn is deprecated) and calls encrypt_checked.
  • sem::OreCllw8VariableV1 and sem::OpeCllw8VariableV1 are renamed OreClww8VariableV1 and OpeClww8VariableV1, to match clww-ore.
  • The block ORE text limit is still a 1023-byte fold. vitaminc-ore now puts the marker byte in front of the text itself, and ore-rs's 1024-byte ceiling counts it, so our own check counts it too.

Typed nulls (vitaminc 0.8's new Sink::typed_null)

  • Every Sink in stack-encrypt, stack-guest-abi, the guest tests and eql-bindings' tests implements typed_null.
  • Sinks that forward a value pass it on. The value-model writer keeps a typed null of a date, decimal, timestamp or plain number, and refuses a uuid's, as it refuses a uuid.
  • A frame carries a typed null as a plain null. The padding only exists in the sealed value, as vitaminc specifies.

Equality on a null

  • vitaminc-normalize 0.8 now gives a null its own equality term. Stack Encrypt drops it on both the typed and dynamic paths, so an absent value still leaves a SQL NULL in its equality column.
  • This PR keeps the old behaviour so that it changes nothing beyond the upgrade. feat(golang, stack-encrypt): optional sealed fields, stored as a sealed null or SQL NULL #1328, stacked on this one, keeps the null term instead: a null's equality term is derived, and a sealed null can sit beside an equality index.

Fixtures, pins and docs

  • These were regenerated: stored_rows_v1.json (with STACK_ENCRYPT_REFREEZE_ORDER_TERMS=1), query_operators.json, record_lowering.json and value_kinds.json; EQL's v4_ordered_queries.json and v4_ordered_rows.json; and the byte pins in term_bytes.rs, frozen_bytes.rs and ore_block.rs.
  • stored_rows itself checks that only order terms moved.
  • The CHANGELOG entries for stack-encrypt and the Go module are added, AGENTS.md's note on where the ORE crates come from is updated, and so is one trybuild snapshot.

Verification

  • cargo nextest run --workspace --all-features (root): 1255 passed, 1 skipped. cargo clippy --workspace --all-features --all-targets -D warnings, mise run test:doc and RUSTDOCFLAGS=-D warnings cargo doc are clean. cargo metadata --locked passes in every Cargo workspace touched.
  • New tests:
    • roundtrip::a_fixed_width_none_is_as_long_as_a_some: a sealed None::<i64>, None::<u32> and None::<f64> is as long as a sealed Some, and opens as None.
    • Three value_frame tests drive a typed null through the frame writer, the host-frame check, opening and the record-row split. A uuid's typed null is refused.
  • cargo mutants --in-diff for stack-auth and stack-encrypt, the CI gate: 35 mutants, 20 caught, 14 unviable, 1 missed. The survivor replaces KindOf::typed_null's Ok(None) with Ok(Default::default()), which is the same value: a null has no kind. It is excluded in .cargo/mutants.toml next to the matching passthrough exclusion.
  • EQL workspace: cargo test -p eql-bindings --all-features and -p eql-encryption-tests pass, and so does mise run test:encryption:postgres against PostgreSQL 15. That means EQL v4's SQL comparators order the new block ORE and CLWW terms.
  • Go: all four guests (normal and deterministic, with and without EQL) were rebuilt, and mise run go:test passes. The guest crate's own cargo test passes.
  • pnpm run test:scripts (78 files) passes.

Related

Refs #1298. Unblocks it: the Rust and then Go work for optional sealed fields stacks on this branch.
Depends on cipherstash/ore.rs#132 (and #131, #130; tracking cipherstash/ore.rs#129). Uses cipherstash/vitaminc#497 and #499, released in vitaminc 0.8.0.

Review notes

https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3

…0.10

vitaminc 0.8 seals a fixed-width None as a typed null padded to the
length of a Some, which #1298 needs before an optional field can be
sealed without showing which rows are absent. Stack Encrypt could not take
it on its own: cllw-ore and ore-rs pinned vitaminc-ore 0.6. Their move to
vitaminc 0.8 (clww-ore, renamed, and ore-rs 0.10) is cipherstash/ore.rs#132,
stacked on #131 and #130, and not yet released, so both come from git at
that PR's head until they are on crates.io.

What moves:

- Order terms encrypt through clww-ore's ClwwOre/ClwwOpe and ore-rs's single
  BlockOre, opened as vitaminc-ore OrderSpaces. Every ORE, OPE and block ORE
  term changes (clww-ore's keystream fix and scheme names, BlockOre's key
  label). Wire formats, this crate's key domains, ciphertexts and equality
  terms do not; the fixtures and pins re-freeze their order terms only.
- Every Sink implements typed_null. Sinks that forward (the frame writers)
  pass it on; the value model holds a typed null of its own types and
  refuses a uuid's, as it refuses a uuid.
- A null keeps no equality term. vitaminc-normalize 0.8 derives one; it is
  dropped so absent values keep a SQL NULL until #1298 decides otherwise.
- vitaminc-ore 0.7 puts the text marker byte in front itself, so block ORE's
  1024-byte ceiling now counts it in the encoding stack-encrypt checks.

Refs #1298.

Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
…mutant

A typed null has no kind, as any null has none, so KindOf::typed_null's
Ok(None) is Ok(Default::default()). The one survivor of cargo mutants
--in-diff on the vitaminc 0.8 upgrade; folded into the passthrough
exclusion, which is equivalent for the same reason.

Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
@changeset-bot

changeset-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9ea8b7b

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 21, 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
20 0 14 0

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

coderdan added a commit that referenced this pull request Oct 21, 2026
vitaminc-normalize 0.8 derives an equality term for a null: one term per
context, shared by every null, typed or plain. #1327 dropped it to keep
the old behaviour; this keeps it (#1298).

- stack-encrypt: `EqualityTerm` of a `None`, and `IndexSpec::Equality`
  on a null, now derive that term. No other index has one.
- Data plans: `"null": "sealed"` takes a field whose only index is
  equality, on the field or on the EQL type it names. The null is sealed
  as long as a value and stores the null term beside it, so the absent
  rows are one equality group, as any repeated value is, and the column
  holds no SQL NULL. An `eq` query whose input is a null derives that
  term (`query_term`, `record::query`); a null query of any other field
  or operator is the new `Reason::NullQuery`.
- eql-bindings: a type whose only term is equality's (`DateEq`,
  `IntegerEq`, ...) and its query twin take an `Option` of the plaintext,
  as storage-only types do, and the by-name dispatch encrypts, opens and
  queries them through it.
- Go: stashgen defaults to `null=sealed` for a fixed-width field whose
  only index is equality, and generates `EqNull`, the `eq` query of nil,
  through the new `gensupport.Field.NullTerm`.

Verified in PostgreSQL: `=` with the query of a null finds exactly the
rows of a DateEq column whose value was a sealed null.

Refs #1298.

Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3

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