Client-side encryption of values under per-value ZeroKMS data keys, and the
derivation of searchable index terms from the same values. Covers
stack-encrypt, stack-encrypt-derive, and the WASI guest in
languages/golang/encrypt/guest that exposes them to Go.
Cipher-directed:
Encryption driven by the value's shape: the value's Encrypt implementation
walks the cipher and the caller decides the context, which may be absent.
Nothing declares an index and no output type is involved. One of the two
preferred entry points for a native Rust caller. A language SDK may leave it
out: the Go SDK seals a whole value through a plan, so that every sealed
value has a declared context.
Avoid: raw path, low-level path
Target-directed:
Encryption driven by the output type: the type being produced (a ciphertext, a
term, a record) declares what it is derived from, which operations produce it,
and which context it demands; execution belongs to the cipher. Rust-only: a
hand-written EncryptFrom type, the combinators, and the derive. With
cipher-directed, the preferred entry point for a native Rust caller. A
binding reaches indexes and field-by-field encryption only through a plan
lowered from data, which runs the same engine.
Avoid: typed path, high-level path
Binding: The FFI or WASI interface between the engine and a target language: the guest's exports and the data that crosses them. A user of that language does not call it. Avoid: SDK, mirror, name binding (that is a keyset name's)
Language SDK: What users of a target language work with day to day. In Go: struct tags, the generator, and the code the generator writes. It reaches the engine only through the binding. Avoid: binding, wrapper, mirror
Declaration: The statement of how each field is encrypted: its context and its indexes. It is what a user of a language SDK writes; in Go, struct tags or a policy. A saved declaration is a plan. Avoid: schema, config, mapping
Operation description: The target's declaration of the ciphertext and term operations, source selections, and context requirements needed to produce it. Avoid: user-supplied encryption callback, plan (a plan is reusable and produces a description each time it runs; a description is one run)
Plan: A saved declaration of how a value is encrypted: its context and its indexes, or, field by field, each field's. It starts as a one-value plan, over one plaintext type, or as a fields plan. Its context comes from exactly one source: the plan itself, the call that runs it, or a context field of the value; two sources is an error, and a caller's context may still extend the one source. Reusable, and validated when it is built (every label plain, no field named twice, no two fields under one identity), so a plan in hand is a valid one. Written by hand as a chain, generated by the derive from a type's attributes, or lowered from data by a binding; the write, the query and the read all take the same plan, so none of them can respell the context. Running a plan produces one operation description. Avoid: schema (that is the source's shape, which a plan does not describe), mapping, config, target (a plan may name a target type for a field)
Target:
A type that decides a field's layout, and so the queries it answers, through
its own EncryptFrom: an EQL type, Encrypted<Terms>, or a type of the
consumer's. A plan field is either plain verbs (encrypt, index, passthrough)
or one target, never both. Rust-only, since it names a Rust type; a binding's
counterpart is generated code (in Go, the generator's output), which
assembles the same layout from the engine's standard outputs.
Avoid: domain (that is EQL's word for a SQL type), registry, kind
Index: A declared derivation of a searchable value from a plaintext — equality, match, ORE, OPE or JSON — named in a plan. A field carries indexes beside its ciphertext, or indexes alone, in which case it is written and searched but never read back unless an index's own output is reversible (the JSON index's is; a term is not). A type on the Rust side, generic over the plaintext, so an index that does not apply to a type (match on an integer) does not compile; data at a binding boundary. An index answers queries by source type: a scalar index answers one, the JSON index answers containment, a path, and a value at a path. Avoid: term (that is what an index produces), output, search type, target (the JSON index replaces the ciphertext but is still an index)
Ciphertext transcoding: Construction or inspection of an encrypted target through its native encrypted structure, preserving the distinctions between ciphertext, terms, metadata, and authenticated structural markers. Avoid: plaintext serialization, re-encryption
Context:
The value a ciphertext is authenticated under and a term is derived under. A
leaf requires a nonempty context, validated by Vitamin C and owned in a
CallerContext (both encodings — what a term is derived under, and what a
record deriving terms threads to every field) or an AeadContext (the AAD
encoding alone — what a ciphertext is sealed and opened under; a record
deriving terms hands its ciphertext fields that half of its CallerContext);
a nonempty!("users").with("email") pair (a table and a column are two
parts, rendered users/email), a NonEmpty::new(value)? at runtime, or a
bare integer. It becomes the ciphertext's associated data,
the term's PRF context, and the ZeroKMS descriptor of the data key.
Avoid: AAD (that is one of its encodings, not the concept), lock context
Own context:
The context a field carries itself: <record context>/<identity>, where the
identity defaults to the field's name and identity = ".." pins it (see
Identity). A caller's context extends it ((("users", "age"), id)); it
is never discarded. A subtree of a declaration is given one with under (the
caller's is then optional) or extend (the caller's stays required).
Avoid: default context, field prefix
Context field: A field of a value whose value is the context of the value's other fields. It is stored as a passthrough field, so the record can be opened without being told the context, and on open each field is checked against it, so a context field changed in storage does not open. One of a plan's three context sources. Avoid: key field, tenant column, scope field
Threaded context:
The one context a target's declaration tree hands to every operation beneath
it (ADR-0004): a type parameter of Encryption, so a target cannot route what
it is handed to one operation and something else to another, and two subtrees
needing different kinds of context do not zip. under and extend are the
only ways to change it; each covers a whole subtree and is written in the
declaration, and the tree does not tell a record's two fields from a target's
two halves.
Avoid: scope (that is a Pending's), shared context, per-operation context
Descriptor:
The context, rendered as the string ZeroKMS binds into every data key and
logs per retrieval, rendered from the context's parts: plain text verbatim,
integers by their width, sign-blind (7u64, and 7i64 is 7u64), a list's
parts joined by / (users/email; a nested list is parenthesised,
(users/email)/7u64); text that could read as another form — containing
/, ( or ), beginning with b64:, a digit or - — is b64:-escaped,
so one text part can never read as two, and an empty part inside a list is
the bare b64:. Rendered by one function, Descriptor::from_piece, and
frozen: a change re-keys everything. Finer than the encodings for a
pre-encoded Aad (opaque bytes) and coarser for shapes that render alike
(7i64 and 7u64): seal and open must present the context in the same
shape. The descriptor is derived, never authored: nothing takes a descriptor
string from a caller.
Avoid: key name, key id, path (that is a Label)
Describe:
The trait of a value whose parts are a descriptor of its own — the identity
data is keyed under, as opposed to an arbitrary context. An implementor
returns its parts as a Description, built from a first part so it is never
empty, and never writes rendered text, so the one renderer keeps distinct
values apart whoever implements it. Open: a consumer's own column or
document type implements it; Label does, and EQL's Identifier does once
stack#971 lands. A Describe type is also a context, through the same parts
(to_context is what its IntoContext returns).
Avoid: descriptor trait, Descriptor (the rendered string), DescriptorBuilder
Label:
The name of the data a value is sealed under (users/email,
documents/v2/body): the first-class Describe type, a path of plain
segments, each checked (non-empty; no /, (, ), control or invisible
format characters; not beginning with b64:, a digit or -), so its Display is its
descriptor and parses back losslessly. A context carries a name and,
optionally, a scope (a tenant, a row id), and each has one spelling: the
name is a Label, a flat list; the scope is with, which appends and nests
((users/email)/7u64). Never build a name with with or put a scope in a
Label. A two-segment label is the context a struct = .. derive binds for
a field, so a label opens a row a derive wrote. An EQL consumer names its
data with an Identifier, a two-segment label.
Avoid: identifier (that is EQL's two-segment case), prefix, column context
Leaf: An output type that authenticates or derives directly — a ciphertext or a single index term — and therefore owes nothing to a context but the one it is handed. Avoid: primitive, scalar output
Record:
An output type assembled from leaves derived from one plaintext (a plaintext = T derive).
Avoid: composite, struct (the plaintext is the struct; the record is
derived from it), row (that is where a database stores a record)
Field-by-field record:
A record whose top-level fields are each sealed under their own context,
<context>/<field>, so a field can be stored, read, indexed and queried
without the others. One level: a nested value is a tree under its field's
label, as encrypt on a nested value is everywhere else. What a struct = T
derive produces and a fields() plan declares.
Avoid: struct record, columns, row, table
Passthrough: A field or leaf carried through encryption as it is: present in the output so the value is whole, neither sealed nor authenticated, so a change to it in storage is not detected. A plan names a passthrough field explicitly; a field the plan does not name is an error, in either direction. Avoid: plaintext field, skipped field, excluded field
Identity: The label segment a field's data is keyed under, fixed when the data is first written and never changed afterwards. It defaults to the field's record name and is pinned apart from it once a stored column is renamed, so the stored name moves and the key does not. Avoid: column name (that is where the data is stored now), context (an identity is one segment of one)
EQL type: An output type that participates in EQL — a ciphertext or index term stored for query — and so carries the contract that its context is supplied and non-empty. EQL integration supplies a concrete identifier; generic target records need not be EQL types. Avoid: searchable type, indexed type
Term: A deterministic, one-way value an index derives from a plaintext under a context — one for equality, ORE or OPE; a set of them for match. The JSON index produces a searchable document instead: entries whose selectors are terms and whose ciphertexts are the nodes, under one data key. Avoid: index (that is the declaration; a term is what it produces), token, hash
Query: Deriving the terms for a plaintext under a field's plan, with no ciphertext, to match terms already stored. A query against an index the plan never declared is refused, so a query cannot silently match nothing. Avoid: probe, search term, lookup, encrypt query (that is the TypeScript package's method name, not the concept)
Open: Decrypting through a plan: reading back every field of a record that can come back (sealed and passthrough fields, not index-only ones), each under the context the plan gave it, or the one value of a one-value plan as its plaintext type. Decrypt is the cipher-directed counterpart, which takes a ciphertext and its context with no plan. Avoid: decrypt (for the plan form), unseal, read
Pending:
An output whose local work (term derivation, per-leaf sealing plan) is done
and whose ZeroKMS key requests are queued but not sent. Pendings compose
(zip, map, all) so a whole struct or Vec settles in one batched call.
Avoid: future, promise
Keyset:
The ZeroKMS key domain a data key is minted under and an index key belongs
to — one per tenant is the common shape. A client may use any number;
StackCipher is scoped to the client, not to a keyset. Its id (a UUID)
is its identity: globally unique, carried in every sealed leaf, never
re-checked.
Avoid: dataset, key ring, tenant (a tenant has a keyset)
Keyset cipher:
KeysetCipher, the cipher bound to one keyset, and what every operation
that mints binds to — sealing values, sealing records, deriving terms.
An owned handle (a cipher reference plus the keyset's loaded state), cheap
to clone and to hold per request. Decrypting through one is a constraint,
not a capability: it refuses a leaf from any other keyset.
Avoid: keyset handle (use "handle" only for the object, not the concept),
sub-cipher, tenant cipher
Scope:
What a Pending was built through, and therefore what it is allowed to do:
a KeysetCipher scope mints under its keyset and opens leaves from no
other; a StackCipher scope mints nothing and opens leaves from any keyset.
CipherScope is the sealed trait both references implement; dynamic::Scope
is the same choice as a runtime value, for a binding whose caller makes it
per call. Two pendings merge when their scopes agree on a keyset — which
cipher value each came from is not part of the rule.
Avoid: binding (that is a name's), context (that is the AAD's), opener
Name binding:
The cache's record that a keyset name resolved to a keyset id, and when.
A name is a lookup ZeroKMS answers, not an identity — ZeroKMS allows
renames — so a binding is trusted only within a window, a keyset holds at
most one at a time, and no binding outlives the id it names.
Avoid: alias (the struct is called Alias; the concept is a binding),
name cache entry
Freshness window:
How long a name binding is trusted before the next selection by that name
asks ZeroKMS again (DEFAULT_NAME_TTL, five minutes;
StackCipherBuilder::keyset_name_ttl). It bounds how long a rename can go
unnoticed by a running process, the way a resolver's TTL does; ZERO makes
every selection by name a round trip. Selection by id has no window.
Avoid: cache expiry, staleness (a binding past its window is stale, the
window itself is not)
Resolution ticket:
A monotonic stamp (Resolution) a lookup takes on its way to ZeroKMS and
hands back on insert. Resolutions run outside the cache lock, so answers
land in any order; the ticket is what says which question was later, and a
binding follows the later question rather than the earlier arrival.
Avoid: generation, version, sequence number
Watermark: The place in the resolution order of the latest answer the cache holds nothing of to order an older answer against: an entry eviction has dropped, or ZeroKMS's answer that a name is bound to nothing. Once an entry is gone there is nothing left to order an older answer for that keyset against, and a negative answer is held as no binding at all, so no binding is made from an answer older than the watermark. One watermark for every name, not one per forgotten name — a cache whose whole contract is a bound must not grow a record per eviction or per unbound name. Avoid: tombstone, negative cache, evicted binding (it is the entry's place, not a binding's), eviction watermark (eviction is one of two things that raise it)
Foreign keyset:
A keyset other than the one a KeysetCipher is bound to, from that
handle's point of view. Handing it a leaf sealed under one is
Error::ForeignKeyset, refused before any key is retrieved — the
guarantee a tenant-scoped handler asked for by taking a handle.
Avoid: wrong keyset, other tenant
Reservation:
The guest's whole linear memory, address space of the module's declared
maximum taken once (mmap PROT_NONE, VirtualAlloc MEM_RESERVE) so the
memory never moves. Growth commits more of it from the front.
Avoid: buffer (that is wazero's view of the committed part), allocation
Commit: Making a range of the reservation readable and writable as the guest grows, and locking it. A commit is what a lock is granted or refused on. Avoid: grow (that is the guest's request; the commit is the host's answer)
Lock:
Pinning committed memory in RAM (mlock, VirtualLock) so it is never
written to swap, and on Linux excluding the reservation from core dumps
(MADV_DONTDUMP). "Locked", of a client, means both held.
Avoid: pinned, wired
Lock policy:
What a refused lock means for a client. Best effort, the default: the
refusal is recorded and reported (MemoryLocked, MemoryLockError) and
the client works on with memory that may be swapped. Strict
(RequireLockedMemory): NewClient fails with ErrMemoryLock, and so
does any later call whose growth cannot be locked.
Avoid: mode, hard/soft
Growth refusal: Under the strict policy, a commit whose lock was refused and was therefore given back before the guest saw it. It fails the call that needed it and leaves the client's lock report unchanged, since nothing unlocked was admitted. A refusal of the guest's own allocation aborts the guest and closes the client. Avoid: lock failure (that is the report of memory admitted unlocked)
Heap fallback: A Go slice standing in for a reservation where none can be made: a platform with no primitive this package uses, or a 32-bit host asked for wasm's 4 GiB default. It still wipes on growth and release; it cannot be locked, and the client reports so. Avoid: default allocator (wazero's, which is never used), unlocked mode