Repository navigation
feat(stack-encrypt)!: EQL types as plan field targets, from Go through the guest #1095
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7a9ef7c
2480dc3
ba6a32d
fae8568
7987a2f
e235774
0e94d76
9c8f440
e9993b6
31d2013
b3d22ca
3459f07
8e67d88
7005882
25d3cee
1e5d524
3795827
06b526f
f3a3c6f
a8798fe
165c390
afbccc0
e007c94
0f0e55d
a223e1f
687b36c
0049376
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@cipherstash/eql': minor | ||
| --- | ||
|
|
||
| **The `eql-bindings` crate resolves an EQL type named as a string to its own Stack Encrypt plan** (`stack-encrypt` feature). `eql_bindings::encryption::targets` carries a catalog-generated table of every EQL type a data plan may name as a field target — its name across languages, family and suffix, the plaintext `ValueKind` it takes, the indexes it carries, its query twin, and whether the engine can produce it today with the reason when not — and `encrypt` / `decrypt` / `query` entry points that dispatch on the name and run the type's derived `EncryptFrom` / `DecryptInto`, resolving to the EQL value's JSON bytes through the engine's `Pending` so a guest batches it with the rest of a plan. This is the EQL half of EQL types as plan field targets (cipherstash/stack#1062): a Go data plan names `TextEq` and the guest returns the finished EQL value instead of assembling one. `TextEq` is the only producible type; every other name is refused with the plan's reason. The SQL surface and the TypeScript package are unchanged. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,8 @@ Your program calls those functions, and it never builds or names a plan. | |
| type User struct { | ||
| _ struct{} `stash:"context=users"` | ||
| ID int64 `stash:"id,passthrough"` | ||
| Email string `stash:"email,encrypt,index=equality;match"` | ||
| Name string `stash:"name,encrypt"` | ||
| Email string `stash:"email,encrypt_into=TextEq"` | ||
| Name string `stash:"name,encrypt_into=TextEq"` | ||
| } | ||
| ``` | ||
|
|
||
|
|
@@ -40,12 +40,11 @@ Your program calls those functions, and it never builds or names a plan. | |
| ```go | ||
| encrypted, err := users.Encrypt(ctx, cipher, people) | ||
| opened, err := users.Decrypt(ctx, cipher, encrypted) | ||
| term, err := users.Fields.Email.Equality(ctx, cipher, "bob@example.com") | ||
| query, err := users.Fields.Email.Query(ctx, cipher, "bob@example.com") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: one equality search has two method names, Impact: A developer who moves a field from Evidence: Step 5 of Fix: Name the EQL method after the search it makes, for example Found by 1 model: claude |
||
| ``` | ||
|
|
||
| 6. Store the encrypted type. | ||
| Each sealed field is one or more byte columns: `Email.Ciphertext`, `Email.Equality`, `Email.Match`. | ||
| `encrypt.Ciphertext` and each term type implement `driver.Valuer` and `sql.Scanner`, so a database library binds and scans each one as bytes; map each one to its own column. | ||
| Each `encrypt_into` field is one EQL column: `eql.TextEq` implements `driver.Valuer` and `sql.Scanner`, so a database library binds and scans it as the JSON a `public.eql_v3_text_eq` column holds. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: the docs do not say how to move a field from separate columns to one EQL column. Impact: Before this PR, this step stored Evidence: This PR replaces the separate-column text of this step with the EQL text. Fix: Add a short section that says how to move stored rows to the new column. For example: decrypt each row with the old generated code, encrypt it with the new code, and write the new column. Found by 1 model: claude |
||
|
|
||
| 7. Run the generator again after each change to the struct or to a tag. | ||
| A change to the fields of the struct stops the build until you do. | ||
|
|
@@ -140,10 +139,14 @@ It ignores its own output file when it loads the package, so a stale file does n | |
| The same input always gives the same file: fields keep their declared order, and the file carries no version and no time. | ||
|
|
||
| `stashgen` checks each declaration with the engine, and holds no copy of the engine's rules: it runs the WASI guest the SDK embeds and asks it, one field at a time, so the error names the field. | ||
| It asks the engine for the EQL types it holds: each name, its plaintext type, its indexes and its query form. | ||
| This build of the engine produces no EQL type, so `encrypt_into` is refused with "EQL types are not available yet"; the next release adds `TextEq` and `encrypt/eql`. | ||
| It asks the engine for the EQL types it holds: each name, its plaintext type, its indexes, its query form, and whether the engine produces it today. | ||
| The command links the build of the engine that holds the EQL types (`encrypt/eql`), so it can answer for every type; a generated file imports `encrypt/eql` only when it names one. | ||
| The engine produces `TextEq` today; `encrypt_into` with any other type is refused with the type's name. | ||
| Separate columns work today for four indexes: `equality`, `match`, `ore` and `ope`. | ||
|
|
||
| A field with `encrypt_into` is stored under its table and column, which is what an EQL value records in its `i`. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix in a follow-up: this line names the column that Impact: A developer can read Evidence: A reviewer swapped the Fix: Add this sentence after this line: `i` does not name the row, so a value copied to another row of the same column decrypts there with no error. See "The cipher" in `encrypt/README.md`.Found by 1 model: claude |
||
| A cipher extended with a tenant part (`cipher.Extend(...)`) has no column for the extended label, so it refuses a struct with an `encrypt_into` field; use `index=` columns for a tenant-extended struct until that rule is settled. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change before merge: tenants that share a keyset share each equality term, and this line does not say so. Impact: This line tells a developer with tenant-extended data to use Evidence: A reviewer ran this at Fix: Add this note after this line: > **Take care**
>
> Without `Extend`, every tenant on one keyset derives the same equality term for the same value. A person who can read the database can then see that two tenants hold the same value. To keep tenants apart with `encrypt_into`, give each tenant its own keyset: `client.Keyset(encrypt.KeysetName(tenant))`.Found by 1 model: claude |
||
|
|
||
| ## When stashgen stops | ||
|
|
||
| `stashgen` stops with an error, and writes no file, for each of these. | ||
|
|
@@ -216,5 +219,5 @@ A protobuf message with a `oneof` cannot be generated from a policy: protoc-gen- | |
|
|
||
| ## Status | ||
|
|
||
| The command runs the WASI guest the SDK embeds, so it needs the guest built: `mise run wasm:guest:build`. | ||
| The command runs the WASI guest the SDK embeds, so it needs the guests built: `mise run wasm:guest:build wasm:guest:build:eql`. | ||
| The library, `github.com/cipherstash/stack/languages/golang/stashgen`, takes any `Engine`; `stashgen.Generate` takes one with `WithEngine`, and `stashgen/enginetest` has a static one for tests. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change before merge: this example gives
Namean equality index, so a developer who copies it reveals which rows share a name.Impact: Before this PR, this line was
stash:"name,encrypt", with no index. The paper found that developers copy the first example as it is. WithTextEq, each row stores an equality term forName, and rows with the same name have the same term. The example never searches by name, so the index adds exposure and no feature.Evidence: The
TextEqdoc says only "the text family with the eq index" (encrypt/eql/eql_gen.go:1361). The engine's docs say "equal values are visibly equal" (packages/stack-encrypt/src/sem/mod.rs:71). No text at the point of choice says whatTextEqreveals. One struct can hold both kinds of field:testusers.Contacthas anencrypt_into=TextEqfield and anencryptfield.Fix: Keep
Namewithout an index:Then add one line about
TextEqbelow the tag table:Found by 1 model: claude