Repository navigation
Description
Background
Stack Encrypt (packages/stack-encrypt) is the Rust engine that every CipherStash language SDK runs on. Each value it encrypts is sealed under a context. A context is a list of parts, such as a table name, column name, tenant ID, or row ID. The context goes into the data key and into the AAD (additional authenticated data). Encryption checks the AAD but does not hide it.
A value opens only under the same context. Search terms, the values PostgreSQL compares in a query, are also derived under a context.
The Stack Encrypt API plan, merged in pull request #1365, gives every part one of two kinds:
- A scope is a part a query can supply, such as a table, column, or tenant.
- A binding ties a value to one record, such as a row ID. A query does not know it.
Problem
Today every part is in both contexts. So a value bound to its row ID also has search terms derived under that row ID. A query for "every user named Ada" cannot produce those terms, so it finds nothing. A developer has to choose between binding a value to its row and searching the column.
Proposal
The plan decided the rule on 7 November 2026.
- Search terms derive under the query context. That is the scopes only.
- The key and the AAD derive under the stored context. That is every part: the scopes, then the bindings, in that fixed order.
- The kind of a part is not in any derivation. Only the values and their order are. The kind decides only which parts are in the query context. So a record with no binding stores exactly the bytes it stores today.
- Add the binding kind to the Rust declaration and to the dynamic grammar the Go SDK sends. A binding can be a field stored in the row.
- Change the words. The Rust chain's
.extend(..)is removed, and the context argument carries scopes and bindings instead. The Go methodExtendbecomesScope, withExtendkept as a deprecated alias until 0.1.0. Go addsBind. The context contract and the conformance README say "scope" where they say "extension". - Keep the frozen vectors. No stored byte names the kind, so they do not change.
Facts about how a row was written, such as who wrote it, are labels, not bindings. The key policy document covers labels. It also covers claims. ZeroKMS, our key service, resolves a claim from the caller's token. Both are separate work.
Sending scopes and bindings to ZeroKMS as key request items is also separate. The plan item "Context parts as key request items" covers it. This issue does not change how the data key is requested.
Acceptance
Each check runs in Rust and in Go:
- A record with a binding row ID encrypts.
- A column query finds that record.
- The record fails to open under another row ID.
- The stored bytes of a record with no binding are unchanged.
- A binding passed to a query is refused. Rust refuses it at compile time. The dynamic path refuses it before any key is fetched.
- Cross-language cases cover a match, a refusal, and a tampered binding.
- The new search-term form sits beside the rc.3 form. rc.3 rows stay searchable.
Relationship to other work
- Follows stack-encrypt conformance: specify and test context composition #1475, the current context baseline. Coordinate definitions with stack-encrypt refactor: complete configured definitions and typed query APIs #1486 and records with stack-encrypt refactor: migrate record derives and plans to owned declarations #1483.
- stack-encrypt never sends lock context or audit context to ZeroKMS — bind chosen field facts and send audit facts as unverified context #1067 asks Stack Encrypt to send key request items to ZeroKMS.
- stack-encrypt: a second
.extend()replaces the first, so Rust builds a different context than Go and a query silently misses #1120, where a second.extend()replaces the first, closes when the chain is removed in stack-encrypt refactor: remove transitional traits and redundant execution APIs #1488.
Plans: the Stack Encrypt API plan and the conformance roadmap.
Activity
coderdan commentedon Nov 9, 2026
The
contexts()view andassert_context!from #1568 do not yet cover scenarios 2 to 7 of the context scenarios, because they need thescope(..)andbind(..)attributes this issue adds. #1590 tracks that work; the pull request for this issue should close it, with #1486 covering the field-marker form.feat(stack-encrypt): split the context into scopes and bindings
fix(stack-encrypt): address the review of scopes and bindings
feat(stack-encrypt): split the context into scopes and bindings
fix(stack-encrypt): address the review of scopes and bindings
test(stack-encrypt): pin the context-view mutants and exclude the equ…
feat(stack-encrypt): split the context into scopes and bindings
fix(stack-encrypt): address the review of scopes and bindings
test(stack-encrypt): pin the context-view mutants and exclude the equ…
test(stack-encrypt): state the one-part list test through `Stated`
fix(stack-encrypt): open a bound JSON document with its root open, an…
feat(stack-encrypt): split the context into scopes and bindings
6 remaining items
fix(stack-encrypt): address the review of scopes and bindings
test(stack-encrypt): pin the context-view mutants and exclude the equ…
test(stack-encrypt): state the one-part list test through `Stated`
fix(stack-encrypt): open a bound JSON document with its root open, an…
docs(stack-encrypt): drop the ADR reference from the context module's…
feat(stack-encrypt): split the context into scopes and bindings
fix(stack-encrypt): address the review of scopes and bindings
test(stack-encrypt): pin the context-view mutants and exclude the equ…
test(stack-encrypt): state the one-part list test through `Stated`
fix(stack-encrypt): open a bound JSON document with its root open, an…
docs(stack-encrypt): drop the ADR reference from the context module's…
fix(stack-encrypt): refuse an empty or grouped bind list, and name th…
test(stack-encrypt): pin that a plan with an EQL field refuses a binding
fix(golang): return copies of byte parts from Scopes and Bindings