Skip to content

stashgen refuses TextMatch, TextSearch, TextOrd and TextOrdOpe — the engine derives their terms, but no EQL type is built from them #1131

Description

@coderdan

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:

  1. A Go developer declares a Name string field with the EQL type TextMatch or TextSearch, to search names with LIKE-style matching.
  2. stashgen refuses it with: "the engine derives match and OPE terms, but no EQL type is built from them yet".
  3. 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

  1. Check the engine's match term against what EQL's bloom SQL reads, and the engine's OPE term against what EQL's ope SQL 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.
  2. If the SQL can compare them, add ("text", "match"), ("text", "ord"), ("text", "ord_ope") and ("text", "search") to ENCRYPTION_DOMAINS for the v4 structs and regenerate. If it can't, decide which side changes before adding them.
  3. Prove each type end to end the way TextEq was, against an eql_v4 install: 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

Activity

  1. added
    enhancementNew feature or request
    rustPull requests that update Rust code
    on Oct 7, 2026
  2. self-assigned this
    on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

SDKenhancementNew feature or requestrustPull requests that update Rust code

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions