From b97ace2fa9f9a1a5d16a59e262d38767e75fb505 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 12 Oct 2026 21:07:51 +1100 Subject: [PATCH 1/4] feat(eql-bindings): produce the unordered EQL v4 number, boolean and text types A Go partner needs a pre-release of EQL v4 with Stack Encrypt before vitaminc 0.6 settles ordering. These are the v4 types that need no ordering term and whose value kind is already in vitaminc 0.5's value enum: Text, Integer, IntegerEq, Bigint, BigintEq, Boolean, Real and Double, beside TextEq. vitaminc 0.5 implements Encrypt for String but not for i32, i64, bool or the floats, and stack-encrypt deliberately has no combinator that re-encodes a plaintext. So eql_bindings::encryption::plaintext adds Int32, Int64, Bool, Float32 and Float64: each seals vitaminc's tagged leaf, the bytes FfiValue seals for the same value, opens only its own tag, and the integers derive the equality term of the integer inside. A value written through these types and one the Go SDK writes as a plain field open each other, and IntegerEq's term is a plain i32's. The derive reasons now have one source, encryption_gap: ordering terms wait for vitaminc-ore, float equality for vitaminc 0.6's canonical float, smallint/date/timestamp/numeric for their value kinds. Json stays unproducible: a JSON value seals as a tree of leaves and EQL's c holds one, and how a document becomes one leaf is a wire decision not made here. The encryption test crate checks each type through both entry points, against the value enum's leaf and the plain integer's term, and in PostgreSQL on an eql_v4 install: equality queries on IntegerEq and BigintEq return the right rows and every stored value opens. Refs #1130, #1133 --- .changeset/eql-v4-bindings.md | 2 +- languages/golang/encrypt/eql/eql_gen.go | 196 ++++---- languages/golang/encrypt/eql/eql_test.go | 17 +- languages/golang/encrypt/guest/Cargo.lock | 2 + packages/eql/Cargo.lock | 2 + packages/eql/crates/eql-bindings/Cargo.toml | 7 + packages/eql/crates/eql-bindings/README.md | 23 +- .../eql/crates/eql-bindings/src/encryption.rs | 1 + .../eql-bindings/src/encryption/plaintext.rs | 199 ++++++++ .../eql-bindings/src/encryption/targets.rs | 43 +- .../eql/crates/eql-bindings/src/v4/bigint.rs | 62 +++ .../eql/crates/eql-bindings/src/v4/boolean.rs | 21 + .../eql/crates/eql-bindings/src/v4/double.rs | 21 + .../eql/crates/eql-bindings/src/v4/integer.rs | 62 +++ .../eql/crates/eql-bindings/src/v4/real.rs | 21 + .../eql/crates/eql-bindings/src/v4/targets.rs | 229 +++++---- .../eql/crates/eql-bindings/src/v4/text.rs | 30 +- .../eql/crates/eql-codegen/src/bindings.rs | 130 +++-- packages/eql/crates/eql-codegen/src/go_eql.rs | 2 +- .../eql/crates/eql-codegen/src/targets.rs | 203 ++++---- packages/eql/mise.toml | 8 +- .../eql/tests/encryption/tests/targets.rs | 51 +- .../eql/tests/encryption/tests/v4_types.rs | 455 ++++++++++++++++++ 23 files changed, 1420 insertions(+), 367 deletions(-) create mode 100644 packages/eql/crates/eql-bindings/src/encryption/plaintext.rs create mode 100644 packages/eql/tests/encryption/tests/v4_types.rs diff --git a/.changeset/eql-v4-bindings.md b/.changeset/eql-v4-bindings.md index e29c9aa91..b29c1ee00 100644 --- a/.changeset/eql-v4-bindings.md +++ b/.changeset/eql-v4-bindings.md @@ -2,6 +2,6 @@ '@cipherstash/eql': minor --- -**The `eql-bindings` crate has an `eql_bindings::v4` module: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`. `SchemaVersion` is now generic over the version it admits: `v3::SchemaVersion` is `SchemaVersion<3>` and `v4::SchemaVersion` is `SchemaVersion<4>`, each refusing the other's payloads, and plain `SchemaVersion` still means v3. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table (now `eql_bindings::v4::targets`) names v4 domains. +**The `eql-bindings` crate has an `eql_bindings::v4` module: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`. `SchemaVersion` is now generic over the version it admits: `v3::SchemaVersion` is `SchemaVersion<3>` and `v4::SchemaVersion` is `SchemaVersion<4>`, each refusing the other's payloads, and plain `SchemaVersion` still means v3. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table (now `eql_bindings::v4::targets`) names v4 domains. Besides `TextEq`, Stack Encrypt now produces `Text`, `Integer`, `IntegerEq`, `Bigint`, `BigintEq`, `Boolean`, `Real` and `Double`, from the plaintext types in `eql_bindings::encryption::plaintext`; the Go SDK's `encrypt/eql` table marks them producible. Nothing a released version shipped has changed. The v3 types, the v3 SQL and the TypeScript package are as they were; the `stack-encrypt` derives on the v3 text types and the `v3::targets` module were added after the last release and are moved to v4 here, not removed from a release. diff --git a/languages/golang/encrypt/eql/eql_gen.go b/languages/golang/encrypt/eql/eql_gen.go index e1bc5688b..c1950eeed 100644 --- a/languages/golang/encrypt/eql/eql_gen.go +++ b/languages/golang/encrypt/eql/eql_gen.go @@ -9,61 +9,61 @@ import "database/sql/driver" // engine produces the type today; stashgen refuses encrypt_into for one // it does not, with the Reason. var Types = []Type{ - {Name: "Integer", GoName: "Integer", Family: "integer", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_integer", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "IntegerEq", GoName: "IntegerEq", Family: "integer", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_integer_eq", Indexes: []string{"eq"}, Query: "IntegerEqQuery", QuerySQLDomain: "eql_v4.query_integer_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "IntegerOrdOre", GoName: "IntegerOrdOre", Family: "integer", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_integer_ord_ore", Indexes: []string{"ore"}, Query: "IntegerOrdOreQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "IntegerOrd", GoName: "IntegerOrd", Family: "integer", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_integer_ord", Indexes: []string{"ope"}, Query: "IntegerOrdQuery", QuerySQLDomain: "eql_v4.query_integer_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "IntegerOrdOpe", GoName: "IntegerOrdOpe", Family: "integer", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_integer_ord_ope", Indexes: []string{"ope"}, Query: "IntegerOrdOpeQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Smallint", GoName: "Smallint", Family: "smallint", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_smallint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "SmallintEq", GoName: "SmallintEq", Family: "smallint", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_smallint_eq", Indexes: []string{"eq"}, Query: "SmallintEqQuery", QuerySQLDomain: "eql_v4.query_smallint_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "SmallintOrdOre", GoName: "SmallintOrdOre", Family: "smallint", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord_ore", Indexes: []string{"ore"}, Query: "SmallintOrdOreQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "SmallintOrd", GoName: "SmallintOrd", Family: "smallint", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord", Indexes: []string{"ope"}, Query: "SmallintOrdQuery", QuerySQLDomain: "eql_v4.query_smallint_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "SmallintOrdOpe", GoName: "SmallintOrdOpe", Family: "smallint", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord_ope", Indexes: []string{"ope"}, Query: "SmallintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Bigint", GoName: "Bigint", Family: "bigint", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_bigint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "BigintEq", GoName: "BigintEq", Family: "bigint", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_bigint_eq", Indexes: []string{"eq"}, Query: "BigintEqQuery", QuerySQLDomain: "eql_v4.query_bigint_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "BigintOrdOre", GoName: "BigintOrdOre", Family: "bigint", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_bigint_ord_ore", Indexes: []string{"ore"}, Query: "BigintOrdOreQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "BigintOrd", GoName: "BigintOrd", Family: "bigint", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_bigint_ord", Indexes: []string{"ope"}, Query: "BigintOrdQuery", QuerySQLDomain: "eql_v4.query_bigint_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "BigintOrdOpe", GoName: "BigintOrdOpe", Family: "bigint", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_bigint_ord_ope", Indexes: []string{"ope"}, Query: "BigintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Date", GoName: "Date", Family: "date", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_date", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DateEq", GoName: "DateEq", Family: "date", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_date_eq", Indexes: []string{"eq"}, Query: "DateEqQuery", QuerySQLDomain: "eql_v4.query_date_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DateOrdOre", GoName: "DateOrdOre", Family: "date", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_date_ord_ore", Indexes: []string{"ore"}, Query: "DateOrdOreQuery", QuerySQLDomain: "eql_v4.query_date_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DateOrd", GoName: "DateOrd", Family: "date", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_date_ord", Indexes: []string{"ope"}, Query: "DateOrdQuery", QuerySQLDomain: "eql_v4.query_date_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DateOrdOpe", GoName: "DateOrdOpe", Family: "date", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_date_ord_ope", Indexes: []string{"ope"}, Query: "DateOrdOpeQuery", QuerySQLDomain: "eql_v4.query_date_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Timestamp", GoName: "Timestamp", Family: "timestamp", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_timestamp", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "TimestampEq", GoName: "TimestampEq", Family: "timestamp", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_eq", Indexes: []string{"eq"}, Query: "TimestampEqQuery", QuerySQLDomain: "eql_v4.query_timestamp_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "TimestampOrdOre", GoName: "TimestampOrdOre", Family: "timestamp", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord_ore", Indexes: []string{"ore"}, Query: "TimestampOrdOreQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "TimestampOrd", GoName: "TimestampOrd", Family: "timestamp", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord", Indexes: []string{"ope"}, Query: "TimestampOrdQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "TimestampOrdOpe", GoName: "TimestampOrdOpe", Family: "timestamp", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord_ope", Indexes: []string{"ope"}, Query: "TimestampOrdOpeQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Numeric", GoName: "Numeric", Family: "numeric", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_numeric", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "NumericEq", GoName: "NumericEq", Family: "numeric", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_numeric_eq", Indexes: []string{"eq"}, Query: "NumericEqQuery", QuerySQLDomain: "eql_v4.query_numeric_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "NumericOrdOre", GoName: "NumericOrdOre", Family: "numeric", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord_ore", Indexes: []string{"ore"}, Query: "NumericOrdOreQuery", QuerySQLDomain: "eql_v4.query_numeric_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "NumericOrd", GoName: "NumericOrd", Family: "numeric", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord", Indexes: []string{"ope"}, Query: "NumericOrdQuery", QuerySQLDomain: "eql_v4.query_numeric_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "NumericOrdOpe", GoName: "NumericOrdOpe", Family: "numeric", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord_ope", Indexes: []string{"ope"}, Query: "NumericOrdOpeQuery", QuerySQLDomain: "eql_v4.query_numeric_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Text", GoName: "Text", Family: "text", Suffix: "", Plaintext: "string", SQLDomain: "public.eql_v4_text", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "the storage-only text domain follows once TextEq is proven end to end in PostgreSQL"}, + {Name: "Integer", GoName: "Integer", Family: "integer", Suffix: "", Plaintext: "int32", SQLDomain: "public.eql_v4_integer", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, + {Name: "IntegerEq", GoName: "IntegerEq", Family: "integer", Suffix: "Eq", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_eq", Indexes: []string{"eq"}, Query: "IntegerEqQuery", QuerySQLDomain: "eql_v4.query_integer_eq", Producible: true, Reason: ""}, + {Name: "IntegerOrdOre", GoName: "IntegerOrdOre", Family: "integer", Suffix: "OrdOre", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord_ore", Indexes: []string{"ore"}, Query: "IntegerOrdOreQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "IntegerOrd", GoName: "IntegerOrd", Family: "integer", Suffix: "Ord", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord", Indexes: []string{"ope"}, Query: "IntegerOrdQuery", QuerySQLDomain: "eql_v4.query_integer_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "IntegerOrdOpe", GoName: "IntegerOrdOpe", Family: "integer", Suffix: "OrdOpe", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord_ope", Indexes: []string{"ope"}, Query: "IntegerOrdOpeQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "Smallint", GoName: "Smallint", Family: "smallint", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_smallint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "SmallintEq", GoName: "SmallintEq", Family: "smallint", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_smallint_eq", Indexes: []string{"eq"}, Query: "SmallintEqQuery", QuerySQLDomain: "eql_v4.query_smallint_eq", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "SmallintOrdOre", GoName: "SmallintOrdOre", Family: "smallint", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord_ore", Indexes: []string{"ore"}, Query: "SmallintOrdOreQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ore", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "SmallintOrd", GoName: "SmallintOrd", Family: "smallint", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord", Indexes: []string{"ope"}, Query: "SmallintOrdQuery", QuerySQLDomain: "eql_v4.query_smallint_ord", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "SmallintOrdOpe", GoName: "SmallintOrdOpe", Family: "smallint", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord_ope", Indexes: []string{"ope"}, Query: "SmallintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ope", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "Bigint", GoName: "Bigint", Family: "bigint", Suffix: "", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, + {Name: "BigintEq", GoName: "BigintEq", Family: "bigint", Suffix: "Eq", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_eq", Indexes: []string{"eq"}, Query: "BigintEqQuery", QuerySQLDomain: "eql_v4.query_bigint_eq", Producible: true, Reason: ""}, + {Name: "BigintOrdOre", GoName: "BigintOrdOre", Family: "bigint", Suffix: "OrdOre", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord_ore", Indexes: []string{"ore"}, Query: "BigintOrdOreQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "BigintOrd", GoName: "BigintOrd", Family: "bigint", Suffix: "Ord", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord", Indexes: []string{"ope"}, Query: "BigintOrdQuery", QuerySQLDomain: "eql_v4.query_bigint_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "BigintOrdOpe", GoName: "BigintOrdOpe", Family: "bigint", Suffix: "OrdOpe", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord_ope", Indexes: []string{"ope"}, Query: "BigintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "Date", GoName: "Date", Family: "date", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_date", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "DateEq", GoName: "DateEq", Family: "date", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_date_eq", Indexes: []string{"eq"}, Query: "DateEqQuery", QuerySQLDomain: "eql_v4.query_date_eq", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "DateOrdOre", GoName: "DateOrdOre", Family: "date", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_date_ord_ore", Indexes: []string{"ore"}, Query: "DateOrdOreQuery", QuerySQLDomain: "eql_v4.query_date_ord_ore", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "DateOrd", GoName: "DateOrd", Family: "date", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_date_ord", Indexes: []string{"ope"}, Query: "DateOrdQuery", QuerySQLDomain: "eql_v4.query_date_ord", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "DateOrdOpe", GoName: "DateOrdOpe", Family: "date", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_date_ord_ope", Indexes: []string{"ope"}, Query: "DateOrdOpeQuery", QuerySQLDomain: "eql_v4.query_date_ord_ope", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "Timestamp", GoName: "Timestamp", Family: "timestamp", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_timestamp", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "TimestampEq", GoName: "TimestampEq", Family: "timestamp", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_eq", Indexes: []string{"eq"}, Query: "TimestampEqQuery", QuerySQLDomain: "eql_v4.query_timestamp_eq", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "TimestampOrdOre", GoName: "TimestampOrdOre", Family: "timestamp", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord_ore", Indexes: []string{"ore"}, Query: "TimestampOrdOreQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord_ore", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "TimestampOrd", GoName: "TimestampOrd", Family: "timestamp", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord", Indexes: []string{"ope"}, Query: "TimestampOrdQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "TimestampOrdOpe", GoName: "TimestampOrdOpe", Family: "timestamp", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord_ope", Indexes: []string{"ope"}, Query: "TimestampOrdOpeQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord_ope", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "Numeric", GoName: "Numeric", Family: "numeric", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_numeric", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "NumericEq", GoName: "NumericEq", Family: "numeric", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_numeric_eq", Indexes: []string{"eq"}, Query: "NumericEqQuery", QuerySQLDomain: "eql_v4.query_numeric_eq", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "NumericOrdOre", GoName: "NumericOrdOre", Family: "numeric", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord_ore", Indexes: []string{"ore"}, Query: "NumericOrdOreQuery", QuerySQLDomain: "eql_v4.query_numeric_ord_ore", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "NumericOrd", GoName: "NumericOrd", Family: "numeric", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord", Indexes: []string{"ope"}, Query: "NumericOrdQuery", QuerySQLDomain: "eql_v4.query_numeric_ord", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "NumericOrdOpe", GoName: "NumericOrdOpe", Family: "numeric", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord_ope", Indexes: []string{"ope"}, Query: "NumericOrdOpeQuery", QuerySQLDomain: "eql_v4.query_numeric_ord_ope", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "Text", GoName: "Text", Family: "text", Suffix: "", Plaintext: "string", SQLDomain: "public.eql_v4_text", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, {Name: "TextEq", GoName: "TextEq", Family: "text", Suffix: "Eq", Plaintext: "string", SQLDomain: "public.eql_v4_text_eq", Indexes: []string{"eq"}, Query: "TextEqQuery", QuerySQLDomain: "eql_v4.query_text_eq", Producible: true, Reason: ""}, - {Name: "TextMatch", GoName: "TextMatch", Family: "text", Suffix: "Match", Plaintext: "string", SQLDomain: "public.eql_v4_text_match", Indexes: []string{"match"}, Query: "TextMatchQuery", QuerySQLDomain: "eql_v4.query_text_match", Producible: false, Reason: "the engine derives match and OPE terms, but no EQL type is built from them yet"}, - {Name: "TextOrdOre", GoName: "TextOrdOre", Family: "text", Suffix: "OrdOre", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord_ore", Indexes: []string{"eq", "ore"}, Query: "TextOrdOreQuery", QuerySQLDomain: "eql_v4.query_text_ord_ore", Producible: false, Reason: "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms"}, - {Name: "TextOrd", GoName: "TextOrd", Family: "text", Suffix: "Ord", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord", Indexes: []string{"eq", "ope"}, Query: "TextOrdQuery", QuerySQLDomain: "eql_v4.query_text_ord", Producible: false, Reason: "the engine derives match and OPE terms, but no EQL type is built from them yet"}, - {Name: "TextOrdOpe", GoName: "TextOrdOpe", Family: "text", Suffix: "OrdOpe", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord_ope", Indexes: []string{"eq", "ope"}, Query: "TextOrdOpeQuery", QuerySQLDomain: "eql_v4.query_text_ord_ope", Producible: false, Reason: "the engine derives match and OPE terms, but no EQL type is built from them yet"}, - {Name: "TextSearchOre", GoName: "TextSearchOre", Family: "text", Suffix: "SearchOre", Plaintext: "string", SQLDomain: "public.eql_v4_text_search_ore", Indexes: []string{"eq", "ore", "match"}, Query: "TextSearchOreQuery", QuerySQLDomain: "eql_v4.query_text_search_ore", Producible: false, Reason: "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms"}, - {Name: "TextSearch", GoName: "TextSearch", Family: "text", Suffix: "Search", Plaintext: "string", SQLDomain: "public.eql_v4_text_search", Indexes: []string{"eq", "ope", "match"}, Query: "TextSearchQuery", QuerySQLDomain: "eql_v4.query_text_search", Producible: false, Reason: "the engine derives match and OPE terms, but no EQL type is built from them yet"}, - {Name: "Boolean", GoName: "Boolean", Family: "boolean", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_boolean", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Real", GoName: "Real", Family: "real", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_real", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "RealEq", GoName: "RealEq", Family: "real", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_real_eq", Indexes: []string{"eq"}, Query: "RealEqQuery", QuerySQLDomain: "eql_v4.query_real_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "RealOrdOre", GoName: "RealOrdOre", Family: "real", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_real_ord_ore", Indexes: []string{"ore"}, Query: "RealOrdOreQuery", QuerySQLDomain: "eql_v4.query_real_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "RealOrd", GoName: "RealOrd", Family: "real", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_real_ord", Indexes: []string{"ope"}, Query: "RealOrdQuery", QuerySQLDomain: "eql_v4.query_real_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "RealOrdOpe", GoName: "RealOrdOpe", Family: "real", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_real_ord_ope", Indexes: []string{"ope"}, Query: "RealOrdOpeQuery", QuerySQLDomain: "eql_v4.query_real_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Double", GoName: "Double", Family: "double", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_double", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DoubleEq", GoName: "DoubleEq", Family: "double", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_double_eq", Indexes: []string{"eq"}, Query: "DoubleEqQuery", QuerySQLDomain: "eql_v4.query_double_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DoubleOrdOre", GoName: "DoubleOrdOre", Family: "double", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_double_ord_ore", Indexes: []string{"ore"}, Query: "DoubleOrdOreQuery", QuerySQLDomain: "eql_v4.query_double_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DoubleOrd", GoName: "DoubleOrd", Family: "double", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_double_ord", Indexes: []string{"ope"}, Query: "DoubleOrdQuery", QuerySQLDomain: "eql_v4.query_double_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DoubleOrdOpe", GoName: "DoubleOrdOpe", Family: "double", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_double_ord_ope", Indexes: []string{"ope"}, Query: "DoubleOrdOpeQuery", QuerySQLDomain: "eql_v4.query_double_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, + {Name: "TextMatch", GoName: "TextMatch", Family: "text", Suffix: "Match", Plaintext: "string", SQLDomain: "public.eql_v4_text_match", Indexes: []string{"match"}, Query: "TextMatchQuery", QuerySQLDomain: "eql_v4.query_text_match", Producible: false, Reason: "the engine derives match terms, but no EQL type is built from them yet"}, + {Name: "TextOrdOre", GoName: "TextOrdOre", Family: "text", Suffix: "OrdOre", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord_ore", Indexes: []string{"eq", "ore"}, Query: "TextOrdOreQuery", QuerySQLDomain: "eql_v4.query_text_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "TextOrd", GoName: "TextOrd", Family: "text", Suffix: "Ord", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord", Indexes: []string{"eq", "ope"}, Query: "TextOrdQuery", QuerySQLDomain: "eql_v4.query_text_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "TextOrdOpe", GoName: "TextOrdOpe", Family: "text", Suffix: "OrdOpe", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord_ope", Indexes: []string{"eq", "ope"}, Query: "TextOrdOpeQuery", QuerySQLDomain: "eql_v4.query_text_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "TextSearchOre", GoName: "TextSearchOre", Family: "text", Suffix: "SearchOre", Plaintext: "string", SQLDomain: "public.eql_v4_text_search_ore", Indexes: []string{"eq", "ore", "match"}, Query: "TextSearchOreQuery", QuerySQLDomain: "eql_v4.query_text_search_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "TextSearch", GoName: "TextSearch", Family: "text", Suffix: "Search", Plaintext: "string", SQLDomain: "public.eql_v4_text_search", Indexes: []string{"eq", "ope", "match"}, Query: "TextSearchQuery", QuerySQLDomain: "eql_v4.query_text_search", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "Boolean", GoName: "Boolean", Family: "boolean", Suffix: "", Plaintext: "bool", SQLDomain: "public.eql_v4_boolean", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, + {Name: "Real", GoName: "Real", Family: "real", Suffix: "", Plaintext: "float32", SQLDomain: "public.eql_v4_real", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, + {Name: "RealEq", GoName: "RealEq", Family: "real", Suffix: "Eq", Plaintext: "float32", SQLDomain: "public.eql_v4_real_eq", Indexes: []string{"eq"}, Query: "RealEqQuery", QuerySQLDomain: "eql_v4.query_real_eq", Producible: false, Reason: "vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002)"}, + {Name: "RealOrdOre", GoName: "RealOrdOre", Family: "real", Suffix: "OrdOre", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord_ore", Indexes: []string{"ore"}, Query: "RealOrdOreQuery", QuerySQLDomain: "eql_v4.query_real_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "RealOrd", GoName: "RealOrd", Family: "real", Suffix: "Ord", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord", Indexes: []string{"ope"}, Query: "RealOrdQuery", QuerySQLDomain: "eql_v4.query_real_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "RealOrdOpe", GoName: "RealOrdOpe", Family: "real", Suffix: "OrdOpe", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord_ope", Indexes: []string{"ope"}, Query: "RealOrdOpeQuery", QuerySQLDomain: "eql_v4.query_real_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "Double", GoName: "Double", Family: "double", Suffix: "", Plaintext: "float64", SQLDomain: "public.eql_v4_double", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, + {Name: "DoubleEq", GoName: "DoubleEq", Family: "double", Suffix: "Eq", Plaintext: "float64", SQLDomain: "public.eql_v4_double_eq", Indexes: []string{"eq"}, Query: "DoubleEqQuery", QuerySQLDomain: "eql_v4.query_double_eq", Producible: false, Reason: "vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002)"}, + {Name: "DoubleOrdOre", GoName: "DoubleOrdOre", Family: "double", Suffix: "OrdOre", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord_ore", Indexes: []string{"ore"}, Query: "DoubleOrdOreQuery", QuerySQLDomain: "eql_v4.query_double_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "DoubleOrd", GoName: "DoubleOrd", Family: "double", Suffix: "Ord", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord", Indexes: []string{"ope"}, Query: "DoubleOrdQuery", QuerySQLDomain: "eql_v4.query_double_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "DoubleOrdOpe", GoName: "DoubleOrdOpe", Family: "double", Suffix: "OrdOpe", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord_ope", Indexes: []string{"ope"}, Query: "DoubleOrdOpeQuery", QuerySQLDomain: "eql_v4.query_double_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, {Name: "SteVecDocument", GoName: "SteVecDocument", Family: "json", Suffix: "Search", Plaintext: "", SQLDomain: "public.eql_v4_json_search", Indexes: []string{"json"}, Query: "SteVecQuery", QuerySQLDomain: "eql_v4.query_json", Producible: false, Reason: "the JSON index is a new operation in the engine"}, - {Name: "Json", GoName: "JSON", Family: "json", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_json", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, + {Name: "Json", GoName: "JSON", Family: "json", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_json", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "a JSON value seals as a tree of leaves and EQL's `c` holds one; how a document becomes one sealed leaf is not decided"}, } // Integer is the EQL type stored in public.eql_v4_integer: the integer family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a int32. type Integer []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -87,7 +87,7 @@ func (v *Integer) UnmarshalJSON(b []byte) error { } // IntegerEq is the EQL type stored in public.eql_v4_integer_eq: the integer family with the eq index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a int32. type IntegerEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -134,7 +134,7 @@ func (v *IntegerEqQuery) UnmarshalJSON(b []byte) error { } // IntegerOrdOre is the EQL type stored in public.eql_v4_integer_ord_ore: the integer family with the ore index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type IntegerOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -181,7 +181,7 @@ func (v *IntegerOrdOreQuery) UnmarshalJSON(b []byte) error { } // IntegerOrd is the EQL type stored in public.eql_v4_integer_ord: the integer family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type IntegerOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -228,7 +228,7 @@ func (v *IntegerOrdQuery) UnmarshalJSON(b []byte) error { } // IntegerOrdOpe is the EQL type stored in public.eql_v4_integer_ord_ope: the integer family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type IntegerOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -275,7 +275,7 @@ func (v *IntegerOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Smallint is the EQL type stored in public.eql_v4_smallint: the smallint family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type Smallint []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -299,7 +299,7 @@ func (v *Smallint) UnmarshalJSON(b []byte) error { } // SmallintEq is the EQL type stored in public.eql_v4_smallint_eq: the smallint family with the eq index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type SmallintEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -346,7 +346,7 @@ func (v *SmallintEqQuery) UnmarshalJSON(b []byte) error { } // SmallintOrdOre is the EQL type stored in public.eql_v4_smallint_ord_ore: the smallint family with the ore index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type SmallintOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -393,7 +393,7 @@ func (v *SmallintOrdOreQuery) UnmarshalJSON(b []byte) error { } // SmallintOrd is the EQL type stored in public.eql_v4_smallint_ord: the smallint family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type SmallintOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -440,7 +440,7 @@ func (v *SmallintOrdQuery) UnmarshalJSON(b []byte) error { } // SmallintOrdOpe is the EQL type stored in public.eql_v4_smallint_ord_ope: the smallint family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type SmallintOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -487,7 +487,7 @@ func (v *SmallintOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Bigint is the EQL type stored in public.eql_v4_bigint: the bigint family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a int64. type Bigint []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -511,7 +511,7 @@ func (v *Bigint) UnmarshalJSON(b []byte) error { } // BigintEq is the EQL type stored in public.eql_v4_bigint_eq: the bigint family with the eq index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a int64. type BigintEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -558,7 +558,7 @@ func (v *BigintEqQuery) UnmarshalJSON(b []byte) error { } // BigintOrdOre is the EQL type stored in public.eql_v4_bigint_ord_ore: the bigint family with the ore index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type BigintOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -605,7 +605,7 @@ func (v *BigintOrdOreQuery) UnmarshalJSON(b []byte) error { } // BigintOrd is the EQL type stored in public.eql_v4_bigint_ord: the bigint family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type BigintOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -652,7 +652,7 @@ func (v *BigintOrdQuery) UnmarshalJSON(b []byte) error { } // BigintOrdOpe is the EQL type stored in public.eql_v4_bigint_ord_ope: the bigint family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type BigintOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -699,7 +699,7 @@ func (v *BigintOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Date is the EQL type stored in public.eql_v4_date: the date family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type Date []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -723,7 +723,7 @@ func (v *Date) UnmarshalJSON(b []byte) error { } // DateEq is the EQL type stored in public.eql_v4_date_eq: the date family with the eq index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type DateEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -770,7 +770,7 @@ func (v *DateEqQuery) UnmarshalJSON(b []byte) error { } // DateOrdOre is the EQL type stored in public.eql_v4_date_ord_ore: the date family with the ore index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type DateOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -817,7 +817,7 @@ func (v *DateOrdOreQuery) UnmarshalJSON(b []byte) error { } // DateOrd is the EQL type stored in public.eql_v4_date_ord: the date family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type DateOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -864,7 +864,7 @@ func (v *DateOrdQuery) UnmarshalJSON(b []byte) error { } // DateOrdOpe is the EQL type stored in public.eql_v4_date_ord_ope: the date family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type DateOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -911,7 +911,7 @@ func (v *DateOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Timestamp is the EQL type stored in public.eql_v4_timestamp: the timestamp family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type Timestamp []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -935,7 +935,7 @@ func (v *Timestamp) UnmarshalJSON(b []byte) error { } // TimestampEq is the EQL type stored in public.eql_v4_timestamp_eq: the timestamp family with the eq index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type TimestampEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -982,7 +982,7 @@ func (v *TimestampEqQuery) UnmarshalJSON(b []byte) error { } // TimestampOrdOre is the EQL type stored in public.eql_v4_timestamp_ord_ore: the timestamp family with the ore index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type TimestampOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1029,7 +1029,7 @@ func (v *TimestampOrdOreQuery) UnmarshalJSON(b []byte) error { } // TimestampOrd is the EQL type stored in public.eql_v4_timestamp_ord: the timestamp family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type TimestampOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1076,7 +1076,7 @@ func (v *TimestampOrdQuery) UnmarshalJSON(b []byte) error { } // TimestampOrdOpe is the EQL type stored in public.eql_v4_timestamp_ord_ope: the timestamp family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type TimestampOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1123,7 +1123,7 @@ func (v *TimestampOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Numeric is the EQL type stored in public.eql_v4_numeric: the numeric family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type Numeric []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1147,7 +1147,7 @@ func (v *Numeric) UnmarshalJSON(b []byte) error { } // NumericEq is the EQL type stored in public.eql_v4_numeric_eq: the numeric family with the eq index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type NumericEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1194,7 +1194,7 @@ func (v *NumericEqQuery) UnmarshalJSON(b []byte) error { } // NumericOrdOre is the EQL type stored in public.eql_v4_numeric_ord_ore: the numeric family with the ore index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type NumericOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1241,7 +1241,7 @@ func (v *NumericOrdOreQuery) UnmarshalJSON(b []byte) error { } // NumericOrd is the EQL type stored in public.eql_v4_numeric_ord: the numeric family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type NumericOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1288,7 +1288,7 @@ func (v *NumericOrdQuery) UnmarshalJSON(b []byte) error { } // NumericOrdOpe is the EQL type stored in public.eql_v4_numeric_ord_ope: the numeric family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). type NumericOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1335,7 +1335,7 @@ func (v *NumericOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Text is the EQL type stored in public.eql_v4_text: the text family with no index. -// The engine cannot produce it yet: the storage-only text domain follows once TextEq is proven end to end in PostgreSQL. +// The engine produces it from a string. type Text []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1406,7 +1406,7 @@ func (v *TextEqQuery) UnmarshalJSON(b []byte) error { } // TextMatch is the EQL type stored in public.eql_v4_text_match: the text family with the match index. -// The engine cannot produce it yet: the engine derives match and OPE terms, but no EQL type is built from them yet. +// The engine cannot produce it yet: the engine derives match terms, but no EQL type is built from them yet. type TextMatch []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1453,7 +1453,7 @@ func (v *TextMatchQuery) UnmarshalJSON(b []byte) error { } // TextOrdOre is the EQL type stored in public.eql_v4_text_ord_ore: the text family with the eq and ore indexes. -// The engine cannot produce it yet: EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type TextOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1500,7 +1500,7 @@ func (v *TextOrdOreQuery) UnmarshalJSON(b []byte) error { } // TextOrd is the EQL type stored in public.eql_v4_text_ord: the text family with the eq and ope indexes. -// The engine cannot produce it yet: the engine derives match and OPE terms, but no EQL type is built from them yet. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type TextOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1547,7 +1547,7 @@ func (v *TextOrdQuery) UnmarshalJSON(b []byte) error { } // TextOrdOpe is the EQL type stored in public.eql_v4_text_ord_ope: the text family with the eq and ope indexes. -// The engine cannot produce it yet: the engine derives match and OPE terms, but no EQL type is built from them yet. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type TextOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1594,7 +1594,7 @@ func (v *TextOrdOpeQuery) UnmarshalJSON(b []byte) error { } // TextSearchOre is the EQL type stored in public.eql_v4_text_search_ore: the text family with the eq and ore and match indexes. -// The engine cannot produce it yet: EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type TextSearchOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1641,7 +1641,7 @@ func (v *TextSearchOreQuery) UnmarshalJSON(b []byte) error { } // TextSearch is the EQL type stored in public.eql_v4_text_search: the text family with the eq and ope and match indexes. -// The engine cannot produce it yet: the engine derives match and OPE terms, but no EQL type is built from them yet. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type TextSearch []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1688,7 +1688,7 @@ func (v *TextSearchQuery) UnmarshalJSON(b []byte) error { } // Boolean is the EQL type stored in public.eql_v4_boolean: the boolean family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a bool. type Boolean []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1712,7 +1712,7 @@ func (v *Boolean) UnmarshalJSON(b []byte) error { } // Real is the EQL type stored in public.eql_v4_real: the real family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a float32. type Real []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1736,7 +1736,7 @@ func (v *Real) UnmarshalJSON(b []byte) error { } // RealEq is the EQL type stored in public.eql_v4_real_eq: the real family with the eq index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002). type RealEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1783,7 +1783,7 @@ func (v *RealEqQuery) UnmarshalJSON(b []byte) error { } // RealOrdOre is the EQL type stored in public.eql_v4_real_ord_ore: the real family with the ore index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type RealOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1830,7 +1830,7 @@ func (v *RealOrdOreQuery) UnmarshalJSON(b []byte) error { } // RealOrd is the EQL type stored in public.eql_v4_real_ord: the real family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type RealOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1877,7 +1877,7 @@ func (v *RealOrdQuery) UnmarshalJSON(b []byte) error { } // RealOrdOpe is the EQL type stored in public.eql_v4_real_ord_ope: the real family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type RealOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1924,7 +1924,7 @@ func (v *RealOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Double is the EQL type stored in public.eql_v4_double: the double family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a float64. type Double []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1948,7 +1948,7 @@ func (v *Double) UnmarshalJSON(b []byte) error { } // DoubleEq is the EQL type stored in public.eql_v4_double_eq: the double family with the eq index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002). type DoubleEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1995,7 +1995,7 @@ func (v *DoubleEqQuery) UnmarshalJSON(b []byte) error { } // DoubleOrdOre is the EQL type stored in public.eql_v4_double_ord_ore: the double family with the ore index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type DoubleOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -2042,7 +2042,7 @@ func (v *DoubleOrdOreQuery) UnmarshalJSON(b []byte) error { } // DoubleOrd is the EQL type stored in public.eql_v4_double_ord: the double family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type DoubleOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -2089,7 +2089,7 @@ func (v *DoubleOrdQuery) UnmarshalJSON(b []byte) error { } // DoubleOrdOpe is the EQL type stored in public.eql_v4_double_ord_ope: the double family with the ope index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type DoubleOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -2183,7 +2183,7 @@ func (v *SteVecQuery) UnmarshalJSON(b []byte) error { } // JSON is the EQL type stored in public.eql_v4_json: the json family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: a JSON value seals as a tree of leaves and EQL's `c` holds one; how a document becomes one sealed leaf is not decided. type JSON []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. diff --git a/languages/golang/encrypt/eql/eql_test.go b/languages/golang/encrypt/eql/eql_test.go index 426158bd7..c5b1ca9c1 100644 --- a/languages/golang/encrypt/eql/eql_test.go +++ b/languages/golang/encrypt/eql/eql_test.go @@ -7,7 +7,7 @@ import ( "github.com/cipherstash/stack/languages/golang/encrypt/eql" ) -func TestTypesTableNamesTextEqAsTheOneProducibleType(t *testing.T) { +func TestTypesTableNamesTheProducibleTypes(t *testing.T) { var producible []string for _, typ := range eql.Types { if typ.Producible { @@ -19,8 +19,19 @@ func TestTypesTableNamesTextEqAsTheOneProducibleType(t *testing.T) { t.Errorf("%s: an unproducible type has a reason", typ.Name) } } - if len(producible) != 1 || producible[0] != "TextEq" { - t.Fatalf("producible = %v, want [TextEq]", producible) + want := []string{"Integer", "IntegerEq", "Bigint", "BigintEq", "Text", "TextEq", "Boolean", "Real", "Double"} + if len(producible) != len(want) { + t.Fatalf("producible = %v, want %v", producible, want) + } + for i := range want { + if producible[i] != want[i] { + t.Fatalf("producible = %v, want %v", producible, want) + } + } + for name, plaintext := range map[string]string{"IntegerEq": "int32", "BigintEq": "int64", "Boolean": "bool", "Real": "float32", "Double": "float64"} { + if typ, ok := eql.Lookup(name); !ok || typ.Plaintext != plaintext { + t.Errorf("%s: Plaintext = %q, want %q", name, typ.Plaintext, plaintext) + } } textEq, ok := eql.Lookup("TextEq") if !ok || textEq.GoName != "TextEq" || textEq.Plaintext != "string" || textEq.Query != "TextEqQuery" || textEq.SQLDomain != "public.eql_v4_text_eq" || len(textEq.Indexes) != 1 || textEq.Indexes[0] != "eq" { diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 09815fb6a..f9ce384e9 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -709,8 +709,10 @@ dependencies = [ "stack-encrypt", "thiserror 2.0.20", "ts-rs", + "vitaminc-aead", "vitaminc-aead-value", "vitaminc-prf", + "vitaminc-protected", "zeroize", ] diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index e053231f3..f2cfa8072 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -1204,8 +1204,10 @@ dependencies = [ "stack-encrypt", "thiserror 2.0.20", "ts-rs", + "vitaminc-aead 0.5.1", "vitaminc-aead-value", "vitaminc-prf", + "vitaminc-protected 0.5.1", "zeroize", ] diff --git a/packages/eql/crates/eql-bindings/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml index 602ff7851..2d1ea219d 100644 --- a/packages/eql/crates/eql-bindings/Cargo.toml +++ b/packages/eql/crates/eql-bindings/Cargo.toml @@ -55,6 +55,11 @@ vitaminc-prf = { version = "0.5.0", optional = true } # nothing from stack-encrypt's own `dynamic` feature; the same line # stack-encrypt and the Go guest build against. vitaminc-aead-value = { version = "0.5.1", optional = true } +# `DecipherVisitor` and `Protected`: the scalar plaintexts in +# `encryption::plaintext` seal and open vitaminc's tagged leaves themselves, +# because vitaminc 0.5 has no native `Encrypt` for the scalar types. +vitaminc-aead = { version = "0.5.1", optional = true } +vitaminc-protected = { version = "0.5.1", optional = true } # The plaintext a target reads out of its `Protected` runtime value is held # in `Zeroizing` until it is sealed, so the copy is wiped like the original. zeroize = { version = "1", optional = true } @@ -68,6 +73,8 @@ stack-encrypt = [ "dep:stack-encrypt", "dep:vitaminc-prf", "dep:vitaminc-aead-value", + "dep:vitaminc-aead", + "dep:vitaminc-protected", "dep:zeroize", "dep:thiserror", "dep:base64", diff --git a/packages/eql/crates/eql-bindings/README.md b/packages/eql/crates/eql-bindings/README.md index f4bb4c87e..af8f3ab9f 100644 --- a/packages/eql/crates/eql-bindings/README.md +++ b/packages/eql/crates/eql-bindings/README.md @@ -104,8 +104,27 @@ evolution plan. The optional `stack-encrypt` feature enables `EncryptFrom` for `v4::text::TextEq` and `v4::text::TextEqQuery`, and `DecryptInto` for `v4::text::TextEq`. Stack Encrypt writes EQL v4 only, so no `v3` type -implements these traits. Other v4 domains do not yet expose these conversions. -The derives are emitted by `eql-codegen`. +implements these traits. The derives are emitted by `eql-codegen`. + +The other v4 types Stack Encrypt produces take a plaintext from +`eql_bindings::encryption::plaintext`, because vitaminc 0.5 has no native +encryption for the scalar types: + +| v4 type | Plaintext | +| --- | --- | +| `Text` | `String` | +| `Integer`, `IntegerEq` | `Int32` | +| `Bigint`, `BigintEq` | `Int64` | +| `Boolean` | `Bool` | +| `Real` | `Float32` | +| `Double` | `Float64` | + +Each seals as vitaminc's tagged leaf, the bytes the value enum (`FfiValue`) +seals for the same value, so values written here and by the Go SDK open each +other, and `IntegerEq` / `BigintEq` derive the equality term of the integer +inside. Ordering types wait for vitaminc 0.6's order encodings, float +equality for its canonical float form, and `Json` for a decision on sealing a +document as one leaf. The generated API docs include a complete, executable example in the `eql_bindings::encryption` module, from cipher setup through decryption. Its diff --git a/packages/eql/crates/eql-bindings/src/encryption.rs b/packages/eql/crates/eql-bindings/src/encryption.rs index 3588f30f6..0134178a3 100644 --- a/packages/eql/crates/eql-bindings/src/encryption.rs +++ b/packages/eql/crates/eql-bindings/src/encryption.rs @@ -78,6 +78,7 @@ //! require::(); //! ``` +pub mod plaintext; pub mod targets; use base64::{engine::general_purpose::STANDARD, Engine as _}; diff --git a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs new file mode 100644 index 000000000..95e3cdbc7 --- /dev/null +++ b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs @@ -0,0 +1,199 @@ +//! Scalar plaintexts for the v4 number and boolean types. +//! +//! A Stack Encrypt target encrypts a plaintext through Vitamin C's +//! `Encrypt`, `Decrypt` and `PrfValue` traits, and vitaminc 0.5 implements +//! `Encrypt` for `String` but not for `i32`, `i64`, `bool`, `f32` or `f64`. +//! These newtypes fill that gap for the EQL v4 types: [`Int32`] for +//! `Integer` and `IntegerEq`, [`Int64`] for `Bigint` and `BigintEq`, [`Bool`] +//! for `Boolean`, and [`Float32`] / [`Float64`] for `Real` and `Double`. +//! +//! Each seals as vitaminc's tagged leaf, `[tag] ++ payload` from the frozen +//! table in `vitaminc_aead_value::tags`, which is exactly what the value enum +//! (`FfiValue`) seals for the same value. So a value written through these +//! types and one written by the Go SDK as a plain `int32` field open each +//! other, and an equality term derived from [`Int32`] is the one derived from +//! the `i32` inside it. +//! +//! ```rust,ignore +//! use eql_bindings::encryption::plaintext::Int32; +//! use eql_bindings::v4::integer::IntegerEq; +//! +//! let stored: IntegerEq = keyset.encrypt_as(&Int32(42), column).await?; +//! let opened: Int32 = cipher.decrypt_as(stored, expected).await?; +//! ``` +//! +//! None of them implements `Debug`, so a plaintext does not reach a log by +//! accident. + +use stack_encrypt::{Cipher, Decipher, Decrypt, Encrypt, IntoAad, Unspecified}; +use vitaminc_aead::DecipherVisitor; +use vitaminc_aead_value::tagged::{ + TaggedBoolFalse, TaggedBoolTrue, TaggedFloat32, TaggedFloat64, TaggedInt32, TaggedInt64, +}; +use vitaminc_aead_value::tags; +use vitaminc_prf::{IntoPrfContext, Prf, PrfValue, PrfVisitor}; +use vitaminc_protected::{Controlled, Protected}; +use zeroize::Zeroize; + +/// Reads one tagged scalar leaf back, refusing any other tag or width: a +/// value sealed as one kind never opens as another. +struct LeafVisitor { + tag: u8, + parse: fn(&[u8]) -> Option, +} + +impl<'c, T: Send> DecipherVisitor<'c> for LeafVisitor { + type Value = T; + + fn visit_bytes_vec(self, data: Protected>) -> Result { + match data.risky_ref().split_first() { + Some((&tag, payload)) if tag == self.tag => (self.parse)(payload).ok_or(Unspecified), + _ => Err(Unspecified), + } + } +} + +macro_rules! fixed_scalar { + ($(#[$doc:meta])* $name:ident($inner:ty), $tagged:ty, $tag:expr, $width:literal, |$bytes:ident| $decode:expr) => { + $(#[$doc])* + #[derive(Clone, Copy, PartialEq, Zeroize)] + pub struct $name(pub $inner); + + impl From<$inner> for $name { + fn from(value: $inner) -> Self { + Self(value) + } + } + + impl From<$name> for $inner { + fn from(value: $name) -> Self { + value.0 + } + } + + impl Encrypt for $name { + fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result + where + C: Cipher, + A: IntoAad<'a>, + { + <$tagged>::from(self.0).encrypt_with_aad(cipher, aad) + } + } + + impl<'c> Decrypt<'c> for $name { + fn decrypt_with_aad<'a, D, A>(decipher: D, aad: A) -> D::Ok + where + D: Decipher<'c>, + A: IntoAad<'a>, + { + decipher.decrypt_any( + LeafVisitor { + tag: $tag, + parse: |payload| { + let $bytes: [u8; $width] = payload.try_into().ok()?; + Some($name($decode)) + }, + }, + aad, + ) + } + } + }; +} + +fixed_scalar!( + /// The plaintext of `Integer` and `IntegerEq`: a 32-bit signed integer. + Int32(i32), TaggedInt32, tags::INT32, 4, |b| i32::from_le_bytes(b) +); +fixed_scalar!( + /// The plaintext of `Bigint` and `BigintEq`: a 64-bit signed integer. + Int64(i64), TaggedInt64, tags::INT64, 8, |b| i64::from_le_bytes(b) +); +fixed_scalar!( + /// The plaintext of `Real`: a 32-bit float, sealed by its bit pattern, so + /// `-0.0` and a NaN's payload survive a round trip. + Float32(f32), TaggedFloat32, tags::FLOAT32, 4, |b| f32::from_bits(u32::from_le_bytes(b)) +); +fixed_scalar!( + /// The plaintext of `Double`: a 64-bit float, sealed by its bit pattern. + Float64(f64), TaggedFloat64, tags::FLOAT64, 8, |b| f64::from_bits(u64::from_le_bytes(b)) +); + +/// The equality term of an integer is the term of the integer inside it, so +/// `IntegerEq` and a plain `int32` field with an equality index agree. +macro_rules! prf_by_inner { + ($($name:ident),+) => {$( + impl PrfValue for $name { + fn prf_visit_with_context<'a, P, V, C>( + self, + prf: &P, + context: C, + visitor: V, + ) -> P::Ok + where + P: Prf, + V: PrfVisitor, + C: IntoPrfContext<'a>, + { + self.0.prf_visit_with_context(prf, context, visitor) + } + } + )+}; +} +prf_by_inner!(Int32, Int64); + +/// The plaintext of `Boolean`. A boolean is the tag itself: no payload. +#[derive(Clone, Copy, PartialEq, Zeroize)] +pub struct Bool(pub bool); + +impl From for Bool { + fn from(value: bool) -> Self { + Self(value) + } +} + +impl From for bool { + fn from(value: Bool) -> Self { + value.0 + } +} + +impl Encrypt for Bool { + fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result + where + C: Cipher, + A: IntoAad<'a>, + { + if self.0 { + TaggedBoolTrue::tag_only().encrypt_with_aad(cipher, aad) + } else { + TaggedBoolFalse::tag_only().encrypt_with_aad(cipher, aad) + } + } +} + +/// Opens either boolean tag, and nothing else. +struct BoolVisitor; + +impl<'c> DecipherVisitor<'c> for BoolVisitor { + type Value = Bool; + + fn visit_bytes_vec(self, data: Protected>) -> Result { + match data.risky_ref().as_slice() { + [tags::BOOL_TRUE] => Ok(Bool(true)), + [tags::BOOL_FALSE] => Ok(Bool(false)), + _ => Err(Unspecified), + } + } +} + +impl<'c> Decrypt<'c> for Bool { + fn decrypt_with_aad<'a, D, A>(decipher: D, aad: A) -> D::Ok + where + D: Decipher<'c>, + A: IntoAad<'a>, + { + decipher.decrypt_any(BoolVisitor, aad) + } +} diff --git a/packages/eql/crates/eql-bindings/src/encryption/targets.rs b/packages/eql/crates/eql-bindings/src/encryption/targets.rs index b3c6d9944..ed23c11c0 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/targets.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/targets.rs @@ -86,6 +86,7 @@ use stack_encrypt::{ }; use vitaminc_aead_value::{FfiValue, ValueKind}; +use super::plaintext; use crate::v4::targets::{decrypt_named, encrypt_named, query_named, TARGETS}; use crate::Identifier; @@ -383,8 +384,15 @@ pub(crate) fn refuse(name: &str) -> TargetError { } mod sealed { + use super::plaintext::{Bool, Float32, Float64, Int32, Int64}; + pub trait Sealed {} impl Sealed for String {} + impl Sealed for Int32 {} + impl Sealed for Int64 {} + impl Sealed for Bool {} + impl Sealed for Float32 {} + impl Sealed for Float64 {} } /// A Rust plaintext an EQL type is produced from, read out of and written @@ -392,7 +400,7 @@ mod sealed { /// plaintext types the catalog's producible families name, and the generated /// dispatch picks one per type. `Zeroize`, because [`from_value`](Self::from_value) /// copies the plaintext out of the runtime value's `Protected` buffer and -/// the copy is wiped when it is dropped (the dispatch holds it in +/// the copy is wiped when it is dropped (`run_target` holds it in /// [`zeroize::Zeroizing`]), as the original is. pub trait Plaintext: sealed::Sealed + Sized + MaybeSend + zeroize::Zeroize + 'static { /// The kind of value this plaintext is. @@ -429,6 +437,39 @@ impl Plaintext for String { } } +/// The scalar plaintexts read exactly their own kind: a JSON number is not +/// narrowed or widened into another width, because the stored bytes must be +/// the declared kind's. +macro_rules! scalar_plaintext { + ($($ty:ident => $variant:ident),+ $(,)?) => {$( + impl Plaintext for plaintext::$ty { + const KIND: ValueKind = ValueKind::$variant; + + fn from_value(target: &'static str, value: FfiValue) -> Result { + match value { + FfiValue::$variant(v) => Ok(plaintext::$ty(v)), + other => Err(TargetError::Plaintext { + target, + expected: Self::KIND, + found: other.kind(), + }), + } + } + + fn into_value(self) -> FfiValue { + FfiValue::$variant(self.0) + } + } + )+}; +} +scalar_plaintext!( + Int32 => Int32, + Int64 => Int64, + Bool => Bool, + Float32 => Float32, + Float64 => Float64, +); + /// Run a target's own plan over one runtime value and resolve to the EQL /// value's JSON bytes. The generated dispatch calls this with the type and /// its plaintext; it is the one place the typed `encrypt_as` is reached from diff --git a/packages/eql/crates/eql-bindings/src/v4/bigint.rs b/packages/eql/crates/eql-bindings/src/v4/bigint.rs index 98e4aa73a..ee0942ed3 100644 --- a/packages/eql/crates/eql-bindings/src/v4/bigint.rs +++ b/packages/eql/crates/eql-bindings/src/v4/bigint.rs @@ -14,10 +14,31 @@ use ts_rs::TS; doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`Int64`](crate::encryption::plaintext::Int64). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Int64) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Bigint { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } @@ -55,10 +76,31 @@ impl DomainType for Bigint { doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`Int64`](crate::encryption::plaintext::Int64). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Int64) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct BigintEq { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -223,10 +265,30 @@ impl DomainType for BigintOrdOpe { doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input: [`Int64`](crate::encryption::plaintext::Int64). With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Int64) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct BigintEqQuery { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub hm: Hmac256, } diff --git a/packages/eql/crates/eql-bindings/src/v4/boolean.rs b/packages/eql/crates/eql-bindings/src/v4/boolean.rs index ca4a1c71a..c9983ab0e 100644 --- a/packages/eql/crates/eql-bindings/src/v4/boolean.rs +++ b/packages/eql/crates/eql-bindings/src/v4/boolean.rs @@ -14,10 +14,31 @@ use ts_rs::TS; doc, doc = " See the [EQL boolean reference](https://cipherstash.com/docs/reference/eql/booleans) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`Bool`](crate::encryption::plaintext::Bool). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Bool) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Boolean { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } diff --git a/packages/eql/crates/eql-bindings/src/v4/double.rs b/packages/eql/crates/eql-bindings/src/v4/double.rs index bd73e4043..e69c39c35 100644 --- a/packages/eql/crates/eql-bindings/src/v4/double.rs +++ b/packages/eql/crates/eql-bindings/src/v4/double.rs @@ -14,10 +14,31 @@ use ts_rs::TS; doc, doc = " See the [EQL double reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`Float64`](crate::encryption::plaintext::Float64). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Float64) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Double { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } diff --git a/packages/eql/crates/eql-bindings/src/v4/integer.rs b/packages/eql/crates/eql-bindings/src/v4/integer.rs index f320c8794..4d885febb 100644 --- a/packages/eql/crates/eql-bindings/src/v4/integer.rs +++ b/packages/eql/crates/eql-bindings/src/v4/integer.rs @@ -14,10 +14,31 @@ use ts_rs::TS; doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`Int32`](crate::encryption::plaintext::Int32). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Int32) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Integer { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } @@ -55,10 +76,31 @@ impl DomainType for Integer { doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`Int32`](crate::encryption::plaintext::Int32). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Int32) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct IntegerEq { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -223,10 +265,30 @@ impl DomainType for IntegerOrdOpe { doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input: [`Int32`](crate::encryption::plaintext::Int32). With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Int32) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct IntegerEqQuery { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub hm: Hmac256, } diff --git a/packages/eql/crates/eql-bindings/src/v4/real.rs b/packages/eql/crates/eql-bindings/src/v4/real.rs index 6d6ef3b88..389153d5a 100644 --- a/packages/eql/crates/eql-bindings/src/v4/real.rs +++ b/packages/eql/crates/eql-bindings/src/v4/real.rs @@ -14,10 +14,31 @@ use ts_rs::TS; doc, doc = " See the [EQL real reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`Float32`](crate::encryption::plaintext::Float32). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] +#[cfg_attr( + feature = "stack-encrypt", + stash(plaintext = crate::encryption::plaintext::Float32) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Real { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } diff --git a/packages/eql/crates/eql-bindings/src/v4/targets.rs b/packages/eql/crates/eql-bindings/src/v4/targets.rs index cfaa39d8b..eefa79df6 100644 --- a/packages/eql/crates/eql-bindings/src/v4/targets.rs +++ b/packages/eql/crates/eql-bindings/src/v4/targets.rs @@ -1,5 +1,10 @@ // @generated by eql-codegen from the eql-domains catalog — do not edit //! The EQL types a Stack Encrypt data plan may name as a field target — every stored EQL v4 domain type in eql-domains::CATALOG order, as data a guest serializes (`TARGETS`), with the by-name dispatch that runs a producible type's own Rust plan. Generated from the catalog; the descriptor type, the errors and the public entry points stay hand-written in `crate::encryption::targets`, which documents the wire format. +use crate::encryption::plaintext::Bool; +use crate::encryption::plaintext::Float32; +use crate::encryption::plaintext::Float64; +use crate::encryption::plaintext::Int32; +use crate::encryption::plaintext::Int64; use crate::encryption::targets::{ open_target, refuse, refuse_query, run_target, Opener, Target, TargetError, }; @@ -15,70 +20,66 @@ pub const TARGETS: &[Target] = &[ name: "Integer", family: "integer", suffix: "", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "IntegerEq", family: "integer", suffix: "Eq", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer_eq", indexes: &["eq"], query: Some("IntegerEqQuery"), query_sql_domain: Some("eql_v4.query_integer_eq"), - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "IntegerOrdOre", family: "integer", suffix: "OrdOre", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer_ord_ore", indexes: &["ore"], query: Some("IntegerOrdOreQuery"), query_sql_domain: Some("eql_v4.query_integer_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "IntegerOrd", family: "integer", suffix: "Ord", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer_ord", indexes: &["ope"], query: Some("IntegerOrdQuery"), query_sql_domain: Some("eql_v4.query_integer_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "IntegerOrdOpe", family: "integer", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer_ord_ope", indexes: &["ope"], query: Some("IntegerOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_integer_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -92,7 +93,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -106,7 +107,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -120,7 +121,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -134,7 +135,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -148,77 +149,73 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "Bigint", family: "bigint", suffix: "", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "BigintEq", family: "bigint", suffix: "Eq", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint_eq", indexes: &["eq"], query: Some("BigintEqQuery"), query_sql_domain: Some("eql_v4.query_bigint_eq"), - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "BigintOrdOre", family: "bigint", suffix: "OrdOre", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint_ord_ore", indexes: &["ore"], query: Some("BigintOrdOreQuery"), query_sql_domain: Some("eql_v4.query_bigint_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "BigintOrd", family: "bigint", suffix: "Ord", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint_ord", indexes: &["ope"], query: Some("BigintOrdQuery"), query_sql_domain: Some("eql_v4.query_bigint_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "BigintOrdOpe", family: "bigint", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint_ord_ope", indexes: &["ope"], query: Some("BigintOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_bigint_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -232,7 +229,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -246,7 +243,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -260,7 +257,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -274,7 +271,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -288,7 +285,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -302,7 +299,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -316,7 +313,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -330,7 +327,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -344,7 +341,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -358,7 +355,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -372,7 +369,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -386,7 +383,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -400,7 +397,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -414,7 +411,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -428,7 +425,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -440,10 +437,8 @@ pub const TARGETS: &[Target] = &[ indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "the storage-only text domain follows once TextEq is proven end to end in PostgreSQL", - ), + producible: true, + reason: None, }, Target { name: "TextEq", @@ -468,7 +463,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_match"), producible: false, reason: Some( - "the engine derives match and OPE terms, but no EQL type is built from them yet", + "the engine derives match terms, but no EQL type is built from them yet", ), }, Target { @@ -482,7 +477,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord_ore"), producible: false, reason: Some( - "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -496,7 +491,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord"), producible: false, reason: Some( - "the engine derives match and OPE terms, but no EQL type is built from them yet", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -510,7 +505,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord_ope"), producible: false, reason: Some( - "the engine derives match and OPE terms, but no EQL type is built from them yet", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -524,7 +519,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_search_ore"), producible: false, reason: Some( - "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -538,161 +533,155 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_search"), producible: false, reason: Some( - "the engine derives match and OPE terms, but no EQL type is built from them yet", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "Boolean", family: "boolean", suffix: "", - plaintext: None, + plaintext: Some("bool"), sql_domain: "public.eql_v4_boolean", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "Real", family: "real", suffix: "", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "RealEq", family: "real", suffix: "Eq", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real_eq", indexes: &["eq"], query: Some("RealEqQuery"), query_sql_domain: Some("eql_v4.query_real_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "RealOrdOre", family: "real", suffix: "OrdOre", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real_ord_ore", indexes: &["ore"], query: Some("RealOrdOreQuery"), query_sql_domain: Some("eql_v4.query_real_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "RealOrd", family: "real", suffix: "Ord", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real_ord", indexes: &["ope"], query: Some("RealOrdQuery"), query_sql_domain: Some("eql_v4.query_real_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "RealOrdOpe", family: "real", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real_ord_ope", indexes: &["ope"], query: Some("RealOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_real_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "Double", family: "double", suffix: "", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "DoubleEq", family: "double", suffix: "Eq", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double_eq", indexes: &["eq"], query: Some("DoubleEqQuery"), query_sql_domain: Some("eql_v4.query_double_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "DoubleOrdOre", family: "double", suffix: "OrdOre", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double_ord_ore", indexes: &["ore"], query: Some("DoubleOrdOreQuery"), query_sql_domain: Some("eql_v4.query_double_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "DoubleOrd", family: "double", suffix: "Ord", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double_ord", indexes: &["ope"], query: Some("DoubleOrdQuery"), query_sql_domain: Some("eql_v4.query_double_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "DoubleOrdOpe", family: "double", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double_ord_ope", indexes: &["ope"], query: Some("DoubleOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_double_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -718,7 +707,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "a JSON value seals as a tree of leaves and EQL's `c` holds one; how a document becomes one sealed leaf is not decided", ), }, ]; @@ -732,9 +721,32 @@ pub(crate) fn encrypt_named<'a, K: 'static>( plaintext: FfiValue, ) -> Result, K>, TargetError> { match name { + "Integer" => { + run_target::("Integer", keyset, column, plaintext) + } + "IntegerEq" => run_target::( + "IntegerEq", + keyset, + column, + plaintext, + ), + "Bigint" => { + run_target::("Bigint", keyset, column, plaintext) + } + "BigintEq" => { + run_target::("BigintEq", keyset, column, plaintext) + } + "Text" => run_target::("Text", keyset, column, plaintext), "TextEq" => { run_target::("TextEq", keyset, column, plaintext) } + "Boolean" => { + run_target::("Boolean", keyset, column, plaintext) + } + "Real" => run_target::("Real", keyset, column, plaintext), + "Double" => { + run_target::("Double", keyset, column, plaintext) + } _ => Err(refuse(name)), } } @@ -747,7 +759,27 @@ pub(crate) fn decrypt_named<'a, K: 'static>( stored: &[u8], ) -> Result, TargetError> { match name { + "Integer" => { + open_target::("Integer", opener, column, stored) + } + "IntegerEq" => { + open_target::("IntegerEq", opener, column, stored) + } + "Bigint" => { + open_target::("Bigint", opener, column, stored) + } + "BigintEq" => { + open_target::("BigintEq", opener, column, stored) + } + "Text" => open_target::("Text", opener, column, stored), "TextEq" => open_target::("TextEq", opener, column, stored), + "Boolean" => { + open_target::("Boolean", opener, column, stored) + } + "Real" => open_target::("Real", opener, column, stored), + "Double" => { + open_target::("Double", opener, column, stored) + } _ => Err(refuse(name)), } } @@ -761,6 +793,15 @@ pub(crate) fn query_named<'a, K: 'static>( plaintext: FfiValue, ) -> Result, K>, TargetError> { match name { + "IntegerEq" => run_target::( + "IntegerEq", + keyset, + column, + plaintext, + ), + "BigintEq" => run_target::( + "BigintEq", keyset, column, plaintext, + ), "TextEq" => { run_target::("TextEq", keyset, column, plaintext) } diff --git a/packages/eql/crates/eql-bindings/src/v4/text.rs b/packages/eql/crates/eql-bindings/src/v4/text.rs index ce20abc8a..317ebf763 100644 --- a/packages/eql/crates/eql-bindings/src/v4/text.rs +++ b/packages/eql/crates/eql-bindings/src/v4/text.rs @@ -14,10 +14,28 @@ use ts_rs::TS; doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." +)] +#[cfg_attr( + all(doc, feature = "stack-encrypt"), + doc = " See the [complete encryption example](crate::encryption#example)." +)] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] +#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] +#[cfg_attr(feature = "stack-encrypt", stash(plaintext = String))] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Text { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } @@ -55,10 +73,10 @@ impl DomainType for Text { doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] #[cfg_attr( - doc, - doc = " Plaintext input and decrypted output: [`String`]. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input and decrypted output: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." )] #[cfg_attr( all(doc, feature = "stack-encrypt"), @@ -374,10 +392,10 @@ impl DomainType for TextSearch { doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] +#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] #[cfg_attr( - doc, - doc = " Plaintext input: [`String`]. With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." + all(doc, feature = "stack-encrypt"), + doc = " Plaintext input: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." )] #[cfg_attr( all(doc, feature = "stack-encrypt"), diff --git a/packages/eql/crates/eql-codegen/src/bindings.rs b/packages/eql/crates/eql-codegen/src/bindings.rs index bb96b4fc4..196817952 100644 --- a/packages/eql/crates/eql-codegen/src/bindings.rs +++ b/packages/eql/crates/eql-codegen/src/bindings.rs @@ -333,17 +333,45 @@ fn render_query_struct(eql: EqlVersion, family: &DomainFamily, domain: &Domain) } } -/// The `(family, domain)` pairs whose generated structs carry the opt-in -/// `stack-encrypt` derives. Text equality is the first; every other stored -/// domain has a reason in [`encryption_gap`], and a test holds the two -/// together so a new domain cannot arrive without either a derive or a reason. -pub const ENCRYPTION_DOMAINS: &[(&str, &str)] = &[("text", "eq")]; +/// The `(family, domain)` pairs whose generated v4 structs carry the opt-in +/// `stack-encrypt` derives: every storage-only domain and every +/// equality-only domain whose plaintext vitaminc 0.5 encodes and, for +/// equality, derives a term for. Every other stored domain has a reason in +/// [`encryption_gap`], and a test holds the two together so a new domain +/// cannot arrive without either a derive or a reason. +pub const ENCRYPTION_DOMAINS: &[(&str, &str)] = &[ + ("integer", ""), + ("integer", "eq"), + ("bigint", ""), + ("bigint", "eq"), + ("text", ""), + ("text", "eq"), + ("boolean", ""), + ("real", ""), + ("double", ""), +]; /// Why no `eql_v3` struct derives Stack Encrypt's traits. pub const V3_ENCRYPTION_GAP: &str = "Stack Encrypt produces EQL v4 only: its output is not byte-for-byte compatible with \ cipherstash-client's, which writes the eql_v3 domains"; +/// The plaintext a family's v4 domains are produced from: the vitaminc +/// `ValueKind` name a plan field's `"type"` key spells, and the Rust type the +/// derive and the by-name dispatch name. `None` for a family vitaminc 0.5 +/// has no scalar kind for. +pub fn plaintext_of(family: &str) -> Option<(&'static str, &'static str)> { + Some(match family { + "text" => ("string", "String"), + "integer" => ("int32", "crate::encryption::plaintext::Int32"), + "bigint" => ("int64", "crate::encryption::plaintext::Int64"), + "boolean" => ("bool", "crate::encryption::plaintext::Bool"), + "real" => ("float32", "crate::encryption::plaintext::Float32"), + "double" => ("float64", "crate::encryption::plaintext::Float64"), + _ => return None, + }) +} + /// Why a stored domain carries no `stack-encrypt` derive yet, or `None` when /// it does. Derived from the catalog's own facts (family, terms), not from a /// per-domain list, so it answers for a domain added tomorrow. No `eql_v3` @@ -359,22 +387,30 @@ pub fn encryption_gap( if ENCRYPTION_DOMAINS.contains(&(family.name, domain.name)) { return None; } - Some(if family.name != "text" { - "only the text family's plaintext encoding is specified for the stack-encrypt \ - producer profile; the numeric, date, boolean and JSON encodings are plan section 2 \ - and have not been settled" - } else if domain.terms.iter().any(|t| !matches!(t, Term::Hm)) { - "ordering and match terms need stack-encrypt operations that have not landed \ - (block-ORE and OPE term derivation, bloom-filter match terms)" - } else if domain.terms.is_empty() { - "the storage-only text domain follows once TextEq is proven end to end in \ - PostgreSQL and the interoperability matrix is recorded" + Some(if !domain.is_scalar() { + "the JSON index is a new operation in the engine" + } else if family.name == "json" { + "a JSON value seals as a tree of leaves and EQL's `c` holds one; how a document \ + becomes one sealed leaf is not decided" + } else if plaintext_of(family.name).is_none() { + "vitaminc 0.5's value enum has no kind for this family; the kind and its \ + encodings arrive in vitaminc 0.6 (ADR-0002)" + } else if domain + .terms + .iter() + .any(|t| matches!(t, Term::Ore | Term::Ope)) + { + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)" + } else if domain.terms.contains(&Term::Bloom) { + "the engine derives match terms, but no EQL type is built from them yet" + } else if matches!(family.name, "real" | "double") { + "vitaminc 0.5 derives no equality term for a float; the canonical float form \ + arrives in vitaminc 0.6 (ADR-0002)" } else { - // Reached by an equality-only text domain other than `eq`: nothing in - // the catalog today, but the branch above would otherwise call it - // storage-only, which it is not. - "an equality-only text domain carries the same term TextEq does and derives the \ - same way; it is not listed in ENCRYPTION_DOMAINS yet" + // Reached by an equality-only domain of a family whose plaintext is + // specified but which is not listed: nothing in the catalog today. + "this domain derives the way its family's other domains do; it is not listed in \ + ENCRYPTION_DOMAINS yet" }) } @@ -393,22 +429,31 @@ fn encryption_attrs( let decrypt = (!query).then( || quote!(#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))]), ); + let (_, rust) = plaintext_of(family.name).expect("a derived family names its plaintext"); + let plaintext_ty: syn::Type = syn::parse_str(rust).expect("plaintext is a type path"); + let shown = rust.rsplit("::").next().unwrap_or(rust); let plaintext = if query { - " Plaintext input: [`String`]. With the `stack-encrypt` feature, implements \ - `EncryptFrom` to build equality query terms. Query operands do not decrypt." + format!( + " Plaintext input: [`{shown}`]({rust}). With the `stack-encrypt` feature, \ + implements `EncryptFrom<{shown}>` to build equality query terms. Query operands \ + do not decrypt." + ) } else { - " Plaintext input and decrypted output: [`String`]. With the `stack-encrypt` \ - feature, implements `EncryptFrom` and `DecryptInto`." + format!( + " Plaintext input and decrypted output: [`{shown}`]({rust}). With the \ + `stack-encrypt` feature, implements `EncryptFrom<{shown}>` and \ + `DecryptInto<{shown}>`." + ) }; ( quote! { - #[cfg_attr(doc, doc = "")] - #[cfg_attr(doc, doc = #plaintext)] + #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] + #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = #plaintext)] #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`.")] #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = " See the [complete encryption example](crate::encryption#example).")] #[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] #decrypt - #[cfg_attr(feature = "stack-encrypt", stash(plaintext = String))] + #[cfg_attr(feature = "stack-encrypt", stash(plaintext = #plaintext_ty))] }, quote!(#[cfg_attr(feature = "stack-encrypt", stash(default))]), quote!(#[cfg_attr(feature = "stack-encrypt", stash(context_field))]), @@ -1664,16 +1709,26 @@ mod tests { // catalog fact that selects it. Some(reason) => { let full = domain.full_name(family.name); - let expected = if family.name != "text" { - "only the text family" - } else if domain.terms.iter().any(|t| !matches!(t, Term::Hm)) { - "ordering and match terms" + let expected = if !domain.is_scalar() { + "JSON index" + } else if family.name == "json" { + "tree of leaves" + } else if plaintext_of(family.name).is_none() { + "has no kind for this family" + } else if domain + .terms + .iter() + .any(|t| matches!(t, Term::Ore | Term::Ope)) + { + "ordering terms" + } else if domain.terms.contains(&Term::Bloom) { + "match terms" } else { assert!( - domain.terms.is_empty(), - "{full}: an hm-only text domain other than eq is not in the catalog" + matches!(family.name, "real" | "double"), + "{full}: every other specified, unordered domain is derived" ); - "storage-only text domain" + "equality term for a float" }; assert!(reason.contains(expected), "{full}: {reason}"); } @@ -1711,7 +1766,7 @@ mod tests { } #[test] - fn an_equality_only_text_domain_is_not_called_storage_only() { + fn an_unlisted_equality_only_domain_says_it_is_unlisted() { // Not in the catalog, which is why the branch needs a synthetic // domain: the only text domain reaching the hm-only branch today has // no terms, so a wrong reason there would survive every catalog walk. @@ -1725,7 +1780,10 @@ mod tests { shape: eql_domains::Shape::Scalar, }; let reason = encryption_gap(EqlVersion::V4, &family, &hm_only).unwrap(); - assert!(reason.contains("equality-only"), "{reason}"); + assert!( + reason.contains("not listed in ENCRYPTION_DOMAINS"), + "{reason}" + ); assert!(!reason.contains("storage-only"), "{reason}"); } } diff --git a/packages/eql/crates/eql-codegen/src/go_eql.rs b/packages/eql/crates/eql-codegen/src/go_eql.rs index c9ecb05b7..2a1d74f4c 100644 --- a/packages/eql/crates/eql-codegen/src/go_eql.rs +++ b/packages/eql/crates/eql-codegen/src/go_eql.rs @@ -206,7 +206,7 @@ mod tests { // TextEq is producible and says so; TextOrdOre is not and says why. assert!(out.contains("// TextEq is the EQL type stored in public.eql_v4_text_eq")); assert!(out.contains("Producible: true, Reason: \"\"")); - assert!(out.contains("Name: \"TextOrdOre\"") && out.contains("CLLW")); + assert!(out.contains("Name: \"TextOrdOre\"") && out.contains("ordering terms wait")); // Every table row is one line: the shape gofmt leaves alone. for line in out.lines().filter(|l| l.starts_with("\t{Name: ")) { assert!(line.ends_with("},"), "{line}"); diff --git a/packages/eql/crates/eql-codegen/src/targets.rs b/packages/eql/crates/eql-codegen/src/targets.rs index 976df60c6..2de603e00 100644 --- a/packages/eql/crates/eql-codegen/src/targets.rs +++ b/packages/eql/crates/eql-codegen/src/targets.rs @@ -29,7 +29,7 @@ use quote::{format_ident, quote}; use eql_domains::{Domain, DomainFamily, Shape, Term}; -use crate::bindings::{encryption_gap, format_rs, stored_payload_domains}; +use crate::bindings::{encryption_gap, format_rs, plaintext_of, stored_payload_domains}; use crate::render::EqlVersion; /// The version every target is produced into. @@ -54,47 +54,18 @@ pub fn index_key(term: Term) -> &'static str { /// payload keys. const JSON_INDEX_KEY: &str = "json"; -/// The plaintext a family's domains are produced from, as a vitaminc -/// `ValueKind` name (what a plan field's `"type"` key spells) and the Rust -/// type the generated dispatch names. `None` while the family's plaintext -/// encoding for the stack-encrypt producer profile is unspecified, which is -/// every family but text today. +/// The plaintext a family's domains are produced from: see +/// [`bindings::plaintext_of`](crate::bindings::plaintext_of). pub fn plaintext(family: &DomainFamily) -> Option<(&'static str, &'static str)> { plaintext_of(family.name) } -/// [`plaintext`], by family name: what a rendered row carries. -fn plaintext_of(family: &str) -> Option<(&'static str, &'static str)> { - (family == "text").then_some(("string", "String")) -} - /// Why the engine cannot produce a stored domain's EQL type today, or -/// `None` when it can. `None` exactly when the domain carries the -/// `stack-encrypt` derives ([`encryption_gap`]), because the dispatch runs -/// the derived plan; the reasons are the plan's, keyed on catalog facts so -/// a domain added tomorrow gets an answer. +/// `None` when it can: exactly when the v4 domain carries the +/// `stack-encrypt` derives, because the dispatch runs the derived plan, so +/// the one source of reasons is [`encryption_gap`]. pub fn target_gap(family: &DomainFamily, domain: &Domain) -> Option<&'static str> { - encryption_gap(TARGET_VERSION, family, domain)?; - Some(if matches!(domain.shape, Shape::SteVec) { - "the JSON index is a new operation in the engine" - } else if family.name != "text" { - "how this family encodes a plaintext for the stack-encrypt producer profile is not \ - specified; only the text family's encoding is" - } else if domain.terms.contains(&Term::Ore) { - "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are \ - different algorithms" - } else if domain - .terms - .iter() - .any(|t| matches!(t, Term::Bloom | Term::Ope)) - { - "the engine derives match and OPE terms, but no EQL type is built from them yet" - } else if domain.terms.is_empty() { - "the storage-only text domain follows once TextEq is proven end to end in PostgreSQL" - } else { - "an equality-only text domain derives the same way TextEq does; it is not listed in \ - ENCRYPTION_DOMAINS yet" - }) + encryption_gap(TARGET_VERSION, family, domain) } /// One row of the generated table, computed from the catalog: what both @@ -245,7 +216,8 @@ fn render_targets_from(rows: &[Row]) -> String { let ty = format_ident!("{strukt}"); let (_, rust) = plaintext_of(r.family).expect("a producible family has a specified plaintext"); - let source = format_ident!("{rust}"); + // Named by its last segment: the file imports every plaintext path. + let source = format_ident!("{}", rust.rsplit("::").next().unwrap_or(rust)); (name.clone(), quote!(super::#module::#ty), quote!(#source)) }; let encrypt_arms: TokenStream = producible @@ -272,6 +244,22 @@ fn render_targets_from(rows: &[Row]) -> String { Some(quote! { #name => run_target::<#ty, #source, K>(#name, keyset, column, plaintext), }) }) .collect(); + // One `use` per plaintext a producible type names by path (`String` is + // in the prelude), so the arms stay short enough to read. + let mut imports: Vec<&str> = producible + .iter() + .filter_map(|r| plaintext_of(r.family).map(|(_, rust)| rust)) + .filter(|rust| rust.contains("::")) + .collect(); + imports.sort_unstable(); + imports.dedup(); + let imports: TokenStream = imports + .iter() + .map(|rust| { + let path: syn::Path = syn::parse_str(rust).expect("plaintext is a type path"); + quote!(use #path;) + }) + .collect(); let helpers = if producible.is_empty() { quote!() } else { @@ -291,6 +279,7 @@ fn render_targets_from(rows: &[Row]) -> String { use vitaminc_aead_value::FfiValue; + #imports use crate::encryption::targets::{#helpers refuse, refuse_query, Opener, Target, TargetError}; use crate::Identifier; use stack_encrypt::{KeysetCipher, NonEmpty, Pending}; @@ -384,63 +373,49 @@ mod tests { ); } - /// Every reason is the plan's reason for that domain, selected by the - /// catalog fact that applies: a wrong branch order would hand the ORE - /// reason to `text_search` or the encoding reason to a text domain. + /// A target's reason is its v4 domain's derive reason: one source, so + /// the table and the derives cannot disagree about why. #[test] - fn every_unproducible_target_has_the_plan_reason_for_its_domain() { + fn every_unproducible_target_has_its_derive_reason() { for (family, domain) in stored_payload_domains() { - let full = domain.full_name(family.name); - let Some(reason) = target_gap(family, domain) else { - continue; - }; - let expected = if matches!(domain.shape, Shape::SteVec) { - "JSON index" - } else if family.name != "text" { - "encodes a plaintext" - } else if domain.terms.contains(&Term::Ore) { - "block ORE" - } else if domain - .terms - .iter() - .any(|t| matches!(t, Term::Bloom | Term::Ope)) - { - "match and OPE terms" - } else { - assert!( - domain.terms.is_empty(), - "{full}: an hm-only text domain other than eq is not in the catalog" - ); - "storage-only text domain" - }; - assert!(reason.contains(expected), "{full}: {reason}"); + assert_eq!( + target_gap(family, domain), + encryption_gap(TARGET_VERSION, family, domain), + "{}", + domain.full_name(family.name) + ); } - // Spelled out for the four plan rows, so the table reads without - // the derivation above. + // Spelled out, so the table reads without the derivation. + let gap = |family: &eql_domains::DomainFamily, name: &str| { + target_gap(family, family.domain_by_name(name).expect("domain")) + }; let text = eql_domains::TEXT; - let by_name = |name: &str| text.domain_by_name(name).expect("text domain"); - assert!(target_gap(&text, by_name("ord_ore")) - .unwrap() - .contains("CLLW")); - assert!(target_gap(&text, by_name("search_ore")) - .unwrap() - .contains("CLLW")); - assert!(target_gap(&text, by_name("match")) - .unwrap() - .contains("no EQL type")); - assert!(target_gap(&text, by_name("ord")) - .unwrap() - .contains("no EQL type")); - assert!(target_gap(&text, by_name("ord_ope")) + for producible in ["", "eq"] { + assert!(gap(&text, producible).is_none(), "text {producible:?}"); + } + for ordered in ["ord", "ord_ore", "ord_ope", "search", "search_ore"] { + assert!( + gap(&text, ordered).unwrap().contains("ordering terms"), + "{ordered}" + ); + } + assert!(gap(&text, "match").unwrap().contains("match terms")); + let family = |name: &str| { + eql_domains::CATALOG + .iter() + .find(|f| f.name == name) + .expect("family") + }; + assert!(gap(family("integer"), "eq").is_none()); + assert!(gap(family("integer"), "ord_ore") .unwrap() - .contains("no EQL type")); - assert!(target_gap(&text, by_name("search")) + .contains("ordering terms")); + assert!(gap(family("real"), "eq").unwrap().contains("float")); + assert!(gap(family("date"), "").unwrap().contains("has no kind")); + assert!(gap(family("smallint"), "eq") .unwrap() - .contains("no EQL type")); - assert!( - target_gap(&text, by_name("eq")).is_none(), - "TextEq is producible" - ); + .contains("has no kind")); + assert!(gap(family("json"), "").unwrap().contains("tree of leaves")); } #[test] @@ -500,47 +475,64 @@ mod tests { }) { let name = domain.rust_struct_name(family.name); let module = family.name; + let (_, plaintext) = plaintext_of(family.name).expect("a derived family's plaintext"); + let plaintext = plaintext.rsplit("::").next().unwrap(); let arm = |helper: &str, ty: &str| { format!( - "\"{name}\" => {{ {helper}::(\"{name}\", " + "\"{name}\" => {{ {helper}::(\"{name}\", " ) }; let flat_arm = |helper: &str, ty: &str| { - format!("\"{name}\" => {helper}::(\"{name}\", ") + format!( + "\"{name}\" => {helper}::(\"{name}\", " + ) }; + // rustfmt breaks long arms at different points, so compare with + // all whitespace gone. + let tight: String = out.split_whitespace().collect(); let count = |helper: &str, ty: &str| { - out.matches(&arm(helper, ty)).count() + out.matches(&flat_arm(helper, ty)).count() + let strip = |p: String| p.split_whitespace().collect::(); + tight.matches(&strip(arm(helper, ty))).count() + + tight.matches(&strip(flat_arm(helper, ty))).count() }; assert_eq!(count("run_target", &name), 1, "{name}: one encrypt arm"); assert_eq!(count("open_target", &name), 1, "{name}: one decrypt arm"); assert_eq!( count("run_target", &format!("{name}Query")), - 1, - "{name}: one query arm" + usize::from(!domain.terms.is_empty()), + "{name}: a query arm exactly when it has a query twin" ); } // No unproducible type has an arm: the only `=>` arms are the // producible ones and the three fall-throughs. + let with_twin = ENCRYPTION_DOMAINS + .iter() + .filter(|(f, d)| { + let family = eql_domains::CATALOG.iter().find(|x| x.name == *f).unwrap(); + !family.domain_by_name(d).unwrap().terms.is_empty() + }) + .count(); let arms = out.matches("\" => ").count(); - assert_eq!(arms, ENCRYPTION_DOMAINS.len() * 3, "arms: {out}"); + assert_eq!( + arms, + ENCRYPTION_DOMAINS.len() * 2 + with_twin, + "arms: {out}" + ); assert_eq!(out.matches("_ => Err(refuse(name))").count(), 2); assert_eq!(out.matches("_ => Err(refuse_query(name))").count(), 1); } - /// A producible type with no query twin — a storage-only domain, once - /// its derive lands — renders encrypt and decrypt arms and no query arm, - /// rather than aborting the generator: the query dispatch's fall-through - /// refuses it as answering no query. Not in the catalog today, so the - /// row is flipped by hand. + /// A producible type with no query twin — the storage-only `Text` — + /// renders encrypt and decrypt arms and no query arm: the query + /// dispatch's fall-through refuses it as answering no query. #[test] fn a_producible_type_without_a_query_twin_gets_no_query_arm() { - let mut rows = rows(); + let rows = rows(); let text = rows - .iter_mut() + .iter() .find(|r| r.name == "Text") .expect("the storage-only text domain"); - assert!(text.query.is_none() && text.reason.is_some()); - text.reason = None; + assert!(text.query.is_none() && text.reason.is_none()); let out: String = render_targets_from(&rows) .split_whitespace() .collect::>() @@ -562,10 +554,5 @@ mod tests { !out.contains("TextQuery"), "no query arm for a type with no twin: {out}" ); - assert_eq!( - out.matches("\" => ").count(), - (ENCRYPTION_DOMAINS.len() + 1) * 3 - 1, - "every producible type has three arms but the twinless one, which has two" - ); } } diff --git a/packages/eql/mise.toml b/packages/eql/mise.toml index 233078020..5fe00f278 100644 --- a/packages/eql/mise.toml +++ b/packages/eql/mise.toml @@ -330,9 +330,11 @@ psql "$EQL_TEST_DATABASE_URL" -v ON_ERROR_STOP=1 -f release/cipherstash-encrypt- # `--ignored` selects by attribute, and cargo exits 0 on an empty selection, so # a test that loses its `#[ignore]` or is renamed would leave this task passing # on nothing. List what the filter selects before running it. -listing=$(cargo test --locked -p eql-encryption-tests --test text_eq -- --ignored --list | sed -n 's/: test$//p') -[ -n "$listing" ] || { echo "error: '--ignored' selects no test in text_eq; the PostgreSQL test has gone missing." >&2; exit 1; } -cargo test --locked -p eql-encryption-tests --test text_eq -- --ignored +for suite in text_eq v4_types; do + listing=$(cargo test --locked -p eql-encryption-tests --test "$suite" -- --ignored --list | sed -n 's/: test$//p') + [ -n "$listing" ] || { echo "error: '--ignored' selects no test in $suite; the PostgreSQL test has gone missing." >&2; exit 1; } + cargo test --locked -p eql-encryption-tests --test "$suite" -- --ignored +done """ [tasks."check:encryption:wasi"] diff --git a/packages/eql/tests/encryption/tests/targets.rs b/packages/eql/tests/encryption/tests/targets.rs index 39c06e5a1..dbf48cae2 100644 --- a/packages/eql/tests/encryption/tests/targets.rs +++ b/packages/eql/tests/encryption/tests/targets.rs @@ -359,8 +359,8 @@ mod given_a_name_the_engine_cannot_produce { let error = refused(targets::encrypt("TextOrdOre", &keyset, &email(), text("x"))); assert_eq!( error.to_string(), - "the engine cannot produce TextOrdOre yet: EQL stores a block ORE term and the engine \ - derives a CLLW ORE term; the two are different algorithms" + "the engine cannot produce TextOrdOre yet: ordering terms wait for vitaminc-ore, the \ + order encodings in vitaminc 0.6 (ADR-0002)" ); // Refused by name before the label or the value is looked at. let error = refused(targets::encrypt( @@ -472,14 +472,24 @@ mod the_table { }) .collect(); assert_eq!(row.suffix, suffix, "{name}: suffix"); + let kind = match family.name { + "text" => Some(ValueKind::String), + "integer" => Some(ValueKind::Int32), + "bigint" => Some(ValueKind::Int64), + "boolean" => Some(ValueKind::Bool), + "real" => Some(ValueKind::Float32), + "double" => Some(ValueKind::Float64), + // The rest have no scalar kind in vitaminc 0.5's value enum. + _ => None, + }; assert_eq!( row.plaintext, - (family.name == "text").then_some("string"), - "{name}: only text's plaintext encoding is specified" + kind.map(ValueKind::name), + "{name}: the family's plaintext kind" ); assert_eq!( row.plaintext_kind(), - (family.name == "text").then_some(ValueKind::String), + kind, "{name}: the plaintext name is a vitaminc kind" ); if matches!(domain.shape, Shape::SteVec) { @@ -543,8 +553,11 @@ mod the_table { } } + /// The types the engine produces in EQL v4 today: every storage-only + /// and equality-only domain whose plaintext vitaminc 0.5 encodes and, + /// for equality, derives a term for. #[test] - fn text_eq_is_the_one_producible_type_today() { + fn the_producible_types_are_the_unordered_ones_vitaminc_0_5_encodes() { let producible: Vec<&str> = targets::targets() .iter() .filter(|t| t.producible) @@ -552,18 +565,28 @@ mod the_table { .collect(); assert_eq!( producible, - ["TextEq"], - "the plan's list: the engine produces TextEq only" + [ + "Integer", + "IntegerEq", + "Bigint", + "BigintEq", + "Text", + "TextEq", + "Boolean", + "Real", + "Double" + ], ); for row in targets::targets().iter().filter(|t| !t.producible) { let reason = row.reason.expect("an unproducible type has a reason"); let expected = match (row.family, row.suffix) { ("json", "Search") => "JSON index", - (family, _) if family != "text" => "encodes a plaintext", - (_, "OrdOre" | "SearchOre") => "CLLW", - (_, "Match" | "Ord" | "OrdOpe" | "Search") => "no EQL type is built", - (_, "") => "storage-only", - (_, suffix) => panic!("{}: unexpected text suffix {suffix}", row.name), + ("json", "") => "tree of leaves", + ("smallint" | "date" | "timestamp" | "numeric", _) => "has no kind", + (_, "Ord" | "OrdOre" | "OrdOpe" | "Search" | "SearchOre") => "ordering terms", + ("text", "Match") => "match terms", + ("real" | "double", "Eq") => "float", + (family, suffix) => panic!("{}: unexpected {family} {suffix}", row.name), }; assert!(reason.contains(expected), "{}: {reason}", row.name); } @@ -589,7 +612,7 @@ mod the_table { "reason": null }) ); - let row = targets::target("Integer").unwrap(); + let row = targets::target("Date").unwrap(); let json = serde_json::to_value(row).unwrap(); assert_eq!( json["plaintext"], diff --git a/packages/eql/tests/encryption/tests/v4_types.rs b/packages/eql/tests/encryption/tests/v4_types.rs new file mode 100644 index 000000000..2e8206bac --- /dev/null +++ b/packages/eql/tests/encryption/tests/v4_types.rs @@ -0,0 +1,455 @@ +//! The EQL v4 types Stack Encrypt produces besides `TextEq`: the integer, +//! bigint, boolean, float and storage-only text types. `Json` is not +//! producible yet: a JSON value seals as a tree of leaves and EQL's `c` +//! holds one. +//! +//! Each must round-trip through both entry points (the typed `encrypt_as` +//! and the by-name dispatch a Go plan reaches), seal exactly the leaf +//! vitaminc's value enum seals for the same value (what the Go SDK writes for +//! a plain field of that kind), derive the equality term a plain `i32` or +//! `i64` derives, and refuse a value of another kind. + +mod common; + +use eql_bindings::encryption::plaintext::{Bool, Float32, Float64, Int32, Int64}; +use eql_bindings::encryption::targets::{self as targets, TargetError}; +use eql_bindings::encryption::NativeCiphertextVisitor as NativeLeaf; +use eql_bindings::v3::terms::Hmac256; +use eql_bindings::v4::{bigint, boolean, double, integer, real, text}; +use eql_bindings::Identifier; +use serde::{de::DeserializeOwned, Serialize}; +use stack_encrypt::target::transcode::{Reader, Transcode}; +use stack_encrypt::{Label, NonEmpty}; +use vitaminc_aead_value::{FfiValue, ValueKind}; + +fn column() -> NonEmpty { + Identifier::for_column("users", "age").unwrap() +} + +fn label() -> Label { + Label::new(["users", "age"]).unwrap() +} + +/// The refusal a dispatch returned; `Pending` has no `Debug`. +fn refused(result: Result) -> TargetError { + match result { + Err(error) => error, + Ok(_) => panic!("the dispatch accepted what it should have refused"), + } +} + +/// The JSON a stored v4 value serializes to: version 4, the column, and the +/// ciphertext with the producer marker. +fn assert_v4_envelope(stored: &T, keys: &[&str]) { + let json = serde_json::to_value(stored).unwrap(); + assert_eq!(json["v"], 4, "Stack Encrypt writes EQL v4"); + assert_eq!(json["i"], serde_json::json!({"t": "users", "c": "age"})); + assert!(json["c"].as_str().unwrap().starts_with("stack-encrypt:1:")); + let mut found: Vec<&str> = json + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect(); + found.sort_unstable(); + let mut expected = keys.to_vec(); + expected.sort_unstable(); + assert_eq!(found, expected); +} + +/// What the stored ciphertext opens to when read as vitaminc's value enum: +/// the leaf the Go SDK would have sealed for the same value. +async fn as_value_enum( + cipher: &stack_encrypt::StackCipher, + c: eql_bindings::v3::terms::Ciphertext, +) -> FfiValue { + let native = c.read(NativeLeaf).unwrap(); + cipher.decrypt(native, column()).await.unwrap() +} + +/// Run one named target over `value` and read the stored JSON back as `T`. +async fn by_name( + keyset: &stack_encrypt::KeysetCipher<'_, common::Observed>, + name: &str, + value: FfiValue, +) -> T { + let bytes = targets::encrypt(name, keyset, &label(), value) + .unwrap() + .await + .unwrap(); + serde_json::from_slice(&bytes).unwrap() +} + +#[tokio::test] +async fn integers_round_trip_and_seal_the_value_enum_leaf() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + for value in [0, 42, -7, i32::MIN, i32::MAX] { + let stored: integer::IntegerEq = keyset.encrypt_as(&Int32(value), column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); + + let opened: Int32 = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, value); + assert!(matches!( + as_value_enum(&cipher, stored.c.clone()).await, + FfiValue::Int32(v) if v == value + )); + + // The term is a plain i32's, so it matches an `int32` field with an + // equality index written by the Go SDK. + let plain: Hmac256 = keyset + .equality_term(value, column()) + .await + .unwrap() + .read(Hmac256::visitor()) + .unwrap(); + assert_eq!(stored.hm, plain, "{value}: the equality term of the i32"); + + // The query twin derives the same term and carries no ciphertext. + let query: integer::IntegerEqQuery = + keyset.encrypt_as(&Int32(value), column()).await.unwrap(); + assert_eq!(query.hm, stored.hm); + + // The by-name dispatch runs the same plan. + let named: integer::IntegerEq = by_name(&keyset, "IntegerEq", FfiValue::Int32(value)).await; + assert_eq!(named.hm, stored.hm); + let opened = targets::decrypt( + "IntegerEq", + &cipher, + &label(), + &serde_json::to_vec(&stored).unwrap(), + ) + .unwrap() + .await + .unwrap(); + assert!(matches!(opened, FfiValue::Int32(v) if v == value)); + + let storage: integer::Integer = keyset.encrypt_as(&Int32(value), column()).await.unwrap(); + assert_v4_envelope(&storage, &["v", "i", "c"]); + } + + for value in [0i64, -1, i64::MIN, i64::MAX] { + let stored: bigint::BigintEq = keyset.encrypt_as(&Int64(value), column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); + let opened: Int64 = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, value); + assert!(matches!( + as_value_enum(&cipher, stored.c.clone()).await, + FfiValue::Int64(v) if v == value + )); + let plain: Hmac256 = keyset + .equality_term(value, column()) + .await + .unwrap() + .read(Hmac256::visitor()) + .unwrap(); + assert_eq!(stored.hm, plain, "{value}: the equality term of the i64"); + } +} + +#[tokio::test] +async fn equal_values_of_different_widths_have_different_terms() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + let narrow: integer::IntegerEq = keyset.encrypt_as(&Int32(42), column()).await.unwrap(); + let wide: bigint::BigintEq = keyset.encrypt_as(&Int64(42), column()).await.unwrap(); + assert_ne!( + narrow.hm, wide.hm, + "the term binds the width, as the separate domains do" + ); +} + +#[tokio::test] +async fn booleans_and_floats_round_trip_as_storage_only_types() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + for value in [true, false] { + let stored: boolean::Boolean = keyset.encrypt_as(&Bool(value), column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c"]); + let opened: Bool = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, value); + assert!(matches!( + as_value_enum(&cipher, stored.c).await, + FfiValue::Bool(v) if v == value + )); + } + // Floats seal their bit pattern: -0.0 and a NaN's payload survive. + for value in [1.5f32, -0.0, f32::NAN, f32::INFINITY] { + let stored: real::Real = keyset.encrypt_as(&Float32(value), column()).await.unwrap(); + let opened: Float32 = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0.to_bits(), value.to_bits()); + assert!(matches!( + as_value_enum(&cipher, stored.c).await, + FfiValue::Float32(v) if v.to_bits() == value.to_bits() + )); + } + for value in [2.25f64, -0.0, f64::NAN] { + let stored: double::Double = keyset.encrypt_as(&Float64(value), column()).await.unwrap(); + let opened: Float64 = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0.to_bits(), value.to_bits()); + } +} + +#[tokio::test] +async fn storage_only_text_round_trips_through_both_entry_points() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + let stored: text::Text = keyset + .encrypt_as(&"a private note".to_owned(), column()) + .await + .unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c"]); + let opened: String = cipher.decrypt_as(stored, column().into()).await.unwrap(); + assert_eq!(opened, "a private note"); + + let named: text::Text = by_name(&keyset, "Text", FfiValue::String("note".into())).await; + let opened: String = cipher.decrypt_as(named, column().into()).await.unwrap(); + assert_eq!(opened, "note"); + // A storage-only type answers no query. + let error = refused(targets::query( + "Text", + &keyset, + &label(), + FfiValue::String("note".into()), + )); + assert!( + matches!(error, TargetError::NoQuery { name: "Text" }), + "{error}" + ); +} + +#[tokio::test] +async fn a_value_of_another_kind_is_refused_not_converted() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + for (name, value, expected) in [ + ("IntegerEq", FfiValue::Int64(1), ValueKind::Int32), + ("BigintEq", FfiValue::Int32(1), ValueKind::Int64), + ("Boolean", FfiValue::Int32(1), ValueKind::Bool), + ("Real", FfiValue::Float64(1.0), ValueKind::Float32), + ("Text", FfiValue::Int32(1), ValueKind::String), + ] { + let error = refused(targets::encrypt(name, &keyset, &label(), value)); + assert!( + matches!(error, TargetError::Plaintext { target, expected: e, .. } if target == name && e == expected), + "{name}: {error}" + ); + } + + // A ciphertext moved into a type of another kind does not open: the tag + // is authenticated, and each plaintext reads only its own. + let narrow: integer::Integer = keyset.encrypt_as(&Int32(42), column()).await.unwrap(); + let moved = bigint::Bigint { + v: narrow.v, + i: narrow.i, + c: narrow.c, + }; + let opened: Result = cipher.decrypt_as(moved, column().into()).await; + assert!(opened.is_err()); +} + +/// Needs an EQL v4 installation; uses fresh real ciphertext with a fake key +/// source. +#[test] +#[ignore = "requires EQL_TEST_DATABASE_URL pointing to an EQL v4 database"] +fn postgres_stores_and_queries_every_producible_v4_type() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let (cipher, _) = runtime.block_on(common::cipher()); + let keyset = cipher.default_keyset(); + let mut db = postgres::Client::connect( + &std::env::var("EQL_TEST_DATABASE_URL").expect("EQL_TEST_DATABASE_URL"), + postgres::NoTls, + ) + .unwrap(); + db.batch_execute( + "CREATE TEMP TABLE v4_types ( + id int PRIMARY KEY, + age public.eql_v4_integer_eq, + balance public.eql_v4_bigint_eq, + age_only public.eql_v4_integer, + active public.eql_v4_boolean, + ratio public.eql_v4_real, + score public.eql_v4_double, + note public.eql_v4_text + ); + CREATE INDEX v4_types_age_idx ON v4_types (eql_v4.eq_term(age));", + ) + .unwrap(); + + let json = |value: &dyn erased::Json| value.to_json(); + for (id, age, balance) in [(1, 42, 10i64), (2, 7, 20), (3, 42, 30)] { + let row = runtime.block_on(async { + ( + keyset + .encrypt_as::<_, integer::IntegerEq>(&Int32(age), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, bigint::BigintEq>(&Int64(balance), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, integer::Integer>(&Int32(age), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, boolean::Boolean>(&Bool(id % 2 == 0), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, real::Real>(&Float32(0.5), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, double::Double>(&Float64(0.25), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, text::Text>(&"note".to_owned(), column()) + .await + .unwrap(), + ) + }); + db.execute( + "INSERT INTO v4_types VALUES ($1, + $2::text::jsonb::public.eql_v4_integer_eq, + $3::text::jsonb::public.eql_v4_bigint_eq, + $4::text::jsonb::public.eql_v4_integer, + $5::text::jsonb::public.eql_v4_boolean, + $6::text::jsonb::public.eql_v4_real, + $7::text::jsonb::public.eql_v4_double, + $8::text::jsonb::public.eql_v4_text)", + &[ + &id, + &json(&row.0), + &json(&row.1), + &json(&row.2), + &json(&row.3), + &json(&row.4), + &json(&row.5), + &json(&row.6), + ], + ) + .unwrap(); + } + + let probe: integer::IntegerEqQuery = runtime + .block_on(async { keyset.encrypt_as(&Int32(42), column()).await }) + .unwrap(); + let ids: Vec = db + .query( + "SELECT id FROM v4_types WHERE age = $1::text::jsonb::eql_v4.query_integer_eq ORDER BY id", + &[&json(&probe)], + ) + .unwrap() + .iter() + .map(|r| r.get(0)) + .collect(); + assert_eq!(ids, [1, 3], "IntegerEq equality finds both 42s"); + + let probe: bigint::BigintEqQuery = runtime + .block_on(async { keyset.encrypt_as(&Int64(20), column()).await }) + .unwrap(); + let ids: Vec = db + .query( + "SELECT id FROM v4_types WHERE balance = $1::text::jsonb::eql_v4.query_bigint_eq", + &[&json(&probe)], + ) + .unwrap() + .iter() + .map(|r| r.get(0)) + .collect(); + assert_eq!(ids, [2], "BigintEq equality"); + + // Every stored value reads back and opens. + let row = db + .query_one( + "SELECT age::text, active::text, score::text, note::text FROM v4_types WHERE id = 2", + &[], + ) + .unwrap(); + let opened: Int32 = runtime + .block_on(async { + cipher + .decrypt_as( + serde_json::from_str::(row.get(0)).unwrap(), + column().into(), + ) + .await + }) + .unwrap(); + assert_eq!(opened.0, 7); + let opened: Bool = runtime + .block_on(async { + cipher + .decrypt_as( + serde_json::from_str::(row.get(1)).unwrap(), + column().into(), + ) + .await + }) + .unwrap(); + assert!(opened.0); + let opened: Float64 = runtime + .block_on(async { + cipher + .decrypt_as( + serde_json::from_str::(row.get(2)).unwrap(), + column().into(), + ) + .await + }) + .unwrap(); + assert_eq!(opened.0, 0.25); + let opened: String = runtime + .block_on(async { + cipher + .decrypt_as( + serde_json::from_str::(row.get(3)).unwrap(), + column().into(), + ) + .await + }) + .unwrap(); + assert_eq!(opened, "note"); + // A storage-only column has no equality operator: it is blocked, not + // silently compared as jsonb. + let err = db + .query("SELECT id FROM v4_types WHERE age_only = age_only", &[]) + .unwrap_err(); + let message = err.as_db_error().map(|e| e.message()).unwrap_or_default(); + assert!( + message.contains("is not supported for public.eql_v4_integer"), + "{message}" + ); +} + +/// A stored value's JSON text, for binding as `$n::text::jsonb`. +mod erased { + pub trait Json { + fn to_json(&self) -> String; + } + impl Json for T { + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } + } +} From 9e2d96d394f071cde51656bd68b33c93845d6085 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 12 Oct 2026 22:57:16 +1100 Subject: [PATCH 2/4] test(eql-bindings): update the target unit tests for a producible Text Text became producible in this PR, so it is now the live storage-only type the no-query rule describes, and TextOrdOre's refusal names vitaminc-ore rather than CLLW. Both unit tests still asserted the old table and failed under --all-features. --- .../eql-bindings/src/encryption/targets.rs | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/eql/crates/eql-bindings/src/encryption/targets.rs b/packages/eql/crates/eql-bindings/src/encryption/targets.rs index ed23c11c0..57b6602f4 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/targets.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/targets.rs @@ -582,28 +582,13 @@ mod tests { #[test] fn a_producible_type_without_a_query_twin_answers_no_query() { - // No such type in the catalog today (`Text` is not producible), so - // the rule is pinned on a synthetic row; `refuse_query` on the live - // table falls through to `refuse`. - let storage_only = Target { - name: "Text", - family: "text", - suffix: "", - plaintext: Some("string"), - sql_domain: "public.eql_v4_text", - indexes: &[], - query: None, - query_sql_domain: None, - producible: true, - reason: None, - }; - let leaked: &'static Target = Box::leak(Box::new(storage_only)); + // `Text` is producible and storage-only, so it has no query twin. assert!(matches!( - no_query(Some(leaked)), + no_query(target("Text")), Some(TargetError::NoQuery { name: "Text" }) )); assert_eq!( - no_query(Some(leaked)).unwrap().to_string(), + refuse_query("Text").to_string(), "Text answers no query: it is a storage-only type" ); assert!( @@ -611,11 +596,11 @@ mod tests { "TextEq answers a query" ); assert!( - no_query(target("Text")).is_none(), + no_query(target("TextOrdOre")).is_none(), "an unproducible type is refused as that" ); assert!(matches!( - refuse_query("Text"), + refuse_query("TextOrdOre"), TargetError::Unproducible { .. } )); assert!(matches!(refuse_query("Nope"), TargetError::Unknown { .. })); @@ -626,7 +611,7 @@ mod tests { assert!(matches!(refuse("Nope"), TargetError::Unknown { name } if name == "Nope")); assert!(matches!( refuse("TextOrdOre"), - TargetError::Unproducible { name: "TextOrdOre", reason } if reason.contains("CLLW") + TargetError::Unproducible { name: "TextOrdOre", reason } if reason.contains("vitaminc-ore") )); assert!(producible("TextEq").is_ok()); } From ef6bb175a9a037de7df7104003f37e7802c1f4e4 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 12 Oct 2026 23:41:39 +1100 Subject: [PATCH 3/4] fix(eql-bindings): hold IntegerEq and BigintEq back until vitaminc 0.6 Their equality terms came from vitaminc 0.5's per-width PRF domains (i32-le/v1, i64-le/v1). vitaminc main replaces those with one integer-orderable/v1 domain for every width, so shipping them now would either break the promise that IntegerEq matches a Go int32 equality field or leave every stored hm unmatchable after the upgrade. Both leave ENCRYPTION_DOMAINS with a reason that names the domain, the generated dispatch and the Go table mark them unproducible, and the PRF impls on Int32 and Int64 are removed so nothing derives a term under the old domains. The storage-only types, TextEq and the plaintexts stay. v4_types.rs now covers what the review found missing: a leaf the value enum sealed opens as each typed plaintext, every producible row goes through all three by-name entry points (a swapped type or plaintext in the generator fails), Float64 is checked against the value enum, and the Postgres test reads every column back. The width test that pinned the per-width design is gone. The guest's producibility assertions and the eql-bindings unit tests use the final producible set, and the stashgen README lists each producible type with the Go field type it takes. --- .changeset/eql-v4-bindings.md | 2 +- languages/golang/cmd/stashgen/README.md | 14 +- languages/golang/encrypt/eql/eql.go | 6 +- languages/golang/encrypt/eql/eql_gen.go | 8 +- languages/golang/encrypt/eql/eql_test.go | 4 +- languages/golang/encrypt/guest/src/targets.rs | 6 +- .../golang/encrypt/guest/tests/native_ops.rs | 6 +- packages/eql/crates/eql-bindings/README.md | 12 +- .../eql-bindings/src/encryption/plaintext.rs | 48 +-- .../eql-bindings/src/encryption/targets.rs | 6 +- .../eql/crates/eql-bindings/src/v4/bigint.rs | 41 -- .../eql/crates/eql-bindings/src/v4/integer.rs | 41 -- .../eql/crates/eql-bindings/src/v4/targets.rs | 36 +- .../eql/crates/eql-codegen/src/bindings.rs | 14 +- .../eql/crates/eql-codegen/src/targets.rs | 10 +- .../eql/tests/encryption/tests/targets.rs | 13 +- .../eql/tests/encryption/tests/v4_types.rs | 405 +++++++++++------- 17 files changed, 340 insertions(+), 332 deletions(-) diff --git a/.changeset/eql-v4-bindings.md b/.changeset/eql-v4-bindings.md index b29c1ee00..546a460d7 100644 --- a/.changeset/eql-v4-bindings.md +++ b/.changeset/eql-v4-bindings.md @@ -2,6 +2,6 @@ '@cipherstash/eql': minor --- -**The `eql-bindings` crate has an `eql_bindings::v4` module: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`. `SchemaVersion` is now generic over the version it admits: `v3::SchemaVersion` is `SchemaVersion<3>` and `v4::SchemaVersion` is `SchemaVersion<4>`, each refusing the other's payloads, and plain `SchemaVersion` still means v3. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table (now `eql_bindings::v4::targets`) names v4 domains. Besides `TextEq`, Stack Encrypt now produces `Text`, `Integer`, `IntegerEq`, `Bigint`, `BigintEq`, `Boolean`, `Real` and `Double`, from the plaintext types in `eql_bindings::encryption::plaintext`; the Go SDK's `encrypt/eql` table marks them producible. +**The `eql-bindings` crate has an `eql_bindings::v4` module: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`. `SchemaVersion` is now generic over the version it admits: `v3::SchemaVersion` is `SchemaVersion<3>` and `v4::SchemaVersion` is `SchemaVersion<4>`, each refusing the other's payloads, and plain `SchemaVersion` still means v3. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table (now `eql_bindings::v4::targets`) names v4 domains. Besides `TextEq`, Stack Encrypt now produces the storage-only types `Text`, `Integer`, `Bigint`, `Boolean`, `Real` and `Double`, from the plaintext types in `eql_bindings::encryption::plaintext`; the Go SDK's `encrypt/eql` table marks them producible. `IntegerEq` and `BigintEq` stay unproducible until vitaminc 0.6 settles the integer equality term. Nothing a released version shipped has changed. The v3 types, the v3 SQL and the TypeScript package are as they were; the `stack-encrypt` derives on the v3 text types and the `v3::targets` module were added after the last release and are moved to v4 here, not removed from a release. diff --git a/languages/golang/cmd/stashgen/README.md b/languages/golang/cmd/stashgen/README.md index b2c521da5..227e1c855 100644 --- a/languages/golang/cmd/stashgen/README.md +++ b/languages/golang/cmd/stashgen/README.md @@ -149,7 +149,19 @@ The same input always gives the same file: fields keep their declared order, and `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, 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. +The engine produces these EQL types today, each from one Go field type: + +| `encrypt_into` | Go field | +|---|---| +| `TextEq`, `Text` | `string` | +| `Integer` | `int32` | +| `Bigint` | `int64` | +| `Boolean` | `bool` | +| `Real` | `float32` | +| `Double` | `float64` | + +The width must match: an `int` or `int64` field is not an `Integer`, and a `float64` field is not a `Real`. +`encrypt_into` with any other type is refused with the type's name and the reason it is not produced yet. 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`. diff --git a/languages/golang/encrypt/eql/eql.go b/languages/golang/encrypt/eql/eql.go index 24b3d7c17..28103f89c 100644 --- a/languages/golang/encrypt/eql/eql.go +++ b/languages/golang/encrypt/eql/eql.go @@ -18,9 +18,9 @@ // // The types and the [Types] table are generated from the EQL catalog by // eql-codegen (`mise run types:generate` in packages/eql); eql_gen.go is -// committed and drift-gated. The engine produces [TextEq] today; every -// other type is listed with the reason it cannot be produced yet, and -// stashgen refuses it. +// committed and drift-gated. [Types] says which types the engine produces +// today; every other type is listed with the reason it cannot be produced +// yet, and stashgen refuses it. package eql import ( diff --git a/languages/golang/encrypt/eql/eql_gen.go b/languages/golang/encrypt/eql/eql_gen.go index c1950eeed..d7b185e15 100644 --- a/languages/golang/encrypt/eql/eql_gen.go +++ b/languages/golang/encrypt/eql/eql_gen.go @@ -10,7 +10,7 @@ import "database/sql/driver" // it does not, with the Reason. var Types = []Type{ {Name: "Integer", GoName: "Integer", Family: "integer", Suffix: "", Plaintext: "int32", SQLDomain: "public.eql_v4_integer", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, - {Name: "IntegerEq", GoName: "IntegerEq", Family: "integer", Suffix: "Eq", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_eq", Indexes: []string{"eq"}, Query: "IntegerEqQuery", QuerySQLDomain: "eql_v4.query_integer_eq", Producible: true, Reason: ""}, + {Name: "IntegerEq", GoName: "IntegerEq", Family: "integer", Suffix: "Eq", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_eq", Indexes: []string{"eq"}, Query: "IntegerEqQuery", QuerySQLDomain: "eql_v4.query_integer_eq", Producible: false, Reason: "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade"}, {Name: "IntegerOrdOre", GoName: "IntegerOrdOre", Family: "integer", Suffix: "OrdOre", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord_ore", Indexes: []string{"ore"}, Query: "IntegerOrdOreQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, {Name: "IntegerOrd", GoName: "IntegerOrd", Family: "integer", Suffix: "Ord", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord", Indexes: []string{"ope"}, Query: "IntegerOrdQuery", QuerySQLDomain: "eql_v4.query_integer_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, {Name: "IntegerOrdOpe", GoName: "IntegerOrdOpe", Family: "integer", Suffix: "OrdOpe", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord_ope", Indexes: []string{"ope"}, Query: "IntegerOrdOpeQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, @@ -20,7 +20,7 @@ var Types = []Type{ {Name: "SmallintOrd", GoName: "SmallintOrd", Family: "smallint", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord", Indexes: []string{"ope"}, Query: "SmallintOrdQuery", QuerySQLDomain: "eql_v4.query_smallint_ord", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, {Name: "SmallintOrdOpe", GoName: "SmallintOrdOpe", Family: "smallint", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord_ope", Indexes: []string{"ope"}, Query: "SmallintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ope", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, {Name: "Bigint", GoName: "Bigint", Family: "bigint", Suffix: "", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, - {Name: "BigintEq", GoName: "BigintEq", Family: "bigint", Suffix: "Eq", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_eq", Indexes: []string{"eq"}, Query: "BigintEqQuery", QuerySQLDomain: "eql_v4.query_bigint_eq", Producible: true, Reason: ""}, + {Name: "BigintEq", GoName: "BigintEq", Family: "bigint", Suffix: "Eq", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_eq", Indexes: []string{"eq"}, Query: "BigintEqQuery", QuerySQLDomain: "eql_v4.query_bigint_eq", Producible: false, Reason: "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade"}, {Name: "BigintOrdOre", GoName: "BigintOrdOre", Family: "bigint", Suffix: "OrdOre", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord_ore", Indexes: []string{"ore"}, Query: "BigintOrdOreQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, {Name: "BigintOrd", GoName: "BigintOrd", Family: "bigint", Suffix: "Ord", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord", Indexes: []string{"ope"}, Query: "BigintOrdQuery", QuerySQLDomain: "eql_v4.query_bigint_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, {Name: "BigintOrdOpe", GoName: "BigintOrdOpe", Family: "bigint", Suffix: "OrdOpe", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord_ope", Indexes: []string{"ope"}, Query: "BigintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, @@ -87,7 +87,7 @@ func (v *Integer) UnmarshalJSON(b []byte) error { } // IntegerEq is the EQL type stored in public.eql_v4_integer_eq: the integer family with the eq index. -// The engine produces it from a int32. +// The engine cannot produce it yet: the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade. type IntegerEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -511,7 +511,7 @@ func (v *Bigint) UnmarshalJSON(b []byte) error { } // BigintEq is the EQL type stored in public.eql_v4_bigint_eq: the bigint family with the eq index. -// The engine produces it from a int64. +// The engine cannot produce it yet: the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade. type BigintEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. diff --git a/languages/golang/encrypt/eql/eql_test.go b/languages/golang/encrypt/eql/eql_test.go index c5b1ca9c1..92ac9ea5a 100644 --- a/languages/golang/encrypt/eql/eql_test.go +++ b/languages/golang/encrypt/eql/eql_test.go @@ -19,7 +19,7 @@ func TestTypesTableNamesTheProducibleTypes(t *testing.T) { t.Errorf("%s: an unproducible type has a reason", typ.Name) } } - want := []string{"Integer", "IntegerEq", "Bigint", "BigintEq", "Text", "TextEq", "Boolean", "Real", "Double"} + want := []string{"Integer", "Bigint", "Text", "TextEq", "Boolean", "Real", "Double"} if len(producible) != len(want) { t.Fatalf("producible = %v, want %v", producible, want) } @@ -28,7 +28,7 @@ func TestTypesTableNamesTheProducibleTypes(t *testing.T) { t.Fatalf("producible = %v, want %v", producible, want) } } - for name, plaintext := range map[string]string{"IntegerEq": "int32", "BigintEq": "int64", "Boolean": "bool", "Real": "float32", "Double": "float64"} { + for name, plaintext := range map[string]string{"Integer": "int32", "Bigint": "int64", "Boolean": "bool", "Real": "float32", "Double": "float64"} { if typ, ok := eql.Lookup(name); !ok || typ.Plaintext != plaintext { t.Errorf("%s: Plaintext = %q, want %q", name, typ.Plaintext, plaintext) } diff --git a/languages/golang/encrypt/guest/src/targets.rs b/languages/golang/encrypt/guest/src/targets.rs index 4afc43340..83df35d40 100644 --- a/languages/golang/encrypt/guest/src/targets.rs +++ b/languages/golang/encrypt/guest/src/targets.rs @@ -179,7 +179,11 @@ mod tests { .filter(|t| t.producible) .map(|t| t.name.as_str()) .collect(); - assert_eq!(producible, ["TextEq"], "the engine produces TextEq only"); + assert_eq!( + producible, + ["Integer", "Bigint", "Text", "TextEq", "Boolean", "Real", "Double"], + "the engine's producible types, in catalog order" + ); assert!(matches!( resolver().resolve("TextOrdOre"), Err(TargetError::Unproducible { .. }) diff --git a/languages/golang/encrypt/guest/tests/native_ops.rs b/languages/golang/encrypt/guest/tests/native_ops.rs index 5a2f86639..53e0a42f4 100644 --- a/languages/golang/encrypt/guest/tests/native_ops.rs +++ b/languages/golang/encrypt/guest/tests/native_ops.rs @@ -1366,7 +1366,11 @@ fn targets_lists_what_the_build_holds() { .filter(|i| producible(i)) .filter_map(name_of) .collect(); - assert_eq!(producible, ["TextEq"], "the engine produces TextEq only"); + assert_eq!( + producible, + ["Integer", "Bigint", "Text", "TextEq", "Boolean", "Real", "Double"], + "the engine's producible types, in catalog order" + ); } // ============================================================================= diff --git a/packages/eql/crates/eql-bindings/README.md b/packages/eql/crates/eql-bindings/README.md index af8f3ab9f..62fed7069 100644 --- a/packages/eql/crates/eql-bindings/README.md +++ b/packages/eql/crates/eql-bindings/README.md @@ -113,16 +113,18 @@ encryption for the scalar types: | v4 type | Plaintext | | --- | --- | | `Text` | `String` | -| `Integer`, `IntegerEq` | `Int32` | -| `Bigint`, `BigintEq` | `Int64` | +| `Integer` | `Int32` | +| `Bigint` | `Int64` | | `Boolean` | `Bool` | | `Real` | `Float32` | | `Double` | `Float64` | Each seals as vitaminc's tagged leaf, the bytes the value enum (`FfiValue`) seals for the same value, so values written here and by the Go SDK open each -other, and `IntegerEq` / `BigintEq` derive the equality term of the integer -inside. Ordering types wait for vitaminc 0.6's order encodings, float +other. `IntegerEq` and `BigintEq` wait for vitaminc 0.6, whose integer +equality term uses one PRF domain for every width; terms derived under +vitaminc 0.5's per-width domains would stop matching after that upgrade. +Ordering types wait for vitaminc 0.6's order encodings, float equality for its canonical float form, and `Json` for a decision on sealing a document as one leaf. @@ -181,7 +183,7 @@ assumed. Outcomes against today's sources: | Writer | Reader and query producer | Outcome | | --- | --- | --- | -| Rust (`stack-encrypt` profile) | Rust (`stack-encrypt` profile) | **Proven.** `tests/encryption/tests/text_eq.rs`: JSON round trip, both canonical directions, PostgreSQL insert and equality query. | +| Rust (`stack-encrypt` profile) | Rust (`stack-encrypt` profile) | **Proven.** `tests/encryption/tests/text_eq.rs`: JSON round trip, both canonical directions, PostgreSQL insert and equality query. `tests/encryption/tests/v4_types.rs`: the same for the other producible v4 types, in both directions between the typed plaintexts and the value enum. | | JS / FFI (`cipherstash-client` profile) | Rust | **Not supported.** `c` lacks the `stack-encrypt:1:` prefix and is refused loudly on decryption. A Rust probe against such a column is accepted by SQL and matches nothing. | | Rust (`stack-encrypt` profile) | JS / FFI | **Not supported.** The JS reader does not know the `stack-encrypt:1:` prefix and the two profiles' equality terms never match. | diff --git a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs index 95e3cdbc7..348299e72 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs @@ -1,24 +1,28 @@ //! Scalar plaintexts for the v4 number and boolean types. //! //! A Stack Encrypt target encrypts a plaintext through Vitamin C's -//! `Encrypt`, `Decrypt` and `PrfValue` traits, and vitaminc 0.5 implements +//! `Encrypt` and `Decrypt` traits, and vitaminc 0.5 implements //! `Encrypt` for `String` but not for `i32`, `i64`, `bool`, `f32` or `f64`. -//! These newtypes fill that gap for the EQL v4 types: [`Int32`] for -//! `Integer` and `IntegerEq`, [`Int64`] for `Bigint` and `BigintEq`, [`Bool`] -//! for `Boolean`, and [`Float32`] / [`Float64`] for `Real` and `Double`. +//! These newtypes fill that gap for the storage-only EQL v4 types: [`Int32`] +//! for `Integer`, [`Int64`] for `Bigint`, [`Bool`] for `Boolean`, and +//! [`Float32`] / [`Float64`] for `Real` and `Double`. //! //! Each seals as vitaminc's tagged leaf, `[tag] ++ payload` from the frozen //! table in `vitaminc_aead_value::tags`, which is exactly what the value enum //! (`FfiValue`) seals for the same value. So a value written through these //! types and one written by the Go SDK as a plain `int32` field open each -//! other, and an equality term derived from [`Int32`] is the one derived from -//! the `i32` inside it. +//! other. +//! +//! None of them derives an equality term. `IntegerEq` and `BigintEq` are held +//! back until vitaminc 0.6, whose integer equality term uses one PRF domain +//! for every width; a term derived here under vitaminc 0.5's per-width +//! domains would stop matching after that upgrade. //! //! ```rust,ignore //! use eql_bindings::encryption::plaintext::Int32; -//! use eql_bindings::v4::integer::IntegerEq; +//! use eql_bindings::v4::integer::Integer; //! -//! let stored: IntegerEq = keyset.encrypt_as(&Int32(42), column).await?; +//! let stored: Integer = keyset.encrypt_as(&Int32(42), column).await?; //! let opened: Int32 = cipher.decrypt_as(stored, expected).await?; //! ``` //! @@ -31,7 +35,6 @@ use vitaminc_aead_value::tagged::{ TaggedBoolFalse, TaggedBoolTrue, TaggedFloat32, TaggedFloat64, TaggedInt32, TaggedInt64, }; use vitaminc_aead_value::tags; -use vitaminc_prf::{IntoPrfContext, Prf, PrfValue, PrfVisitor}; use vitaminc_protected::{Controlled, Protected}; use zeroize::Zeroize; @@ -103,11 +106,11 @@ macro_rules! fixed_scalar { } fixed_scalar!( - /// The plaintext of `Integer` and `IntegerEq`: a 32-bit signed integer. + /// The plaintext of `Integer`: a 32-bit signed integer. Int32(i32), TaggedInt32, tags::INT32, 4, |b| i32::from_le_bytes(b) ); fixed_scalar!( - /// The plaintext of `Bigint` and `BigintEq`: a 64-bit signed integer. + /// The plaintext of `Bigint`: a 64-bit signed integer. Int64(i64), TaggedInt64, tags::INT64, 8, |b| i64::from_le_bytes(b) ); fixed_scalar!( @@ -120,29 +123,6 @@ fixed_scalar!( Float64(f64), TaggedFloat64, tags::FLOAT64, 8, |b| f64::from_bits(u64::from_le_bytes(b)) ); -/// The equality term of an integer is the term of the integer inside it, so -/// `IntegerEq` and a plain `int32` field with an equality index agree. -macro_rules! prf_by_inner { - ($($name:ident),+) => {$( - impl PrfValue for $name { - fn prf_visit_with_context<'a, P, V, C>( - self, - prf: &P, - context: C, - visitor: V, - ) -> P::Ok - where - P: Prf, - V: PrfVisitor, - C: IntoPrfContext<'a>, - { - self.0.prf_visit_with_context(prf, context, visitor) - } - } - )+}; -} -prf_by_inner!(Int32, Int64); - /// The plaintext of `Boolean`. A boolean is the tag itself: no payload. #[derive(Clone, Copy, PartialEq, Zeroize)] pub struct Bool(pub bool); diff --git a/packages/eql/crates/eql-bindings/src/encryption/targets.rs b/packages/eql/crates/eql-bindings/src/encryption/targets.rs index 57b6602f4..764acb045 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/targets.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/targets.rs @@ -596,11 +596,11 @@ mod tests { "TextEq answers a query" ); assert!( - no_query(target("TextOrdOre")).is_none(), - "an unproducible type is refused as that" + no_query(target("Json")).is_none(), + "an unproducible type without a query twin is refused as unproducible" ); assert!(matches!( - refuse_query("TextOrdOre"), + refuse_query("Json"), TargetError::Unproducible { .. } )); assert!(matches!(refuse_query("Nope"), TargetError::Unknown { .. })); diff --git a/packages/eql/crates/eql-bindings/src/v4/bigint.rs b/packages/eql/crates/eql-bindings/src/v4/bigint.rs index ee0942ed3..cfab73e32 100644 --- a/packages/eql/crates/eql-bindings/src/v4/bigint.rs +++ b/packages/eql/crates/eql-bindings/src/v4/bigint.rs @@ -76,31 +76,10 @@ impl DomainType for Bigint { doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`Int64`](crate::encryption::plaintext::Int64). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Int64) -)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct BigintEq { - #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -265,30 +244,10 @@ impl DomainType for BigintOrdOpe { doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input: [`Int64`](crate::encryption::plaintext::Int64). With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Int64) -)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct BigintEqQuery { - #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub hm: Hmac256, } diff --git a/packages/eql/crates/eql-bindings/src/v4/integer.rs b/packages/eql/crates/eql-bindings/src/v4/integer.rs index 4d885febb..b5d28b105 100644 --- a/packages/eql/crates/eql-bindings/src/v4/integer.rs +++ b/packages/eql/crates/eql-bindings/src/v4/integer.rs @@ -76,31 +76,10 @@ impl DomainType for Integer { doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`Int32`](crate::encryption::plaintext::Int32). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Int32) -)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct IntegerEq { - #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -265,30 +244,10 @@ impl DomainType for IntegerOrdOpe { doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input: [`Int32`](crate::encryption::plaintext::Int32). With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Int32) -)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct IntegerEqQuery { - #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub hm: Hmac256, } diff --git a/packages/eql/crates/eql-bindings/src/v4/targets.rs b/packages/eql/crates/eql-bindings/src/v4/targets.rs index eefa79df6..e89ab754b 100644 --- a/packages/eql/crates/eql-bindings/src/v4/targets.rs +++ b/packages/eql/crates/eql-bindings/src/v4/targets.rs @@ -37,8 +37,10 @@ pub const TARGETS: &[Target] = &[ indexes: &["eq"], query: Some("IntegerEqQuery"), query_sql_domain: Some("eql_v4.query_integer_eq"), - producible: true, - reason: None, + producible: false, + reason: Some( + "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade", + ), }, Target { name: "IntegerOrdOre", @@ -173,8 +175,10 @@ pub const TARGETS: &[Target] = &[ indexes: &["eq"], query: Some("BigintEqQuery"), query_sql_domain: Some("eql_v4.query_bigint_eq"), - producible: true, - reason: None, + producible: false, + reason: Some( + "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade", + ), }, Target { name: "BigintOrdOre", @@ -724,18 +728,9 @@ pub(crate) fn encrypt_named<'a, K: 'static>( "Integer" => { run_target::("Integer", keyset, column, plaintext) } - "IntegerEq" => run_target::( - "IntegerEq", - keyset, - column, - plaintext, - ), "Bigint" => { run_target::("Bigint", keyset, column, plaintext) } - "BigintEq" => { - run_target::("BigintEq", keyset, column, plaintext) - } "Text" => run_target::("Text", keyset, column, plaintext), "TextEq" => { run_target::("TextEq", keyset, column, plaintext) @@ -762,15 +757,9 @@ pub(crate) fn decrypt_named<'a, K: 'static>( "Integer" => { open_target::("Integer", opener, column, stored) } - "IntegerEq" => { - open_target::("IntegerEq", opener, column, stored) - } "Bigint" => { open_target::("Bigint", opener, column, stored) } - "BigintEq" => { - open_target::("BigintEq", opener, column, stored) - } "Text" => open_target::("Text", opener, column, stored), "TextEq" => open_target::("TextEq", opener, column, stored), "Boolean" => { @@ -793,15 +782,6 @@ pub(crate) fn query_named<'a, K: 'static>( plaintext: FfiValue, ) -> Result, K>, TargetError> { match name { - "IntegerEq" => run_target::( - "IntegerEq", - keyset, - column, - plaintext, - ), - "BigintEq" => run_target::( - "BigintEq", keyset, column, plaintext, - ), "TextEq" => { run_target::("TextEq", keyset, column, plaintext) } diff --git a/packages/eql/crates/eql-codegen/src/bindings.rs b/packages/eql/crates/eql-codegen/src/bindings.rs index 196817952..73c2a0774 100644 --- a/packages/eql/crates/eql-codegen/src/bindings.rs +++ b/packages/eql/crates/eql-codegen/src/bindings.rs @@ -334,16 +334,14 @@ fn render_query_struct(eql: EqlVersion, family: &DomainFamily, domain: &Domain) } /// The `(family, domain)` pairs whose generated v4 structs carry the opt-in -/// `stack-encrypt` derives: every storage-only domain and every -/// equality-only domain whose plaintext vitaminc 0.5 encodes and, for -/// equality, derives a term for. Every other stored domain has a reason in +/// `stack-encrypt` derives: every storage-only domain whose plaintext +/// vitaminc 0.5 encodes, and the text equality domain. Integer equality is +/// held back until its PRF domain settles (see [`encryption_gap`]). Every other stored domain has a reason in /// [`encryption_gap`], and a test holds the two together so a new domain /// cannot arrive without either a derive or a reason. pub const ENCRYPTION_DOMAINS: &[(&str, &str)] = &[ ("integer", ""), - ("integer", "eq"), ("bigint", ""), - ("bigint", "eq"), ("text", ""), ("text", "eq"), ("boolean", ""), @@ -403,6 +401,10 @@ pub fn encryption_gap( "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)" } else if domain.terms.contains(&Term::Bloom) { "the engine derives match terms, but no EQL type is built from them yet" + } else if matches!(family.name, "integer" | "bigint") && domain.terms == [Term::Hm] { + "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF \ + domain; producing it before that release would leave stored terms unmatchable after \ + the upgrade" } else if matches!(family.name, "real" | "double") { "vitaminc 0.5 derives no equality term for a float; the canonical float form \ arrives in vitaminc 0.6 (ADR-0002)" @@ -1723,6 +1725,8 @@ mod tests { "ordering terms" } else if domain.terms.contains(&Term::Bloom) { "match terms" + } else if matches!(family.name, "integer" | "bigint") { + "integer-orderable/v1" } else { assert!( matches!(family.name, "real" | "double"), diff --git a/packages/eql/crates/eql-codegen/src/targets.rs b/packages/eql/crates/eql-codegen/src/targets.rs index 2de603e00..d9e32fd1d 100644 --- a/packages/eql/crates/eql-codegen/src/targets.rs +++ b/packages/eql/crates/eql-codegen/src/targets.rs @@ -406,7 +406,15 @@ mod tests { .find(|f| f.name == name) .expect("family") }; - assert!(gap(family("integer"), "eq").is_none()); + for family_name in ["integer", "bigint"] { + assert!( + gap(family(family_name), "eq") + .unwrap() + .contains("integer-orderable/v1"), + "{family_name}" + ); + assert!(gap(family(family_name), "").is_none(), "{family_name}"); + } assert!(gap(family("integer"), "ord_ore") .unwrap() .contains("ordering terms")); diff --git a/packages/eql/tests/encryption/tests/targets.rs b/packages/eql/tests/encryption/tests/targets.rs index dbf48cae2..97c7b1189 100644 --- a/packages/eql/tests/encryption/tests/targets.rs +++ b/packages/eql/tests/encryption/tests/targets.rs @@ -565,17 +565,7 @@ mod the_table { .collect(); assert_eq!( producible, - [ - "Integer", - "IntegerEq", - "Bigint", - "BigintEq", - "Text", - "TextEq", - "Boolean", - "Real", - "Double" - ], + ["Integer", "Bigint", "Text", "TextEq", "Boolean", "Real", "Double"], ); for row in targets::targets().iter().filter(|t| !t.producible) { let reason = row.reason.expect("an unproducible type has a reason"); @@ -586,6 +576,7 @@ mod the_table { (_, "Ord" | "OrdOre" | "OrdOpe" | "Search" | "SearchOre") => "ordering terms", ("text", "Match") => "match terms", ("real" | "double", "Eq") => "float", + ("integer" | "bigint", "Eq") => "integer-orderable/v1", (family, suffix) => panic!("{}: unexpected {family} {suffix}", row.name), }; assert!(reason.contains(expected), "{}: {reason}", row.name); diff --git a/packages/eql/tests/encryption/tests/v4_types.rs b/packages/eql/tests/encryption/tests/v4_types.rs index 2e8206bac..126df4ac5 100644 --- a/packages/eql/tests/encryption/tests/v4_types.rs +++ b/packages/eql/tests/encryption/tests/v4_types.rs @@ -1,25 +1,27 @@ -//! The EQL v4 types Stack Encrypt produces besides `TextEq`: the integer, -//! bigint, boolean, float and storage-only text types. `Json` is not -//! producible yet: a JSON value seals as a tree of leaves and EQL's `c` -//! holds one. +//! The EQL v4 types Stack Encrypt produces besides `TextEq`: the +//! storage-only integer, bigint, boolean, float and text types. `Json` is not +//! producible yet: a JSON value seals as a tree of leaves and EQL's `c` holds +//! one. `IntegerEq` and `BigintEq` are held back: their equality term moves to +//! vitaminc 0.6's shared integer PRF domain, and terms stored before that +//! would stop matching. //! -//! Each must round-trip through both entry points (the typed `encrypt_as` -//! and the by-name dispatch a Go plan reaches), seal exactly the leaf -//! vitaminc's value enum seals for the same value (what the Go SDK writes for -//! a plain field of that kind), derive the equality term a plain `i32` or -//! `i64` derives, and refuse a value of another kind. +//! Each type must round-trip through both entry points (the typed +//! `encrypt_as` and the by-name dispatch a Go plan reaches), seal exactly the +//! leaf vitaminc's value enum seals for the same value (what the Go SDK writes +//! for a plain field of that kind) and open the leaf the value enum sealed, +//! and refuse a value of another kind. mod common; use eql_bindings::encryption::plaintext::{Bool, Float32, Float64, Int32, Int64}; use eql_bindings::encryption::targets::{self as targets, TargetError}; -use eql_bindings::encryption::NativeCiphertextVisitor as NativeLeaf; -use eql_bindings::v3::terms::Hmac256; +use eql_bindings::encryption::{CiphertextVisitor, NativeCiphertextVisitor as NativeLeaf}; +use eql_bindings::v3::terms::Ciphertext; use eql_bindings::v4::{bigint, boolean, double, integer, real, text}; use eql_bindings::Identifier; use serde::{de::DeserializeOwned, Serialize}; -use stack_encrypt::target::transcode::{Reader, Transcode}; -use stack_encrypt::{Label, NonEmpty}; +use stack_encrypt::target::transcode::{Reader, Visitor}; +use stack_encrypt::{CipherText, Label, NonEmpty}; use vitaminc_aead_value::{FfiValue, ValueKind}; fn column() -> NonEmpty { @@ -61,12 +63,34 @@ fn assert_v4_envelope(stored: &T, keys: &[&str]) { /// the leaf the Go SDK would have sealed for the same value. async fn as_value_enum( cipher: &stack_encrypt::StackCipher, - c: eql_bindings::v3::terms::Ciphertext, + c: Ciphertext, ) -> FfiValue { let native = c.read(NativeLeaf).unwrap(); cipher.decrypt(native, column()).await.unwrap() } +/// The other direction: the `c` a plain field of `value`'s kind gets when the +/// value enum seals it, as a Go SDK field without an EQL type does. +async fn sealed_by_value_enum( + keyset: &stack_encrypt::KeysetCipher<'_, common::Observed>, + value: FfiValue, +) -> Ciphertext { + match keyset.encrypt(value, column()).await.unwrap() { + CipherText::Single(leaf) => CiphertextVisitor.sealed(leaf).unwrap(), + _ => panic!("a scalar seals as one leaf"), + } +} + +/// A storage-only v4 value of type `T` around `c`, for this test's column. +fn stored_as(c: Ciphertext) -> T { + serde_json::from_value(serde_json::json!({ + "v": 4, + "i": {"t": "users", "c": "age"}, + "c": c, + })) + .unwrap() +} + /// Run one named target over `value` and read the stored JSON back as `T`. async fn by_name( keyset: &stack_encrypt::KeysetCipher<'_, common::Observed>, @@ -80,13 +104,39 @@ async fn by_name( serde_json::from_slice(&bytes).unwrap() } +/// Whether two scalar values are the same value, bit for bit for floats. +fn same(a: &FfiValue, b: &FfiValue) -> bool { + match (a, b) { + (FfiValue::Int32(a), FfiValue::Int32(b)) => a == b, + (FfiValue::Int64(a), FfiValue::Int64(b)) => a == b, + (FfiValue::Bool(a), FfiValue::Bool(b)) => a == b, + (FfiValue::Float32(a), FfiValue::Float32(b)) => a.to_bits() == b.to_bits(), + (FfiValue::Float64(a), FfiValue::Float64(b)) => a.to_bits() == b.to_bits(), + (FfiValue::String(a), FfiValue::String(b)) => a.risky_ref() == b.risky_ref(), + _ => false, + } +} + +/// A sample plaintext of each kind a producible type takes. +fn sample(kind: ValueKind) -> FfiValue { + match kind { + ValueKind::Int32 => FfiValue::Int32(-42), + ValueKind::Int64 => FfiValue::Int64(i64::MIN + 1), + ValueKind::Bool => FfiValue::Bool(true), + ValueKind::Float32 => FfiValue::Float32(-0.0), + ValueKind::Float64 => FfiValue::Float64(f64::NAN), + ValueKind::String => FfiValue::String("a private note".into()), + other => panic!("no sample for {other:?}: add one with the type that takes it"), + } +} + #[tokio::test] async fn integers_round_trip_and_seal_the_value_enum_leaf() { let (cipher, _) = common::cipher().await; let keyset = cipher.default_keyset(); for value in [0, 42, -7, i32::MIN, i32::MAX] { - let stored: integer::IntegerEq = keyset.encrypt_as(&Int32(value), column()).await.unwrap(); - assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); + let stored: integer::Integer = keyset.encrypt_as(&Int32(value), column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c"]); let opened: Int32 = cipher .decrypt_as(stored.clone(), column().into()) @@ -98,42 +148,23 @@ async fn integers_round_trip_and_seal_the_value_enum_leaf() { FfiValue::Int32(v) if v == value )); - // The term is a plain i32's, so it matches an `int32` field with an - // equality index written by the Go SDK. - let plain: Hmac256 = keyset - .equality_term(value, column()) - .await - .unwrap() - .read(Hmac256::visitor()) - .unwrap(); - assert_eq!(stored.hm, plain, "{value}: the equality term of the i32"); - - // The query twin derives the same term and carries no ciphertext. - let query: integer::IntegerEqQuery = - keyset.encrypt_as(&Int32(value), column()).await.unwrap(); - assert_eq!(query.hm, stored.hm); - // The by-name dispatch runs the same plan. - let named: integer::IntegerEq = by_name(&keyset, "IntegerEq", FfiValue::Int32(value)).await; - assert_eq!(named.hm, stored.hm); + let named: integer::Integer = by_name(&keyset, "Integer", FfiValue::Int32(value)).await; let opened = targets::decrypt( - "IntegerEq", + "Integer", &cipher, &label(), - &serde_json::to_vec(&stored).unwrap(), + &serde_json::to_vec(&named).unwrap(), ) .unwrap() .await .unwrap(); assert!(matches!(opened, FfiValue::Int32(v) if v == value)); - - let storage: integer::Integer = keyset.encrypt_as(&Int32(value), column()).await.unwrap(); - assert_v4_envelope(&storage, &["v", "i", "c"]); } for value in [0i64, -1, i64::MIN, i64::MAX] { - let stored: bigint::BigintEq = keyset.encrypt_as(&Int64(value), column()).await.unwrap(); - assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); + let stored: bigint::Bigint = keyset.encrypt_as(&Int64(value), column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c"]); let opened: Int64 = cipher .decrypt_as(stored.clone(), column().into()) .await @@ -143,26 +174,27 @@ async fn integers_round_trip_and_seal_the_value_enum_leaf() { as_value_enum(&cipher, stored.c.clone()).await, FfiValue::Int64(v) if v == value )); - let plain: Hmac256 = keyset - .equality_term(value, column()) - .await - .unwrap() - .read(Hmac256::visitor()) - .unwrap(); - assert_eq!(stored.hm, plain, "{value}: the equality term of the i64"); } } +/// Withdrawn until vitaminc 0.6: a later re-enable is a deliberate change to +/// this test, not a side effect of a regenerated table. #[tokio::test] -async fn equal_values_of_different_widths_have_different_terms() { +async fn integer_equality_types_are_held_back_with_their_reason() { let (cipher, _) = common::cipher().await; let keyset = cipher.default_keyset(); - let narrow: integer::IntegerEq = keyset.encrypt_as(&Int32(42), column()).await.unwrap(); - let wide: bigint::BigintEq = keyset.encrypt_as(&Int64(42), column()).await.unwrap(); - assert_ne!( - narrow.hm, wide.hm, - "the term binds the width, as the separate domains do" - ); + for (name, value) in [ + ("IntegerEq", FfiValue::Int32(1)), + ("BigintEq", FfiValue::Int64(1)), + ] { + let target = targets::target(name).expect("in the table"); + assert!(!target.producible, "{name}"); + let error = refused(targets::encrypt(name, &keyset, &label(), value)); + assert!( + matches!(&error, TargetError::Unproducible { reason, .. } if reason.contains("integer-orderable/v1")), + "{name}: {error}" + ); + } } #[tokio::test] @@ -202,6 +234,106 @@ async fn booleans_and_floats_round_trip_as_storage_only_types() { .await .unwrap(); assert_eq!(opened.0.to_bits(), value.to_bits()); + assert!(matches!( + as_value_enum(&cipher, stored.c).await, + FfiValue::Float64(v) if v.to_bits() == value.to_bits() + )); + } +} + +/// The direction `LeafVisitor` and `BoolVisitor` implement: a leaf the value +/// enum sealed, as a plain Go field writes it, opens as the typed plaintext. +#[tokio::test] +async fn a_leaf_the_value_enum_sealed_opens_as_the_typed_plaintext() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + let c = sealed_by_value_enum(&keyset, FfiValue::Int32(-7)).await; + let opened: Int32 = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, -7); + + let c = sealed_by_value_enum(&keyset, FfiValue::Int64(i64::MAX)).await; + let opened: Int64 = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, i64::MAX); + + let c = sealed_by_value_enum(&keyset, FfiValue::Bool(true)).await; + let opened: Bool = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert!(opened.0); + + let c = sealed_by_value_enum(&keyset, FfiValue::Float32(-0.0)).await; + let opened: Float32 = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert_eq!(opened.0.to_bits(), (-0.0f32).to_bits()); + + let c = sealed_by_value_enum(&keyset, FfiValue::Float64(f64::NAN)).await; + let opened: Float64 = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert_eq!(opened.0.to_bits(), f64::NAN.to_bits()); +} + +/// Every producible row of the generated table, through all three by-name +/// entry points: a swapped type or plaintext in the generator fails here. +#[tokio::test] +async fn every_producible_type_dispatches_by_name() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + let producible: Vec<_> = targets::targets().iter().filter(|t| t.producible).collect(); + assert!(!producible.is_empty()); + for target in producible { + let name = target.name; + let kind = target + .plaintext_kind() + .expect("a producible type names its kind"); + let stored = targets::encrypt(name, &keyset, &label(), sample(kind)) + .unwrap() + .await + .unwrap(); + let json: serde_json::Value = serde_json::from_slice(&stored).unwrap(); + assert_eq!(json["v"], 4, "{name}"); + let opened = targets::decrypt(name, &cipher, &label(), &stored) + .unwrap() + .await + .unwrap(); + assert!(same(&opened, &sample(kind)), "{name}: opened another value"); + + match target.query { + None => { + let error = refused(targets::query(name, &keyset, &label(), sample(kind))); + assert!( + matches!(error, TargetError::NoQuery { .. }), + "{name}: {error}" + ); + } + Some(_) => { + let query = targets::query(name, &keyset, &label(), sample(kind)) + .unwrap() + .await + .unwrap(); + let query: serde_json::Value = serde_json::from_slice(&query).unwrap(); + assert!( + query.get("c").is_none(), + "{name}: a query carries no ciphertext" + ); + if target.indexes.contains(&"eq") { + assert_eq!( + query["hm"], json["hm"], + "{name}: the query probes the stored term" + ); + } + } + } } } @@ -238,8 +370,8 @@ async fn a_value_of_another_kind_is_refused_not_converted() { let (cipher, _) = common::cipher().await; let keyset = cipher.default_keyset(); for (name, value, expected) in [ - ("IntegerEq", FfiValue::Int64(1), ValueKind::Int32), - ("BigintEq", FfiValue::Int32(1), ValueKind::Int64), + ("Integer", FfiValue::Int64(1), ValueKind::Int32), + ("Bigint", FfiValue::Int32(1), ValueKind::Int64), ("Boolean", FfiValue::Int32(1), ValueKind::Bool), ("Real", FfiValue::Float64(1.0), ValueKind::Float32), ("Text", FfiValue::Int32(1), ValueKind::String), @@ -282,15 +414,13 @@ fn postgres_stores_and_queries_every_producible_v4_type() { db.batch_execute( "CREATE TEMP TABLE v4_types ( id int PRIMARY KEY, - age public.eql_v4_integer_eq, - balance public.eql_v4_bigint_eq, - age_only public.eql_v4_integer, + age public.eql_v4_integer, + balance public.eql_v4_bigint, active public.eql_v4_boolean, ratio public.eql_v4_real, score public.eql_v4_double, note public.eql_v4_text - ); - CREATE INDEX v4_types_age_idx ON v4_types (eql_v4.eq_term(age));", + );", ) .unwrap(); @@ -299,15 +429,11 @@ fn postgres_stores_and_queries_every_producible_v4_type() { let row = runtime.block_on(async { ( keyset - .encrypt_as::<_, integer::IntegerEq>(&Int32(age), column()) - .await - .unwrap(), - keyset - .encrypt_as::<_, bigint::BigintEq>(&Int64(balance), column()) + .encrypt_as::<_, integer::Integer>(&Int32(age), column()) .await .unwrap(), keyset - .encrypt_as::<_, integer::Integer>(&Int32(age), column()) + .encrypt_as::<_, bigint::Bigint>(&Int64(balance), column()) .await .unwrap(), keyset @@ -330,13 +456,12 @@ fn postgres_stores_and_queries_every_producible_v4_type() { }); db.execute( "INSERT INTO v4_types VALUES ($1, - $2::text::jsonb::public.eql_v4_integer_eq, - $3::text::jsonb::public.eql_v4_bigint_eq, - $4::text::jsonb::public.eql_v4_integer, - $5::text::jsonb::public.eql_v4_boolean, - $6::text::jsonb::public.eql_v4_real, - $7::text::jsonb::public.eql_v4_double, - $8::text::jsonb::public.eql_v4_text)", + $2::text::jsonb::public.eql_v4_integer, + $3::text::jsonb::public.eql_v4_bigint, + $4::text::jsonb::public.eql_v4_boolean, + $5::text::jsonb::public.eql_v4_real, + $6::text::jsonb::public.eql_v4_double, + $7::text::jsonb::public.eql_v4_text)", &[ &id, &json(&row.0), @@ -345,95 +470,75 @@ fn postgres_stores_and_queries_every_producible_v4_type() { &json(&row.3), &json(&row.4), &json(&row.5), - &json(&row.6), ], ) .unwrap(); } - let probe: integer::IntegerEqQuery = runtime - .block_on(async { keyset.encrypt_as(&Int32(42), column()).await }) - .unwrap(); - let ids: Vec = db - .query( - "SELECT id FROM v4_types WHERE age = $1::text::jsonb::eql_v4.query_integer_eq ORDER BY id", - &[&json(&probe)], - ) - .unwrap() - .iter() - .map(|r| r.get(0)) - .collect(); - assert_eq!(ids, [1, 3], "IntegerEq equality finds both 42s"); - - let probe: bigint::BigintEqQuery = runtime - .block_on(async { keyset.encrypt_as(&Int64(20), column()).await }) - .unwrap(); - let ids: Vec = db - .query( - "SELECT id FROM v4_types WHERE balance = $1::text::jsonb::eql_v4.query_bigint_eq", - &[&json(&probe)], - ) - .unwrap() - .iter() - .map(|r| r.get(0)) - .collect(); - assert_eq!(ids, [2], "BigintEq equality"); - - // Every stored value reads back and opens. + // Every stored value reads back from its domain and opens. let row = db .query_one( - "SELECT age::text, active::text, score::text, note::text FROM v4_types WHERE id = 2", + "SELECT age::text, balance::text, active::text, ratio::text, score::text, note::text + FROM v4_types WHERE id = 2", &[], ) .unwrap(); - let opened: Int32 = runtime - .block_on(async { - cipher - .decrypt_as( - serde_json::from_str::(row.get(0)).unwrap(), - column().into(), - ) - .await - }) - .unwrap(); - assert_eq!(opened.0, 7); - let opened: Bool = runtime - .block_on(async { - cipher - .decrypt_as( - serde_json::from_str::(row.get(1)).unwrap(), - column().into(), - ) - .await - }) - .unwrap(); - assert!(opened.0); - let opened: Float64 = runtime - .block_on(async { - cipher - .decrypt_as( - serde_json::from_str::(row.get(2)).unwrap(), - column().into(), - ) - .await - }) - .unwrap(); - assert_eq!(opened.0, 0.25); - let opened: String = runtime - .block_on(async { - cipher - .decrypt_as( - serde_json::from_str::(row.get(3)).unwrap(), - column().into(), - ) - .await - }) - .unwrap(); - assert_eq!(opened, "note"); + let open = |i: usize| -> String { row.get(i) }; + runtime.block_on(async { + let opened: Int32 = cipher + .decrypt_as( + serde_json::from_str::(&open(0)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened.0, 7); + let opened: Int64 = cipher + .decrypt_as( + serde_json::from_str::(&open(1)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened.0, 20); + let opened: Bool = cipher + .decrypt_as( + serde_json::from_str::(&open(2)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert!(opened.0); + let opened: Float32 = cipher + .decrypt_as( + serde_json::from_str::(&open(3)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened.0, 0.5); + let opened: Float64 = cipher + .decrypt_as( + serde_json::from_str::(&open(4)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened.0, 0.25); + let opened: String = cipher + .decrypt_as( + serde_json::from_str::(&open(5)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened, "note"); + }); + // A storage-only column has no equality operator: it is blocked, not // silently compared as jsonb. let err = db - .query("SELECT id FROM v4_types WHERE age_only = age_only", &[]) + .query("SELECT id FROM v4_types WHERE age = age", &[]) .unwrap_err(); let message = err.as_db_error().map(|e| e.message()).unwrap_or_default(); assert!( From 9f2ebd28937603892474ab279322202c915b3630 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 13 Oct 2026 15:00:07 +1100 Subject: [PATCH 4/4] fix(eql-bindings, stashgen): pin the scalar leaves, compare floats by bits, check widths - plaintext.rs: Float32 and Float64 compare by bit pattern, so -0.0 is not 0.0 and a NaN equals itself, the equality a round trip preserves. Unit tests pin each leaf's tag, width and little-endian order through the same reader Decrypt uses, and refuse another tag or width. - The generated plaintext doc line shows without the stack-encrypt feature again; only its link to crate::encryption is feature-gated. - stashgen refuses a field of the right kind and the wrong width (int64 into Integer, float64 into Real) itself, naming both widths, as its README says. - enginetest.Static lists the seven types the engine produces, with their widths. - The eql-bindings README no longer says TextEq is the only producible type. Claude-Session: https://claude.ai/code/session_01VYKAWbd361xDLmqBdLoUcB --- languages/golang/stashgen/engine.go | 14 +- .../golang/stashgen/enginetest/enginetest.go | 69 +++++++-- languages/golang/stashgen/lower_test.go | 47 ++++++ languages/golang/stashgen/refusal_test.go | 2 + packages/eql/crates/eql-bindings/README.md | 3 +- .../eql-bindings/src/encryption/plaintext.rs | 142 ++++++++++++++++-- .../eql/crates/eql-bindings/src/v4/bigint.rs | 6 +- .../eql/crates/eql-bindings/src/v4/boolean.rs | 6 +- .../eql/crates/eql-bindings/src/v4/double.rs | 6 +- .../eql/crates/eql-bindings/src/v4/integer.rs | 6 +- .../eql/crates/eql-bindings/src/v4/real.rs | 6 +- .../eql/crates/eql-bindings/src/v4/text.rs | 18 ++- .../eql/crates/eql-codegen/src/bindings.rs | 35 +++-- 13 files changed, 306 insertions(+), 54 deletions(-) diff --git a/languages/golang/stashgen/engine.go b/languages/golang/stashgen/engine.go index baa20710f..f48cb5ac1 100644 --- a/languages/golang/stashgen/engine.go +++ b/languages/golang/stashgen/engine.go @@ -41,6 +41,11 @@ type EQLType struct { // Reason says why not when it does not. Producible bool Reason string + + // wire is the engine's exact plaintext kind (int32, not just an + // integer), when the engine named it. Plaintext is the coarse kind a + // field's Go type is compared with first; wire then holds the width. + wire record.Kind } // GuestEngine is the engine the SDK embeds: the WASI guest in package @@ -70,7 +75,7 @@ func (e *guestEngine) EQLTypes(ctx context.Context) ([]EQLType, error) { } out := make([]EQLType, 0, len(targets)) for _, t := range targets { - eqlType := EQLType{Name: t.Name, Plaintext: kindOfWire(t.Plaintext), Query: t.Query, Producible: t.Producible, Reason: t.Reason} + eqlType := EQLType{Name: t.Name, Plaintext: kindOfWire(t.Plaintext), Query: t.Query, Producible: t.Producible, Reason: t.Reason, wire: t.Plaintext} for _, term := range t.Indexes { if name, ok := indexOfOutput[term]; ok { eqlType.Indexes = append(eqlType.Indexes, name) @@ -161,6 +166,13 @@ func lowerDeclaration(d Declaration, eqlTypes []EQLType) (*record.Plan, error) { if eqlType.Plaintext != KindOther && eqlType.Plaintext != f.GoType.Kind { return nil, &FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals a %s, and %s is %s", f.EQLType, eqlType.Plaintext, f.GoType, f.GoType.Kind)} } + // The width too: Integer seals an int32 and Bigint an int64, Real + // a float32 and Double a float64. A narrower Go integer widens + // to the kind the generated code seals it as (int8 and int16 + // seal as int32), so the comparison is on that kind. + if eqlType.wire != record.Untyped && rf.Kind != eqlType.wire { + return nil, &FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals %s, and %s seals as %s", f.EQLType, eqlType.wire, f.GoType, rf.Kind)} + } // An EQL value is stored under a table and a column: the struct's // context is the table and the field's name the column, so a // context of two or more segments leaves the field no column, diff --git a/languages/golang/stashgen/enginetest/enginetest.go b/languages/golang/stashgen/enginetest/enginetest.go index 5a817c87a..31e8b8419 100644 --- a/languages/golang/stashgen/enginetest/enginetest.go +++ b/languages/golang/stashgen/enginetest/enginetest.go @@ -1,6 +1,7 @@ // Package enginetest is a static stand-in for the Rust engine in the -// generator's tests. It knows what the engine produces today: the EQL type -// TextEq, and the four indexes with the Go kinds each applies to. +// generator's tests. It knows what the engine produces today: the EQL types +// in eqlTypes, each with the width of value it seals, and the four indexes +// with the Go kinds each applies to. // // It is a test double, not a copy of the rules the SDK ships: the SDK's // generator asks the embedded guest. When the engine learns a type or an @@ -10,6 +11,7 @@ package enginetest import ( "context" "fmt" + "strings" "github.com/cipherstash/stack/languages/golang/stashgen" ) @@ -19,16 +21,47 @@ type Static struct{} var _ stashgen.Engine = Static{} -// EQLTypes returns TextEq, the one EQL type the engine produces today. +// eqlTypes are the EQL types the engine produces today, with the width of +// value each seals ("" when the kind has no width), mirroring the producible +// rows of encrypt/eql's Types. +var eqlTypes = []struct { + stashgen.EQLType + seals string +}{ + {stashgen.EQLType{Name: "Integer", Plaintext: stashgen.KindInt, Producible: true}, "int32"}, + {stashgen.EQLType{Name: "Bigint", Plaintext: stashgen.KindInt, Producible: true}, "int64"}, + {stashgen.EQLType{Name: "Text", Plaintext: stashgen.KindString, Producible: true}, ""}, + {stashgen.EQLType{Name: "TextEq", Plaintext: stashgen.KindString, Indexes: []stashgen.IndexName{stashgen.IndexEquality}, Query: "TextEqQuery", Producible: true}, ""}, + {stashgen.EQLType{Name: "Boolean", Plaintext: stashgen.KindBool, Producible: true}, ""}, + {stashgen.EQLType{Name: "Real", Plaintext: stashgen.KindFloat, Producible: true}, "float32"}, + {stashgen.EQLType{Name: "Double", Plaintext: stashgen.KindFloat, Producible: true}, "float64"}, +} + +// EQLTypes returns the EQL types the engine produces today. func (Static) EQLTypes(context.Context) ([]stashgen.EQLType, error) { - return []stashgen.EQLType{ - {Name: "TextEq", Plaintext: stashgen.KindString, Indexes: []stashgen.IndexName{stashgen.IndexEquality}, Query: "TextEqQuery", Producible: true}, - }, nil + out := make([]stashgen.EQLType, len(eqlTypes)) + for i, t := range eqlTypes { + out[i] = t.EQLType + } + return out, nil +} + +// sealsAs is the width the generated code seals a Go integer or float as: +// int8, int16 and int32 as int32, int and int64 as int64. +func sealsAs(t stashgen.GoType) string { + switch t.Basic { + case "int8", "int16", "int32": + return "int32" + case "int", "int64": + return "int64" + case "float32", "float64": + return t.Basic + } + return "" } // Check applies the engine's rules as they stand today. func (e Static) Check(ctx context.Context, d stashgen.Declaration) error { - eqlTypes, _ := e.EQLTypes(ctx) for _, f := range d.Fields { if f.Verb == stashgen.VerbOmit { continue @@ -39,17 +72,23 @@ func (e Static) Check(ctx context.Context, d stashgen.Declaration) error { } } if f.Verb == stashgen.VerbEncryptInto { - var found *stashgen.EQLType - for i := range eqlTypes { - if eqlTypes[i].Name == f.EQLType { - found = &eqlTypes[i] + var names []string + found := -1 + for i, t := range eqlTypes { + names = append(names, t.Name) + if t.Name == f.EQLType { + found = i } } - if found == nil { - return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("the engine cannot produce the EQL type %s yet; it produces TextEq", f.EQLType)} + if found < 0 { + return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("the engine cannot produce the EQL type %s yet; it produces %s", f.EQLType, strings.Join(names, ", "))} + } + t := eqlTypes[found] + if t.Plaintext != f.GoType.Kind { + return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals a %s, and %s is %s", f.EQLType, t.Plaintext, f.GoType, f.GoType.Kind)} } - if found.Plaintext != f.GoType.Kind { - return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals a %s, and %s is %s", f.EQLType, found.Plaintext, f.GoType, f.GoType.Kind)} + if t.seals != "" && sealsAs(f.GoType) != t.seals { + return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals %s, and %s seals as %s", f.EQLType, t.seals, f.GoType, sealsAs(f.GoType))} } } for _, idx := range f.Indexes { diff --git a/languages/golang/stashgen/lower_test.go b/languages/golang/stashgen/lower_test.go index 140776870..6a5101128 100644 --- a/languages/golang/stashgen/lower_test.go +++ b/languages/golang/stashgen/lower_test.go @@ -3,6 +3,8 @@ package stashgen import ( "strings" "testing" + + "github.com/cipherstash/stack/languages/golang/internal/record" ) // A struct that names an EQL type beside a context_field has no table of @@ -28,3 +30,48 @@ func TestLoweringRefusesAnEQLTypeBesideAContextField(t *testing.T) { t.Fatalf("lowerDeclaration = %+v, %v", plan, err) } } + +// An EQL type seals one width of its kind. A Go field of the same kind and +// another width is refused here, naming both, rather than by the engine's +// generic refusal; a narrower integer that seals as the type's width passes. +func TestLoweringRefusesAnotherWidthOfTheKind(t *testing.T) { + eql := []EQLType{ + {Name: "Integer", Plaintext: KindInt, Producible: true, wire: record.Int32}, + {Name: "Bigint", Plaintext: KindInt, Producible: true, wire: record.Int64}, + {Name: "Real", Plaintext: KindFloat, Producible: true, wire: record.Float32}, + {Name: "Double", Plaintext: KindFloat, Producible: true, wire: record.Float64}, + } + field := func(basic string, kind Kind, eqlType string) Declaration { + return Declaration{Type: "Row", Context: "rows", Fields: []Field{ + {Name: "v", GoName: "V", GoType: GoType{Name: basic, Kind: kind, Basic: basic}, Verb: VerbEncryptInto, EQLType: eqlType}, + }} + } + for _, c := range []struct { + basic, eqlType, want string + kind Kind + }{ + {"int", "Integer", "Integer seals int32, and int seals as int64", KindInt}, + {"int64", "Integer", "Integer seals int32, and int64 seals as int64", KindInt}, + {"int32", "Bigint", "Bigint seals int64, and int32 seals as int32", KindInt}, + {"float64", "Real", "Real seals float32, and float64 seals as float64", KindFloat}, + {"float32", "Double", "Double seals float64, and float32 seals as float32", KindFloat}, + } { + _, err := lowerDeclaration(field(c.basic, c.kind, c.eqlType), eql) + fe, ok := err.(*FieldError) + if !ok || fe.Field != "V" || fe.Reason != c.want { + t.Errorf("%s into %s: lowerDeclaration = %v, want %q", c.basic, c.eqlType, err, c.want) + } + } + for _, c := range []struct { + basic, eqlType string + kind Kind + }{ + {"int32", "Integer", KindInt}, {"int16", "Integer", KindInt}, {"int8", "Integer", KindInt}, + {"int64", "Bigint", KindInt}, {"int", "Bigint", KindInt}, + {"float32", "Real", KindFloat}, {"float64", "Double", KindFloat}, + } { + if _, err := lowerDeclaration(field(c.basic, c.kind, c.eqlType), eql); err != nil { + t.Errorf("%s into %s: %v", c.basic, c.eqlType, err) + } + } +} diff --git a/languages/golang/stashgen/refusal_test.go b/languages/golang/stashgen/refusal_test.go index b962c0084..2ab440c56 100644 --- a/languages/golang/stashgen/refusal_test.go +++ b/languages/golang/stashgen/refusal_test.go @@ -80,6 +80,8 @@ func TestRefusals(t *testing.T) { {"a _ field with no context", user("\t_ struct{}\n\tEmail string `stash:\"email,encrypt\"`"), stashgen.Request{Type: "User"}, "_", "carries the context tag"}, {"an index that does not apply to the field type", user(ctx + "\tAge int32 `stash:\"age,encrypt,index=match\"`"), stashgen.Request{Type: "User"}, "Age", "match applies to a string, not to int32"}, {"an EQL type that does not apply to the field type", user(ctx + "\tAge int64 `stash:\"age,encrypt_into=TextEq\"`"), stashgen.Request{Type: "User"}, "Age", "TextEq seals a string, and int64 is int"}, + {"an EQL type of another width", user(ctx + "\tAge int64 `stash:\"age,encrypt_into=Integer\"`"), stashgen.Request{Type: "User"}, "Age", "Integer seals int32, and int64 seals as int64"}, + {"a float of another width", user(ctx + "\tScore float64 `stash:\"score,encrypt_into=Real\"`"), stashgen.Request{Type: "User"}, "Score", "Real seals float32, and float64 seals as float64"}, {"a field type the engine cannot seal", user(ctx + "\tDone chan int `stash:\"done,encrypt\"`"), stashgen.Request{Type: "User"}, "Done", "seals only as part of an opaque struct"}, {"a composite sealed outside an opaque struct", user(ctx + "\tAt time.Time `stash:\"at,encrypt\"`"), stashgen.Request{Type: "User"}, "At", "a time.Time seals only as part of an opaque struct"}, {"a slice sealed outside an opaque struct", user(ctx + "\tTags []string `stash:\"tags,encrypt\"`"), stashgen.Request{Type: "User"}, "Tags", "seals only as part of an opaque struct"}, diff --git a/packages/eql/crates/eql-bindings/README.md b/packages/eql/crates/eql-bindings/README.md index 62fed7069..86c2a9a8c 100644 --- a/packages/eql/crates/eql-bindings/README.md +++ b/packages/eql/crates/eql-bindings/README.md @@ -235,7 +235,8 @@ crate would need stack-encrypt API newer than the crates.io release the published crate names, and the guest already depends on both. A type is producible exactly when its generated struct carries the `stack-encrypt` derives (`ENCRYPTION_DOMAINS` in `eql-codegen`), because the dispatch runs the derived -plan; `TextEq` is the only one today. +plan; `targets::targets()` lists them, each with `producible` and, when it is +`false`, the reason. ### Developing the `stack-encrypt` feature diff --git a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs index 348299e72..610a7ad9b 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs @@ -56,12 +56,30 @@ impl<'c, T: Send> DecipherVisitor<'c> for LeafVisitor { } } +/// Bytes a [`LeafVisitor`] reads back: one tag, then exactly `N` payload +/// bytes. Anything else is not this kind. +fn leaf_payload(payload: &[u8]) -> Option<[u8; N]> { + payload.try_into().ok() +} + macro_rules! fixed_scalar { - ($(#[$doc:meta])* $name:ident($inner:ty), $tagged:ty, $tag:expr, $width:literal, |$bytes:ident| $decode:expr) => { + ($(#[$doc:meta])* $name:ident($inner:ty), $tagged:ty, $tag:expr, $width:literal, |$bytes:ident| $decode:expr, |$a:ident, $b:ident| $eq:expr) => { $(#[$doc])* - #[derive(Clone, Copy, PartialEq, Zeroize)] + #[derive(Clone, Copy, Zeroize)] pub struct $name(pub $inner); + /// Equal when the sealed payloads would be: by value for an integer, + /// by bit pattern for a float, so `-0.0 != 0.0` and a NaN equals + /// itself. That is the equality a round trip preserves. + impl PartialEq for $name { + fn eq(&self, other: &Self) -> bool { + let ($a, $b) = (self.0, other.0); + $eq + } + } + + impl Eq for $name {} + impl From<$inner> for $name { fn from(value: $inner) -> Self { Self(value) @@ -90,16 +108,20 @@ macro_rules! fixed_scalar { D: Decipher<'c>, A: IntoAad<'a>, { - decipher.decrypt_any( - LeafVisitor { - tag: $tag, - parse: |payload| { - let $bytes: [u8; $width] = payload.try_into().ok()?; - Some($name($decode)) - }, + decipher.decrypt_any(Self::visitor(), aad) + } + } + + impl $name { + /// The reader [`Decrypt`] opens this type's leaf with. + fn visitor() -> LeafVisitor { + LeafVisitor { + tag: $tag, + parse: |payload| { + let $bytes = leaf_payload::<$width>(payload)?; + Some($name($decode)) }, - aad, - ) + } } } }; @@ -107,24 +129,26 @@ macro_rules! fixed_scalar { fixed_scalar!( /// The plaintext of `Integer`: a 32-bit signed integer. - Int32(i32), TaggedInt32, tags::INT32, 4, |b| i32::from_le_bytes(b) + Int32(i32), TaggedInt32, tags::INT32, 4, |b| i32::from_le_bytes(b), |a, b| a == b ); fixed_scalar!( /// The plaintext of `Bigint`: a 64-bit signed integer. - Int64(i64), TaggedInt64, tags::INT64, 8, |b| i64::from_le_bytes(b) + Int64(i64), TaggedInt64, tags::INT64, 8, |b| i64::from_le_bytes(b), |a, b| a == b ); fixed_scalar!( /// The plaintext of `Real`: a 32-bit float, sealed by its bit pattern, so /// `-0.0` and a NaN's payload survive a round trip. - Float32(f32), TaggedFloat32, tags::FLOAT32, 4, |b| f32::from_bits(u32::from_le_bytes(b)) + Float32(f32), TaggedFloat32, tags::FLOAT32, 4, |b| f32::from_bits(u32::from_le_bytes(b)), + |a, b| a.to_bits() == b.to_bits() ); fixed_scalar!( /// The plaintext of `Double`: a 64-bit float, sealed by its bit pattern. - Float64(f64), TaggedFloat64, tags::FLOAT64, 8, |b| f64::from_bits(u64::from_le_bytes(b)) + Float64(f64), TaggedFloat64, tags::FLOAT64, 8, |b| f64::from_bits(u64::from_le_bytes(b)), + |a, b| a.to_bits() == b.to_bits() ); /// The plaintext of `Boolean`. A boolean is the tag itself: no payload. -#[derive(Clone, Copy, PartialEq, Zeroize)] +#[derive(Clone, Copy, PartialEq, Eq, Zeroize)] pub struct Bool(pub bool); impl From for Bool { @@ -177,3 +201,89 @@ impl<'c> Decrypt<'c> for Bool { decipher.decrypt_any(BoolVisitor, aad) } } + +#[cfg(test)] +mod tests { + //! The leaf each type reads back, byte for byte. Sealing goes through + //! vitaminc's own `Tagged*` types; what is hand-written here is the + //! reading, so these pin its tag, width and byte order. The round trip + //! against the value enum (`FfiValue`) is in the encryption test crate + //! (`tests/encryption/tests/v4_types.rs`). + use super::*; + + fn leaf(tag: u8, payload: &[u8]) -> Protected> { + let mut bytes = vec![tag]; + bytes.extend_from_slice(payload); + Protected::new(bytes) + } + + fn read<'c, V: DecipherVisitor<'c>>(visitor: V, bytes: Protected>) -> Option { + visitor.visit_bytes_vec(bytes).ok() + } + + #[test] + fn integers_read_their_own_little_endian_leaf() { + let (int32, int64) = (Int32::visitor, Int64::visitor); + for v in [0, 1, -1, 42, i32::MIN, i32::MAX] { + assert!(read(int32(), leaf(tags::INT32, &v.to_le_bytes())) == Some(Int32(v))); + } + for v in [0, -1, 42, i64::MIN, i64::MAX] { + assert!(read(int64(), leaf(tags::INT64, &v.to_le_bytes())) == Some(Int64(v))); + } + // Little-endian, not big: 1 is 01 00 00 00. + assert!(read(int32(), leaf(tags::INT32, &[1, 0, 0, 0])) == Some(Int32(1))); + } + + #[test] + fn a_leaf_of_another_tag_or_width_is_refused() { + let (int32, int64) = (Int32::visitor, Int64::visitor); + // An Int64 leaf is not an Int32, even when its value would fit. + assert!(read(int32(), leaf(tags::INT64, &1i64.to_le_bytes())).is_none()); + assert!(read(int64(), leaf(tags::INT32, &1i32.to_le_bytes())).is_none()); + // The right tag with the wrong width. + assert!(read(int32(), leaf(tags::INT32, &1i64.to_le_bytes())).is_none()); + assert!(read(int32(), leaf(tags::INT32, &[1, 0, 0])).is_none()); + assert!(read(int64(), leaf(tags::INT64, &1i32.to_le_bytes())).is_none()); + // No tag at all. + assert!(read(int32(), Protected::new(Vec::new())).is_none()); + // A float's leaf is not an integer's. + assert!(read(int32(), leaf(tags::FLOAT32, &1f32.to_le_bytes())).is_none()); + } + + #[test] + fn floats_read_their_bit_pattern() { + let (f32s, f64s) = (Float32::visitor, Float64::visitor); + let nan32 = f32::from_bits(0x7fc0_1234); + for v in [0.0, -0.0, 1.5, f32::INFINITY, f32::NEG_INFINITY, nan32] { + let opened = read(f32s(), leaf(tags::FLOAT32, &v.to_bits().to_le_bytes())).unwrap(); + assert_eq!(opened.0.to_bits(), v.to_bits()); + } + let nan64 = f64::from_bits(0x7ff8_0000_0000_beef); + for v in [0.0, -0.0, 1.5, f64::INFINITY, nan64] { + let opened = read(f64s(), leaf(tags::FLOAT64, &v.to_bits().to_le_bytes())).unwrap(); + assert_eq!(opened.0.to_bits(), v.to_bits()); + } + assert!(read(f64s(), leaf(tags::FLOAT32, &1f32.to_le_bytes())).is_none()); + assert!(read(f32s(), leaf(tags::FLOAT32, &1f64.to_le_bytes())).is_none()); + } + + #[test] + fn booleans_are_the_tag_alone() { + assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_TRUE])) == Some(Bool(true))); + assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_FALSE])) == Some(Bool(false))); + // A payload after the tag, or another tag, is not a boolean. + assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_TRUE, 0])).is_none()); + assert!(read(BoolVisitor, leaf(tags::INT32, &1i32.to_le_bytes())).is_none()); + assert!(read(BoolVisitor, Protected::new(Vec::new())).is_none()); + } + + #[test] + fn float_equality_is_by_bit_pattern() { + assert!(Float32(-0.0) != Float32(0.0)); + assert!(Float64(-0.0) != Float64(0.0)); + assert!(Float32(f32::NAN) == Float32(f32::NAN)); + assert!(Float64(f64::NAN) == Float64(f64::NAN)); + assert!(Float64(f64::from_bits(0x7ff8_0000_0000_0001)) != Float64(f64::NAN)); + assert!(Int32(7) == Int32(7) && Int64(7) != Int64(8)); + } +} diff --git a/packages/eql/crates/eql-bindings/src/v4/bigint.rs b/packages/eql/crates/eql-bindings/src/v4/bigint.rs index cfab73e32..6059c0931 100644 --- a/packages/eql/crates/eql-bindings/src/v4/bigint.rs +++ b/packages/eql/crates/eql-bindings/src/v4/bigint.rs @@ -14,11 +14,15 @@ use ts_rs::TS; doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr(doc, doc = "")] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Plaintext input and decrypted output: [`Int64`](crate::encryption::plaintext::Int64). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." )] +#[cfg_attr( + all(doc, not(feature = "stack-encrypt")), + doc = " Plaintext input and decrypted output: `Int64`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." diff --git a/packages/eql/crates/eql-bindings/src/v4/boolean.rs b/packages/eql/crates/eql-bindings/src/v4/boolean.rs index c9983ab0e..be25bdbf2 100644 --- a/packages/eql/crates/eql-bindings/src/v4/boolean.rs +++ b/packages/eql/crates/eql-bindings/src/v4/boolean.rs @@ -14,11 +14,15 @@ use ts_rs::TS; doc, doc = " See the [EQL boolean reference](https://cipherstash.com/docs/reference/eql/booleans) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr(doc, doc = "")] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Plaintext input and decrypted output: [`Bool`](crate::encryption::plaintext::Bool). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." )] +#[cfg_attr( + all(doc, not(feature = "stack-encrypt")), + doc = " Plaintext input and decrypted output: `Bool`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." diff --git a/packages/eql/crates/eql-bindings/src/v4/double.rs b/packages/eql/crates/eql-bindings/src/v4/double.rs index e69c39c35..ee52d3ce3 100644 --- a/packages/eql/crates/eql-bindings/src/v4/double.rs +++ b/packages/eql/crates/eql-bindings/src/v4/double.rs @@ -14,11 +14,15 @@ use ts_rs::TS; doc, doc = " See the [EQL double reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr(doc, doc = "")] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Plaintext input and decrypted output: [`Float64`](crate::encryption::plaintext::Float64). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." )] +#[cfg_attr( + all(doc, not(feature = "stack-encrypt")), + doc = " Plaintext input and decrypted output: `Float64`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." diff --git a/packages/eql/crates/eql-bindings/src/v4/integer.rs b/packages/eql/crates/eql-bindings/src/v4/integer.rs index b5d28b105..508595305 100644 --- a/packages/eql/crates/eql-bindings/src/v4/integer.rs +++ b/packages/eql/crates/eql-bindings/src/v4/integer.rs @@ -14,11 +14,15 @@ use ts_rs::TS; doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr(doc, doc = "")] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Plaintext input and decrypted output: [`Int32`](crate::encryption::plaintext::Int32). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." )] +#[cfg_attr( + all(doc, not(feature = "stack-encrypt")), + doc = " Plaintext input and decrypted output: `Int32`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." diff --git a/packages/eql/crates/eql-bindings/src/v4/real.rs b/packages/eql/crates/eql-bindings/src/v4/real.rs index 389153d5a..01f12b7da 100644 --- a/packages/eql/crates/eql-bindings/src/v4/real.rs +++ b/packages/eql/crates/eql-bindings/src/v4/real.rs @@ -14,11 +14,15 @@ use ts_rs::TS; doc, doc = " See the [EQL real reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr(doc, doc = "")] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Plaintext input and decrypted output: [`Float32`](crate::encryption::plaintext::Float32). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." )] +#[cfg_attr( + all(doc, not(feature = "stack-encrypt")), + doc = " Plaintext input and decrypted output: `Float32`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." diff --git a/packages/eql/crates/eql-bindings/src/v4/text.rs b/packages/eql/crates/eql-bindings/src/v4/text.rs index 317ebf763..28ae34d1b 100644 --- a/packages/eql/crates/eql-bindings/src/v4/text.rs +++ b/packages/eql/crates/eql-bindings/src/v4/text.rs @@ -14,11 +14,15 @@ use ts_rs::TS; doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr(doc, doc = "")] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Plaintext input and decrypted output: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." )] +#[cfg_attr( + all(doc, not(feature = "stack-encrypt")), + doc = " Plaintext input and decrypted output: `String`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." @@ -73,11 +77,15 @@ impl DomainType for Text { doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr(doc, doc = "")] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Plaintext input and decrypted output: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." )] +#[cfg_attr( + all(doc, not(feature = "stack-encrypt")), + doc = " Plaintext input and decrypted output: `String`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." +)] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." @@ -392,11 +400,15 @@ impl DomainType for TextSearch { doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] +#[cfg_attr(doc, doc = "")] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Plaintext input: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." )] +#[cfg_attr( + all(doc, not(feature = "stack-encrypt")), + doc = " Plaintext input: `String`. With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." +)] #[cfg_attr( all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." diff --git a/packages/eql/crates/eql-codegen/src/bindings.rs b/packages/eql/crates/eql-codegen/src/bindings.rs index 73c2a0774..e0a100094 100644 --- a/packages/eql/crates/eql-codegen/src/bindings.rs +++ b/packages/eql/crates/eql-codegen/src/bindings.rs @@ -434,23 +434,32 @@ fn encryption_attrs( let (_, rust) = plaintext_of(family.name).expect("a derived family names its plaintext"); let plaintext_ty: syn::Type = syn::parse_str(rust).expect("plaintext is a type path"); let shown = rust.rsplit("::").next().unwrap_or(rust); - let plaintext = if query { - format!( - " Plaintext input: [`{shown}`]({rust}). With the `stack-encrypt` feature, \ - implements `EncryptFrom<{shown}>` to build equality query terms. Query operands \ - do not decrypt." - ) - } else { - format!( - " Plaintext input and decrypted output: [`{shown}`]({rust}). With the \ - `stack-encrypt` feature, implements `EncryptFrom<{shown}>` and \ - `DecryptInto<{shown}>`." - ) + // The plaintext line shows with or without the feature: it says what the + // type is built from, and the sentence itself names the feature. Only the + // link is feature-gated, since a plaintext from `crate::encryption` does + // not exist without it and the link would not resolve. + let plaintext_doc = |named: &str| { + if query { + format!( + " Plaintext input: {named}. With the `stack-encrypt` feature, \ + implements `EncryptFrom<{shown}>` to build equality query terms. Query operands \ + do not decrypt." + ) + } else { + format!( + " Plaintext input and decrypted output: {named}. With the \ + `stack-encrypt` feature, implements `EncryptFrom<{shown}>` and \ + `DecryptInto<{shown}>`." + ) + } }; + let plaintext = plaintext_doc(&format!("[`{shown}`]({rust})")); + let plaintext_unlinked = plaintext_doc(&format!("`{shown}`")); ( quote! { - #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = "")] + #[cfg_attr(doc, doc = "")] #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = #plaintext)] + #[cfg_attr(all(doc, not(feature = "stack-encrypt")), doc = #plaintext_unlinked)] #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`.")] #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = " See the [complete encryption example](crate::encryption#example).")] #[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))]