Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/eql-plan-field-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@cipherstash/eql': minor
---

**The `eql-bindings` crate resolves an EQL type named as a string to its own Stack Encrypt plan** (`stack-encrypt` feature). `eql_bindings::encryption::targets` carries a catalog-generated table of every EQL type a data plan may name as a field target — its name across languages, family and suffix, the plaintext `ValueKind` it takes, the indexes it carries, its query twin, and whether the engine can produce it today with the reason when not — and `encrypt` / `decrypt` / `query` entry points that dispatch on the name and run the type's derived `EncryptFrom` / `DecryptInto`, resolving to the EQL value's JSON bytes through the engine's `Pending` so a guest batches it with the rest of a plan. This is the EQL half of EQL types as plan field targets (cipherstash/stack#1062): a Go data plan names `TextEq` and the guest returns the finished EQL value instead of assembling one. Every target is an `eql_v4_*` domain. `TextEq` is the only producible type; every other name is refused with the plan's reason. The SQL surface and the TypeScript package are unchanged.
**The `eql-bindings` crate resolves an EQL type named as a string to its own Stack Encrypt plan** (`targets` feature, which turns on `stack-encrypt`). `eql_bindings::encryption::targets` carries a catalog-generated table of every EQL type a data plan may name as a field target — its name across languages, family and suffix, the plaintext `ValueKind` it takes, the indexes it carries, its query twin, and whether the engine can produce it today with the reason when not — and `encrypt` / `decrypt` / `query` entry points that dispatch on the name and run the type's derived `EncryptFrom` / `DecryptInto`, resolving to the EQL value's JSON bytes through the engine's `Pending` so a guest batches it with the rest of a plan. This is the EQL half of EQL types as plan field targets (cipherstash/stack#1062): a Go data plan names `TextEq` and the guest returns the finished EQL value instead of assembling one. Every target is an `eql_v4_*` domain. `TextEq` is the only producible type; every other name is refused with the plan's reason. The SQL surface and the TypeScript package are unchanged.
2 changes: 1 addition & 1 deletion .changeset/eql-target-match-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@cipherstash/eql': minor
---

**`eql-bindings` says which match options an EQL type's filter is built under** (`stack-encrypt` feature). `Target::match_options()` returns `EqlMatch`'s options for a type with a match index (`TextMatch`) and `None` otherwise, so Stack Encrypt can answer `matches` through the type's query twin: it cuts the query's tokens for `verify` exactly as the stored filter was cut. The SQL surface and the TypeScript package are unchanged.
**`eql-bindings` says which match options an EQL type's filter is built under** (`targets` feature). `Target::match_options()` returns `EqlMatch`'s options for a type with a match index (`TextMatch`) and `None` otherwise, so Stack Encrypt can answer `matches` through the type's query twin: it cuts the query's tokens for `verify` exactly as the stored filter was cut. The SQL surface and the TypeScript package are unchanged.
2 changes: 1 addition & 1 deletion .changeset/eql-v4-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
'@cipherstash/eql': minor
---

**The `eql-bindings` crate has an `eql_bindings::v4` module, with the `stack-encrypt` feature: 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, so the module exists only with that feature and its types derive Stack Encrypt's traits without a further gate. 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`, and `TextMatch` (3-grams, `k = 6`, a 2048-bit filter, pinned as `eql_bindings::encryption::EqlMatch`; text shorter than three characters is refused, stored or queried, and only `stored @@ query` matches until #1171 is fixed), from the Rust primitives `i32`, `i64`, `bool`, `f32` and `f64`; the Go SDK's `encrypt/eql` table marks them producible. Stack Encrypt also produces the number equality types `IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq`, whose `=` compares as PostgreSQL compares the plain types: an integer has one term whatever its width, and for floats `-0.0 = 0.0` and `NaN = NaN` (ADR-0002's semantic baseline). Every type it cannot produce yet names the issue that tracks its blocker. The by-name target entry points (`eql_bindings::encryption::targets::encrypt`, `decrypt` and `query`) exchange the plaintext as Stack Encrypt's `dynamic::ValueFrame`, one value as its transport frame, rather than a value tree.
**The `eql-bindings` crate has an `eql_bindings::v4` module, with the `stack-encrypt` feature: 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, so the module exists only with that feature and its types derive Stack Encrypt's traits without a further gate. 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`, and `TextMatch` (3-grams, `k = 6`, a 2048-bit filter, pinned as `eql_bindings::encryption::EqlMatch`; text shorter than three characters is refused, stored or queried, and only `stored @@ query` matches until #1171 is fixed), from the Rust primitives `i32`, `i64`, `bool`, `f32` and `f64`; the Go SDK's `encrypt/eql` table marks them producible. Stack Encrypt also produces the number equality types `IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq`, whose `=` compares as PostgreSQL compares the plain types: an integer has one term whatever its width, and for floats `-0.0 = 0.0` and `NaN = NaN` (ADR-0002's semantic baseline). Every type it cannot produce yet names the issue that tracks its blocker. The by-name target entry points (`eql_bindings::encryption::targets::encrypt`, `decrypt` and `query`) exchange the plaintext as Stack Encrypt's `dynamic::ValueFrame`, one value as its transport frame, rather than a value tree. They, and the generated `v4::targets` table, need the new `targets` feature, which turns on `stack-encrypt` and Stack Encrypt's `dynamic` feature. `stack-encrypt` alone no longer builds Stack Encrypt's `dynamic` layer, so a Rust program using the typed v4 types through `encrypt_as` doesn't compile it.

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.
2 changes: 1 addition & 1 deletion languages/golang/encrypt/guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ stack-kms = { path = "../../../../packages/stack-kms", default-features = false
# The EQL types, for the `eql` build only. By path, like every other
# eql-bindings dependency in the repository: the SQL that stores a payload
# and the Rust that emits it must agree (scripts/lint-no-eql-registry-pins.mjs).
eql-bindings = { path = "../../../../packages/eql/crates/eql-bindings", features = ["stack-encrypt"], optional = true }
eql-bindings = { path = "../../../../packages/eql/crates/eql-bindings", features = ["targets"], optional = true }
zerokms-protocol = "=0.12.31"

# The transport frame and ciphertext codec, shared with vitaminc's own
Expand Down
14 changes: 10 additions & 4 deletions packages/eql/crates/eql-bindings/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Encrypt produces an EQL v4 value (ADR-0002), so the module is compiled with
that feature alone, and its producible types derive `EncryptFrom` and
`DecryptInto` with no attribute of their own gated on it.
- **A `targets` feature for the by-name dispatch.** `encryption::targets` and
`v4::targets`, which name an EQL type at run time and take a `ValueFrame`,
need `targets`, which turns on `stack-encrypt` and Stack Encrypt's
`dynamic` feature. `stack-encrypt` alone builds Stack Encrypt with no
default features and no `dynamic` layer, so a Rust program that encrypts
the v4 types through `encrypt_as` no longer compiles it.
- **`IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq` are producible**
(`stack-encrypt` feature), from `i32`, `i64`, `f32` and `f64`. Their equality term follows ADR-0002's semantic
baseline, so `=` compares as PostgreSQL's own integer and `float8 =` do:
one term per integer value whatever its width, `-0.0 = 0.0` and
`NaN = NaN`. The ciphertext keeps the exact value.
- **`Target::match_options`** (`stack-encrypt` feature): the options a type's
- **`Target::match_options`** (`targets` feature): the options a type's
match index is built under, `EqlMatch`'s, for a type that carries one
(`TextMatch`), and `None` otherwise. Stack Encrypt answers `matches`
through the type with them, cutting the query's tokens for `verify` as the
stored filter was cut.
- **EQL types as plan field targets, by name** (`stack-encrypt` feature).
- **EQL types as plan field targets, by name** (`targets` feature).
`eql_bindings::encryption::targets` holds a catalog-generated table
(`targets()`) of every EQL type a Stack Encrypt data plan may name as a
field target — name, family, suffix, plaintext `ValueKind`, SQL domain,
Expand Down Expand Up @@ -71,8 +77,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **The `stack-encrypt` feature builds on vitaminc ADR 0001.**
`encryption::targets` takes `ValueFrame` and `ValueKind` from
`stack_encrypt::dynamic` (the feature turns on stack-encrypt's `dynamic`
feature) in place of `vitaminc-aead-value`, which is gone, and the
`stack_encrypt::dynamic` (the `targets` feature turns on stack-encrypt's
`dynamic` feature) in place of `vitaminc-aead-value`, which is gone, and the
`Ciphertext` target takes any vitaminc `Encrypt` plaintext and opens into any
`Decrypt + Send` type, as stack-encrypt's own targets do.

Expand Down
31 changes: 22 additions & 9 deletions packages/eql/crates/eql-bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,42 @@ schemars = "1"
# from the PACKAGED crate against the registry stack-encrypt instead:
# `cargo publish --dry-run --all-features` in test-eql.yml at PR time, and
# `publish_all_features` in ../../release-plz.toml at release time.
# `dynamic`: `ValueFrame` / `ValueKind`, the runtime plaintext a plan field
# target takes and the kind names a plan's `"type"` key spells
# (`encryption::targets`).
stack-encrypt = { path = "../../../stack-encrypt", version = "0.3.0", optional = true, default-features = false, features = ["dynamic"] }
# 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.
# No default features and no `dynamic`: the typed EQL v4 types need only
# Stack Encrypt's typed API. The `targets` feature turns `dynamic` on.
stack-encrypt = { path = "../../../stack-encrypt", version = "0.3.0", optional = true, default-features = false }
# `targets` only. 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 }
# `targets` only: `TargetError`.
thiserror = { version = "2", optional = true }
base64 = { version = "0.22", optional = true }
hex = { version = "0.4", optional = true }

[features]
default = []
# The EQL v4 types as Stack Encrypt targets: `eql_bindings::v4`, with
# `EncryptFrom` / `DecryptInto` on every producible type, for a Rust program
# that encrypts through `encrypt_as`. No `dynamic` layer.
stack-encrypt = [
"dep:stack-encrypt",
"dep:zeroize",
"dep:thiserror",
"dep:base64",
"dep:hex",
]
# The by-name dispatch a language binding needs (`encryption::targets`,
# `v4::targets`): an EQL type named at run time, a `ValueFrame` plaintext,
# and the target table a guest serializes. Turns on stack-encrypt's
# `dynamic` feature. The Go guest's `eql` build and the encryption tests
# enable it.
targets = [
"stack-encrypt",
"stack-encrypt/dynamic",
"dep:zeroize",
"dep:thiserror",
]

[package.metadata.docs.rs]
features = ["stack-encrypt"]
features = ["targets"]

[dev-dependencies]
# Parity oracle: tests/catalog_parity.rs asserts the v3 domain inventory
Expand Down
13 changes: 12 additions & 1 deletion packages/eql/crates/eql-bindings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ let opened: ValueFrame = targets::decrypt("TextEq", &cipher, &column, &stored)?.
```

The table and the dispatch are generated into `src/v4/targets.rs` beside
the v4 family modules (`mise run types:generate`), gated to the `stack-encrypt`
the v4 family modules (`mise run types:generate`), gated to the `targets`
feature, and drift-gated by the same parity tests. The same generator writes
the Go package `languages/golang/encrypt/eql` from the same rows
(`eql-codegen go-eql`, also under `types:generate` / `types:check`). The
Expand All @@ -248,6 +248,17 @@ when its generated struct carries the `stack-encrypt` derives
plan; `targets::targets()` lists them, each with `producible` and, when it is
`false`, the reason.

### Features

- `stack-encrypt`: the EQL v4 types (`eql_bindings::v4`) with Stack
Encrypt's `EncryptFrom` and `DecryptInto`, for a Rust program that
encrypts through `encrypt_as`. It builds Stack Encrypt with no default
features and without its `dynamic` layer.
- `targets`: `stack-encrypt`, plus the by-name dispatch a language binding
needs (`encryption::targets`, `v4::targets`), where the EQL type is named at
run time and the plaintext is a `ValueFrame`. It turns on Stack Encrypt's
`dynamic` feature. The Go guest's `eql` build enables it.

### Developing the `stack-encrypt` feature

Stack Encrypt lives in this repository (`packages/stack-encrypt`), and the
Expand Down
16 changes: 12 additions & 4 deletions packages/eql/crates/eql-bindings/src/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@
//! builder with `StackCipher::new().await?`, using your configured CipherStash
//! credentials. The encryption and decryption calls are identical.
//!
//! A binding that holds no Rust type to name reaches the same plans by the
//! type's *name* through [`targets`]: the catalog-generated table of EQL
//! types a data plan may name as a field target, and `encrypt` / `decrypt` /
//! `query` dispatching on that name.
#![cfg_attr(
feature = "targets",
doc = "A binding that holds no Rust type to name reaches the same plans by the type's *name* through [`targets`] (`targets` feature): the catalog-generated table of EQL types a data plan may name as a field target, and `encrypt` / `decrypt` / `query` dispatching on that name."
)]
#![cfg_attr(
not(feature = "targets"),
doc = "A binding that holds no Rust type to name reaches the same plans by the type's *name* through `targets`, with the `targets` feature: the catalog-generated table of EQL types a data plan may name as a field target, and `encrypt` / `decrypt` / `query` dispatching on that name."
)]
//!
//! Use the same table and column identifier for writes and queries. Passing
//! `column.into()` on decryption also checks that the stored identifier matches
Expand Down Expand Up @@ -80,6 +84,10 @@
//! require::<TextEq>();
//! ```

// The by-name dispatch over the EQL v4 types (`targets` feature). A plain
// comment: a doc comment here would resolve the module's own links from
// this scope.
#[cfg(feature = "targets")]
pub mod targets;

use base64::{engine::general_purpose::STANDARD, Engine as _};
Expand Down
4 changes: 4 additions & 0 deletions packages/eql/crates/eql-bindings/src/encryption/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
//! [`crate::v4::targets`], beside the v4 family modules, so a type cannot be in the
//! catalog and missing from the table.
//!
//! This module needs the `targets` feature, which also turns on Stack
//! Encrypt's `dynamic` feature. The `stack-encrypt` feature alone gives only
//! the typed EQL v4 types.
//!
//! The guest build that holds the EQL types links this module; the build
//! without them has no `eql-bindings` and refuses a target name before
//! reaching here.
Expand Down
4 changes: 2 additions & 2 deletions packages/eql/crates/eql-bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
doc = "See the [complete encryption example](encryption#example), including cipher setup, table/column identifiers, and decryption."
)]
#![cfg_attr(
feature = "stack-encrypt",
doc = "A binding that names an EQL type as a string reaches the same plans through [`encryption::targets`]: the catalog-generated table of types a data plan may name as a field target, and the by-name `encrypt` / `decrypt` / `query` dispatch."
feature = "targets",
doc = "A binding that names an EQL type as a string reaches the same plans through [`encryption::targets`] (`targets` feature): the catalog-generated table of types a data plan may name as a field target, and the by-name `encrypt` / `decrypt` / `query` dispatch."
)]

use schemars::JsonSchema;
Expand Down
2 changes: 2 additions & 0 deletions packages/eql/crates/eql-bindings/src/v4/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub mod smallint;
/// a field target (`TARGETS`) and the by-name dispatch that runs a
/// producible type's own plan. The descriptor type, the errors and the
/// public entry points are hand-written in [`crate::encryption::targets`].
/// `targets` feature only.
#[cfg(feature = "targets")]
pub mod targets;
pub mod text;
pub mod timestamp;
Expand Down
2 changes: 1 addition & 1 deletion packages/eql/crates/eql-codegen/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ fn render_bindings(dir: &Path) -> Vec<(PathBuf, String)> {
/// Render the v4 bindings: one module per family with a scalar domain (a
/// mixed family's scalar domains under its own name, since v4 has no
/// hand-written SteVec module to sit beside), and the Stack Encrypt target
/// table and dispatch (`targets.rs`, under the `stack-encrypt` feature with
/// table and dispatch (`targets.rs`, declared under the `targets` feature in
/// the hand-written `v4/mod.rs`). Stack Encrypt produces v4 only, so the
/// target table lives here.
fn render_v4_bindings(dir: &Path) -> Vec<(PathBuf, String)> {
Expand Down
2 changes: 1 addition & 1 deletion packages/eql/crates/eql-codegen/src/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! types a Stack Encrypt data plan may name as a field target, and the
//! by-name dispatch that runs a named type's own Rust plan. Generated beside
//! the v4 family modules by the same mechanism ([`crate::bindings`]), and
//! under the `stack-encrypt` feature with the rest of `v4`.
//! under the `targets` feature, inside `v4`.
//! Stack Encrypt produces EQL v4 only (ADR-0002), so every target is an
//! `eql_v4_*` domain.
//!
Expand Down
9 changes: 8 additions & 1 deletion packages/eql/mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,15 @@ run = """
# on purpose. `--locked`: this is a CHECK, and the EQL lockfile is the one the
# lockstep bump moves (see "Working on EQL" in the root AGENTS.md).
set -euo pipefail
# Both feature sets: the typed types alone, as a Rust program builds them,
# and with the by-name dispatch, as the Go guest does.
cargo test --locked -p eql-bindings --features stack-encrypt
cargo test --locked -p eql-bindings --features targets
cargo test --locked -p eql-encryption-tests
cargo clippy --locked -p eql-bindings -p eql-encryption-tests --all-features --all-targets -- -D warnings
Comment thread
coderdan marked this conversation as resolved.
# The typed set alone, linted on its own: a lint that fires only without
# `targets` (an import only the dispatch uses) is invisible to the line above.
cargo clippy --locked -p eql-bindings --features stack-encrypt --all-targets -- -D warnings
"""

[tasks."test:encryption:postgres"]
Expand Down Expand Up @@ -342,13 +348,14 @@ done
"""

[tasks."check:encryption:wasi"]
description = "Compile eql-bindings with the stack-encrypt feature for wasm32-wasip1 (no HTTP), as the Go guest builds it"
description = "Compile eql-bindings for wasm32-wasip1 (no HTTP): the typed stack-encrypt set alone, and with the targets feature as the Go guest builds it"
dir = "{{config_root}}"
run = """
#!/usr/bin/env bash
set -euo pipefail
rustup target add wasm32-wasip1
cargo check --locked -p eql-bindings --features stack-encrypt --target wasm32-wasip1
cargo check --locked -p eql-bindings --features targets --target wasm32-wasip1
Comment thread
coderdan marked this conversation as resolved.
"""

[tasks."types:generate"]
Expand Down
Loading
Loading