Repository navigation
stashgen refuses TextMatch, TextSearch, TextOrd and TextOrdOpe — the engine derives their terms, but no EQL type is built from them #1131
Description
Background
EQL (Encrypt Query Language) is the SQL library we install into a customer's PostgreSQL database. It stores encrypted values and lets the database search them without decrypting. Each EQL type (for example TextEq, IntegerOrd, TextMatch) fixes what a column stores: the ciphertext, plus the search terms that type needs. A search term is a small encrypted value the database can compare, such as an HMAC (a keyed hash, used for equality) or an ORE term (order-revealing encryption, used for < and >).
Stack Encrypt (packages/stack-encrypt) is our Rust encryption engine. Since #1095, a plan field can name an EQL type as its target: the engine then writes the exact value that EQL type stores. The Go SDK uses this through its WASI guest (a WebAssembly build of the engine) and the stashgen code generator.
A target type is producible when the engine can write it. The engine runs the type's own Rust plan, which is the EncryptFrom derive on the type's generated struct. eql-codegen adds that derive only to the types listed in ENCRYPTION_DOMAINS (packages/eql/crates/eql-codegen/src/bindings.rs:316), and today that list is only ("text", "eq"). So of the 51 EQL types, only TextEq is producible. The generated table packages/eql/crates/eql-bindings/src/v3/targets.rs records a reason for each of the other 50, and stashgen refuses them with that reason. #1062 scoped this on purpose to TextEq only. This issue tracks one group of the remaining types.
Only EQL v4 types derive Stack Encrypt's traits. ADR-0002 (#1139) installs the EQL SQL twice: eql_v3 for payloads written by cipherstash-client (the engine behind the TypeScript SDK), and eql_v4 for payloads written by Stack Encrypt. Stack Encrypt's output is not byte-for-byte compatible with cipherstash-client's, so deriving EncryptFrom or DecryptInto on an eql_v3 struct would claim a compatibility that doesn't exist. Every type in this issue becomes producible as its eql_v4_* struct, and no eql_v3_* struct derives either trait. Today's v3 TextEq and TextEqQuery derives move to v4 in #1142. File paths above name the current v3 modules; the v4 ones are generated in #1142.
Problem
Four text types cannot be written by the engine, although the engine already derives the terms they need:
| EQL type | Terms EQL stores |
|---|---|
TextMatch |
bloom filter (fuzzy text match) |
TextOrd |
HMAC + OPE |
TextOrdOpe |
HMAC + OPE |
TextSearch |
HMAC + OPE + bloom filter |
(OPE is order-preserving encryption, used for sorting and range queries. A bloom filter is a fixed-size bit set used for fuzzy text matching.) The term lists come from the EQL catalog, packages/eql/crates/eql-domains/src/lib.rs (the text family).
What happens:
- A Go developer declares a
Name stringfield with the EQL typeTextMatchorTextSearch, to search names withLIKE-style matching. stashgenrefuses it with: "the engine derives match and OPE terms, but no EQL type is built from them yet".- The developer can only use
TextEq, which supports equality and nothing else.
The engine has the operations: Stack Encrypt's sem module derives bloom-filter match terms and OPE terms (packages/stack-encrypt/src/sem/mod.rs). What is missing is the EQL side: these four generated structs do not carry the EncryptFrom derive, because ENCRYPTION_DOMAINS lists only text equality.
Not verified yet: that EQL's SQL can compare the engine's bloom filter and OPE terms: the stored shape (for example the bloom filter as the array EQL's containment operators read) and the comparison semantics (OPE terms ordered by EQL's byte comparison). The terms do not need to match the bytes cipherstash-client writes. These types are produced only into eql_v4_* columns, which cipherstash-client never writes. If the SQL can't compare them, a type built from them would write rows that EQL queries do not find, silently.
Proposal
- Check the engine's match term against what EQL's
bloomSQL reads, and the engine's OPE term against what EQL'sopeSQL compares: shape, byte layout and comparison. Write down any difference. Matching cipherstash-client's parameters (filter size, hash count, n-gram settings) is not required. - If the SQL can compare them, add
("text", "match"),("text", "ord"),("text", "ord_ope")and("text", "search")toENCRYPTION_DOMAINSfor the v4 structs and regenerate. If it can't, decide which side changes before adding them. - Prove each type end to end the way
TextEqwas, against aneql_v4install: write rows from Go through the guest, query them with EQL's operators in PostgreSQL, and check the right rows come back. Add a fixture that pins the term bytes.
A cheap first step: TextOrdOpe alone. It needs only HMAC (already working for TextEq) and OPE.
Relationship to other work
- Go and Rust plans cannot target EQL domains, so Go writes rows EQL cannot query — make domains plan field targets #1062: made EQL types plan targets, with
TextEqonly. - stashgen refuses TextOrdOre and TextSearchOre — EQL stores block ORE terms and the engine derives CLLW ORE #1132:
TextOrdOreandTextSearchOre, which need a different ORE scheme. - stashgen refuses every number, date and boolean EQL type — produce them in EQL v4 using ADR-0002's encodings #1130: the non-text families. stashgen refuses the storage-only Text type — it is held back until TextEq is proven end to end #1133: storage-only
Text. - stack-encrypt: every index is compiled into the core crate — move
Equality,Match,Ore,Opeinto their own crates #1063: moves each index into its own crate. It does not make any type producible.
Activity