Documentation
¶
Overview ¶
Package encrypt is the Stack Encrypt Go SDK: searchable, field-level encryption under per-value ZeroKMS data keys, running the stack-encrypt Rust engine unmodified inside a WASI guest under wazero (CGO_ENABLED=0).
Use the SDK ¶
1. Add the generator to your module (Go 1.26.10 or later, or 1.27.3 or later on 1.27: the first releases whose crypto/x509 blocks the compromised eMudhra intermediate CAs, CVE-2026-111943):
go get -tool github.com/cipherstash/stack/languages/golang/cmd/stashgen
2. Put a stash tag on every exported field of the struct, and a go:generate comment beside it:
//go:generate go tool stashgen -type User
type User struct {
_ struct{} `stash:"context=users"`
ID int64 `stash:"id,passthrough"`
Email string `stash:"email,encrypt_into=TextEq"`
Name string `stash:"name,encrypt_into=TextMatch"`
Age int32 `stash:"age,encrypt_into=Integer"`
}
encrypt_into names the EQL type the field is stored as. The Go field type must match it: TextEq, TextMatch and Text take a string, Smallint and SmallintEq an int16, Integer and IntegerEq an int32, Bigint and BigintEq an int64, Boolean a bool, Real and RealEq a float32, Double and DoubleEq a float64, Date and DateEq a Date, Timestamp and TimestampEq a time.Time, and Numeric and NumericEq a Decimal. An integer EQL type also takes any other built-in Go integer type, sealed at the type's width: one every value of which fits it (an int16 into Integer) always seals, and a wider one (an int64 into Integer) is range checked, a value that does not fit refused with ErrEncoding. The *Eq types answer Eq; the *Ord and *OrdOpe types (IntegerOrd, SmallintOrd, BigintOrd, RealOrd, DoubleOrd, TextOrd and their OrdOpe twins) answer Lt, Lte, Gt and Gte, and Eq too on text; Smallint, Integer, Bigint, Boolean, Real, Double, Text, Date, Timestamp and Numeric are stored and read only. NumericEq compares decimals with their scale normalised (1.5 = 1.50), and TimestampEq compares instants to the microsecond, truncated, never rounded.
3. Run the generator. It writes user_stash.go beside the struct:
go generate ./...
4. Commit the generated file.
5. Call the generated functions where you write and read:
client, err := encrypt.NewClient(ctx)
if err != nil {
return err
}
defer client.Close()
cipher := client.Keyset(encrypt.KeysetName("tenant-42"))
encrypted, err := users.Encrypt(ctx, cipher, people) // []users.EncryptedUser
opened, err := users.Decrypt(ctx, cipher, encrypted) // []users.User
term, err := users.Fields.Email.Eq(ctx, cipher, "bob@example.com")
Encrypt and Decrypt send one ZeroKMS request for each 500 sealed values in the batch, plus one the first time a keyset is used. A query derives its term locally, with no request of its own; only the first use of a keyset, by a query or anything else, loads it from ZeroKMS. See Queries below.
6. Store the encrypted type. Each encrypt_into field is one EQL v4 column (an eql.TextEq goes in a public.eql_v4_text_eq column), implementing driver.Valuer and sql.Scanner, so a database library binds and scans it like any other value. Install EQL v4 into PostgreSQL first, with the eql-v4-install.sql attached to the release. A query casts its parameter to the column's query type:
SELECT id, email, name, age FROM users WHERE email = $1::eql_v4.query_text_eq
A field tagged encrypt or index= stores its ciphertext (Ciphertext) and each term as separate byte columns instead; see Terms below.
7. Run the generator again after each change to the struct or to a tag. A change to the fields of the struct stops the build until you do.
8. In CI, run the generator and fail when a generated file changes:
go generate ./... && git diff --exit-code && test -z "$(git status --porcelain)"
git diff sees only tracked files; the git status check also fails on a generated file that was never committed.
The rest is the reference. The generator's own reference — the tag grammar, the flags, what it writes and what it refuses — is in cmd/stashgen/README.md.
Shape ¶
A Client is one wasm instance and one ZeroKMS client: NewClient resolves the client's Credentials, instantiates the embedded guest, hands it the client key once — the ClientKey the credentials resolved to is consumed and wiped, whatever the outcome — and loads the client's default keyset. It takes functional options (ClientOption), every one with a default, so NewClient(ctx) alone is a working client. Client.Close runs the guest's shutdown so the client key and every loaded index key are wiped before the instance is freed.
A Cipher is the client bound to one keyset (Client.Keyset and Client.DefaultKeyset) and to any extension of the context (Cipher.Extend): what changes from one caller to the next — a tenant, a region — attaches here and not to each call, so the write, the query and the read cannot use different ones. A Cipher opens only its own keyset's records; the Client opens records from any of its keysets. Both are a Decrypter, which the generated Decrypt functions take.
Declarations ¶
A struct's stash tags declare how each field is encrypted: the context of the struct, and for each field whether it is sealed, which indexes are derived beside it, or whether it passes through as it is. stashgen reads the tags and writes the encrypted type, Encrypt, Decrypt and Fields into the struct's package; the generated code hands the declaration to the engine as data through package gensupport, and the engine runs the same plan the Rust chain and the derive run. A program never builds or names a plan, and no call takes a context or an index choice of its own.
A sealed field outside an opaque struct is one value: a string, []byte, bool, an integer of any width (int8 to int64, uint8 to uint64, int, uint, or Int128 and Uint128), a float32 or float64, a Decimal, a Date or a time.Time (a timestamp), or a type defined over one of the built-in types. Each integer seals at its own width. Equality applies to every one but a bool. ORE and OPE apply to integers of 16 bits or more, floats, text, bytes, decimals, dates and timestamps. A bool or an 8-bit integer takes no order index, since over so few values its order terms would show which rows hold which value.
Passthrough fields never cross the binding: the engine does nothing to a passthrough value that a program could observe, and the FFI codec cannot carry every Go type a program stores beside a ciphertext (a driver.Valuer, a sql.NullTime). An opaque struct crosses as one storage blob (vitaminc's storage profile, vcffi.MarshalStorage) and is one column; a row an earlier release sealed as a JSON document still opens.
Terms ¶
A sealed field with an index gets a term beside its ciphertext: EqualityTerm, MatchTerm, OreTerm or OpeTerm, byte-equal to the ones the Rust crate derives, so a term from a generated Fields entry compares against a stored term from any language. The indexes are Equality, Match, Ore and Ope; JSON is declared and refused until the engine derives it. Every stored type implements driver.Valuer and sql.Scanner.
A match index stores text with no token (an empty string, separator-only text, or text shorter than the n-gram length of 3 characters) as the empty filter: a valid value that no query matches, so an optional or short value can live under match. The empty filter tells anyone who can read the column that the value is one of those. A match *query* of such text is refused with ErrTerm, because an empty filter as a query would match every row.
Queries ¶
A generated Fields entry has one method for each operator the engine answers on the field, named for it: Eq, Lt, Lte, Gt, Gte, Between and Matches. A field has no method for an operator it does not answer, so such a query does not compile; the generator asks the engine which operators each field answers, and holds no copy of the rules. Each method takes the field's own Go type.
Eq returns an EqualityTerm (or, for a field with encrypt_into, the EQL type's query value), an ordering an OreTerm or OpeTerm, and Between a Range of two. Their answers are exact. The orderings are answered on integers, on floats, which order as PostgreSQL's float8 does (-0.0 ties 0.0, NaN sorts last), on text, which orders by its folded form (case and accents folded, under Unicode 16), so Café and cafe tie and alice orders before Zoe, and on a Decimal, which orders by value (1.5 ties 1.50), a Date, and a time.Time, which orders by the microsecond. Matches returns a MatchQuery, whose answer can hold rows that do not match: size the fetch with MatchQuery.TargetLimit, then decrypt the rows and keep those MatchQuery.Verify reports true, one call into the engine for the page. On a field with encrypt_into=TextMatch, Matches returns an EQLMatchQuery, the type's query value with the same two methods. Every query type implements driver.Valuer, so it binds as a parameter through database/sql or pgx. With separate byte columns, as the example/ program declares its fields (Email `stash:"email,encrypt,index=equality;match"`, Age `stash:"age,encrypt,index=equality;ore"`), not the encrypt_into User above:
q, err := users.Fields.Email.Matches(ctx, cipher, "alice")
limit, err := q.TargetLimit(ctx, 10, encrypt.Stats{Rows: rowCount})
rows, err := db.QueryContext(ctx, `SELECT id, email, email_eq, email_match, age, age_eq, age_ore
FROM users WHERE match_positions(email_match) @> match_positions($1)
ORDER BY id LIMIT $2`, q, limit.FetchOr(50))
// scan into []users.EncryptedUser, then
people, err := users.Decrypt(ctx, cipher, encrypted)
emails := make([]string, len(people))
for i, p := range people {
emails[i] = p.Email
}
verified, err := q.Verify(ctx, emails) // one bool per row
match_positions is a SQL function of your own that reads a match term's little-endian uint16 positions into an int[]; the package README has one. A MatchQuery keeps the text it was derived from, because Verify cuts its tokens again in the engine. Go cannot wipe a string, so the text stays in memory until the garbage collector reclaims it; a MatchQuery never prints it.
Encrypted JSON documents ¶
A field tagged encrypt_into=JsonSearch is stored as one encrypted JSON document. Declare it as a json.RawMessage, which holds the document's JSON text, or as a struct, map or slice type. The generated code marshals such a type with encoding/json when it seals the field. It unmarshals the opened text into the declared type, so integers to 64 bits and finite float64s come back exactly. The rest comes back as encoding/json reads what it wrote, which is not always the value sealed: invalid UTF-8 in a string becomes U+FFFD, with no error (a property of the value, which the generated code does not walk, so check it with utf8.ValidString first); a nil map or slice is stored as JSON null, which a needle of an empty one does not match; and a time.Time inside a document is stored as RFC 3339 text in its own offset, so EqAt with a time in another zone misses and an ordering on that path orders the text, not the instants. Store a time you query as an integer, such as t.UnixNano().
The field's operands are Go values. Contains and ContainedBy take a needle, and EqAt and the orderings take a scalar. Each is a json.RawMessage of JSON text or any value encoding/json writes:
q, err := customers.Fields.Profile.Contains(ctx, cipher, map[string]any{"tier": "gold"})
older, err := customers.Fields.Profile.At.Age.Gt(ctx, cipher, 40)
A Go string is the string value itself, never parsed. So Contains and ContainedBy refuse a string needle, or a pointer to one; put JSON text in a json.RawMessage. They refuse a needle that writes JSON null (nil, or a nil pointer, map or slice) too: only a null document contains it.
A document contains a needle when it holds every member the needle writes. encoding/json writes a struct field that has no omitempty even when the field is zero. So a zero field narrows the query to documents that hold that zero. Give a needle struct pointer fields with omitempty, or use a map. Contains refuses a needle of the field's own type when that type holds a struct, the likeliest way to make this mistake, a slice of structs included: for a []Visit document, pass a slice of a needle type with pointer fields. ContainedBy takes one: there a zero field widens the query.
A selection or a subtree read opens values as JSON text (JSONValue, JSONRead.Open). DecodeJSON reads one into a Go type.
Two queries at an array's paths can each be answered by a different element. To test several members of one element together, use Match on the array's accessor under Elements, which a field has for each array whose elements have an ordered member. The generated Paths names those members, nested as the JSON nests them, and AtPath makes a predicate from one:
q, err := customers.Fields.Profile.Elements.Visits.Match(ctx, cipher, encrypt.JSONReadOptions{},
encrypt.AtPath(customers.Paths.Profile.Visits.Place).Eq("home"),
encrypt.AtPath(customers.Paths.Profile.Visits.At).Gt(15),
)
// WHERE eql_v4.json_tree_element_match(profile, $1::eql_v4.query_json_elements)
A Path carries its array and the member's Go type, so a value of another type, or a predicate on another array's member, does not compile. For a filter built at run time, the array accessor's Member names the member as text: Visits.Member("place").Eq("home").
Each comparison is of the member's ordering term, so MemberTest.Eq compares text case- and accent-folded, and integers past 2^53 can tie. A row whose array is longer than JSONReadOptions.WildcardBound (64 by default) fails the query with SQLSTATE 22023.
A whole filter binds as one parameter with the field's Match: a jsonpath predicate and its variables, as plaintext jsonb_path_match takes them. The engine parses the predicate and compiles it into a tree of the operands above; nothing in Go reads it.
q, err := customers.Fields.Profile.Match(ctx, cipher,
`$."name" == $name && $."age" < $max`,
map[string]any{"name": "Alice", "max": 40}, encrypt.JSONReadOptions{})
// WHERE profile @@ $1::eql_v4.query_json_predicate
Pass an empty map when the predicate has no variables. A nil vars binds as SQL NULL, as it does over plaintext, and matches no row.
A predicate the engine cannot answer is refused as a Diagnostic wrapping ErrEncoding, whose Reason, Construct and At name the refusal and the byte offset in the predicate where it starts. The tree is evaluated inside eql_v4.json_tree_match, so no index engages: a query that needs one writes its leaves with EqAt, Contains and At.<Path>.
Cancellation ¶
Every call takes a context.Context, and a call whose context is cancelled or passes its deadline fails with that context's error. Only that call fails: one Client is shared by every request in a service, and one request giving up never closes it for the others. A call waiting for the client, which runs one call at a time, leaves the queue when its context ends and never starts. A call waiting on ZeroKMS has that request cancelled; its error matches both the context's error and ErrTransport. A call waiting on its auth strategy for a token returns too, with the context's error. If the strategy is refreshing a device session, the refresh finishes in the background, for up to 10 seconds: the service rotates the refresh token when it answers, and dropping that answer would leave the strategy unable to refresh.
A Client closes itself, and every later call returns ErrState, only when the guest's state is unknown: the guest trapped, or a call ran on for 30 seconds after its context ended, which means a bug, not a caller that gave up. A service that sees ErrState from a client it did not close builds a new one.
Transport and auth ¶
The guest imports exactly one host function, a bearer-token fetch, served by the credentials' auth strategy. It never performs a request itself: an operation that needs ZeroKMS pauses and hands the request to the client, which sends it with any net/http.RoundTripper and resumes the operation with the response. What crosses per ZeroKMS call is what would cross TLS anyway; derived key material never leaves the guest. Under AutoCredentials and OIDCFederation the same RoundTripper also carries the authentication requests to CTS, so one scoped to the ZeroKMS host alone is not enough. Under NewCredentials those requests go through the store the caller opened the strategy from.
Credentials ¶
A Credentials supplies the client id, the client key and the auth strategy the token comes from, and NewClient resolves it host-side: the crypto guest is never given the environment or a filesystem to find them in. The default, AutoCredentials, mirrors the Rust client — the environment first (CS_CLIENT_ACCESS_KEY with CS_WORKSPACE_CRN for the token, CS_CLIENT_ID with CS_CLIENT_KEY for the key), then the developer profile, which it reads through the auth package's credential guest. CS_ZEROKMS_HOST (or CS_VITUR_HOST) pins the endpoint whatever the credentials. NewCredentials takes a client id, a client key and a strategy explicitly, and OIDCFederation mints the token from an identity provider's. None takes a raw token: a token is always a strategy's, since a raw one cannot be refreshed when it expires. Credentials that cannot be resolved fail NewClient with ErrNoCredentials; credentials that resolve but do not work fail it too, at the one ZeroKMS round trip it makes.
Errors ¶
The generator and the compiler find a mistake in a declaration, so no call returns an error for one. A call returns an error for a key, for the network, or for stored data, or because its context ended (see Cancellation): ErrForeignKeyset when a *Cipher is given a record another keyset sealed, ErrAuthentication or ErrForbidden for a ciphertext that does not open under its field's context, ErrEncoding for a stored value that does not fit its declaration. Read the kind with errors.Is, and the detail behind a failure the engine reports (its code, help, structured fields and causes) with errors.As into a *Diagnostic. No error, warning or log line holds a plaintext value; a generated type hides its sealed fields when a program prints it.
Host runtime ¶
The guest also imports WASI random_get and clock_time_get, and the cipher's security rests on the first: ZeroKMS IVs and AEAD nonces are drawn from it. wazero's defaults for both are deterministic, so every instance is configured with the process CSPRNG (crypto/rand.Reader) and the system clocks.
Memory ¶
Every key the guest holds — the client key, each loaded index key, each data key for the length of a call — lives in the guest's linear memory, and the package supplies that memory itself: reserved once so growth never copies it, locked in RAM (mlock, VirtualLock) so it is never written to swap, excluded from core dumps on Linux (MADV_DONTDUMP), and wiped before it is released, on every release path. That is done at allocation, where the caller cannot get it wrong, and not at exit, where they cannot be relied on.
The lock is best effort: RLIMIT_MEMLOCK defaults to 64 KiB on many Linux hosts and the guest is larger, so it is commonly refused, and a client then works on with memory that may be swapped. Client.MemoryLocked reports the outcome and Client.MemoryLockError the reason, naming the limit to raise. WithRequireLockedMemory turns a refusal into a NewClient failure with ErrMemoryLock, and refuses any later growth of the guest's memory that cannot be locked. A Client prints its memory state (Client.String) and logs it (Client.LogValue).
Between calls the guest holds the client key and its keyset cache. Data keys are per-call values, wiped when the export returns, and every buffer staged for a call is wiped before the call's result is returned. One exception remains: after Decrypt, one unwiped copy of each opened string stays in the guest's freed heap until that memory is reused. The test TestPlaintextDoesNotRemainInGuestMemoryAfterDecrypt records the gap and is skipped until vitaminc wipes that copy.
Index ¶
- Variables
- func DecodeJSON[T any](text json.RawMessage) (T, error)
- type Bucket
- type Cause
- type Checker
- type Cipher
- func (cph *Cipher) Client() *Client
- func (cph *Cipher) Context(label string) *Cipher
- func (cph *Cipher) ContextLabel() string
- func (cph *Cipher) Extend(parts ...any) *Cipher
- func (cph *Cipher) Extension() []any
- func (cph *Cipher) JSONOpen(ctx context.Context, plan *record.Plan, field string, request vcvalue.Object, ...) ([][][]byte, error)
- func (cph *Cipher) Keyset() KeysetSelector
- func (cph *Cipher) KeysetID(ctx context.Context) (KeysetID, error)
- func (cph *Cipher) Open(ctx context.Context, plan *record.Plan, records []record.Sealed) ([]record.Source, error)
- func (cph *Cipher) QueryTerm(ctx context.Context, plan *record.Plan, field, operator string, input any) (QueryTerm, error)
- func (cph *Cipher) Seal(ctx context.Context, plan *record.Plan, rows []record.Source) ([]record.Sealed, error)
- func (cph *Cipher) TargetLimit(ctx context.Context, plan *record.Plan, field, operator string, term []byte, ...) (Limit, error)
- func (cph *Cipher) Verify(ctx context.Context, plan *record.Plan, field, operator string, input any, ...) ([]bool, error)
- type Ciphertext
- type Client
- func (c *Client) Close() error
- func (c *Client) DefaultKeyset() *Cipher
- func (c *Client) Keyset(sel KeysetSelector) *Cipher
- func (c *Client) LogValue() slog.Value
- func (c *Client) MemoryLockError() error
- func (c *Client) MemoryLocked() bool
- func (c *Client) Open(ctx context.Context, plan *record.Plan, records []record.Sealed) ([]record.Source, error)
- func (c *Client) String() string
- type ClientKey
- type ClientOption
- type Credentials
- type Date
- type Decimal
- type Decrypter
- type Diagnostic
- type EQLMatchQuery
- func (q EQLMatchQuery[Q, T]) Format(f fmt.State, _ rune)
- func (q EQLMatchQuery[Q, T]) GoString() string
- func (q EQLMatchQuery[Q, T]) LogValue() slog.Value
- func (q EQLMatchQuery[Q, T]) Query() Q
- func (q EQLMatchQuery[Q, T]) String() string
- func (q EQLMatchQuery[Q, T]) TargetLimit(ctx context.Context, want uint64, stats Stats) (Limit, error)
- func (q EQLMatchQuery[Q, T]) Value() (driver.Value, error)
- func (q EQLMatchQuery[Q, T]) Verify(ctx context.Context, values []T) ([]bool, error)
- type ElementPredicate
- func (p ElementPredicate[E]) Format(f fmt.State, _ rune)
- func (p ElementPredicate[E]) GoString() string
- func (p ElementPredicate[E]) LogValue() slog.Value
- func (p ElementPredicate[E]) Member() string
- func (p ElementPredicate[E]) Op() string
- func (p ElementPredicate[E]) String() string
- func (p ElementPredicate[E]) Values() []any
- type EqualityTerm
- type Index
- type Int128
- type JSONOption
- type JSONRange
- type JSONRead
- func (r JSONRead[E]) Format(f fmt.State, _ rune)
- func (r JSONRead[E]) GoString() string
- func (r JSONRead[E]) LogValue() slog.Value
- func (r JSONRead[E]) Open(ctx context.Context, rows [][]E) ([][]json.RawMessage, error)
- func (r JSONRead[E]) OpenOne(ctx context.Context, entries []E) (JSONValue, error)
- func (r JSONRead[E]) String() string
- func (r JSONRead[E]) Tokens() JSONTokens
- type JSONReadOptions
- type JSONSelection
- func (s JSONSelection[E]) Format(f fmt.State, _ rune)
- func (s JSONSelection[E]) GoString() string
- func (s JSONSelection[E]) LogValue() slog.Value
- func (s JSONSelection[E]) Open(ctx context.Context, rows []*E) ([]JSONValue, error)
- func (s JSONSelection[E]) Selector() JSONSelector
- func (s JSONSelection[E]) String() string
- type JSONSelector
- type JSONTermdeprecated
- type JSONToken
- type JSONTokens
- type JSONValue
- type KeysetID
- type KeysetName
- type KeysetSelector
- type Limit
- type MatchOption
- type MatchQuery
- func (q MatchQuery[T]) Format(f fmt.State, _ rune)
- func (q MatchQuery[T]) GoString() string
- func (q MatchQuery[T]) LogValue() slog.Value
- func (q MatchQuery[T]) String() string
- func (q MatchQuery[T]) TargetLimit(ctx context.Context, want uint64, stats Stats) (Limit, error)
- func (q MatchQuery[T]) Term() MatchTerm
- func (q MatchQuery[T]) Value() (driver.Value, error)
- func (q MatchQuery[T]) Verify(ctx context.Context, values []T) ([]bool, error)
- type MatchTerm
- type MemberTest
- func (m MemberTest[E, T]) Between(lo, hi T) ElementPredicate[E]
- func (m MemberTest[E, T]) Eq(v T) ElementPredicate[E]
- func (m MemberTest[E, T]) Gt(v T) ElementPredicate[E]
- func (m MemberTest[E, T]) Gte(v T) ElementPredicate[E]
- func (m MemberTest[E, T]) Lt(v T) ElementPredicate[E]
- func (m MemberTest[E, T]) Lte(v T) ElementPredicate[E]
- type OpeTerm
- type OreTerm
- type Path
- type QueryTerm
- type Range
- type Stats
- type Uint128
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrAuthentication is an AEAD open failure: a tampered ciphertext, a // wrong element derivation, or a wrong AAD that reached the AEAD. Against // ZeroKMS a wrong AAD is usually refused earlier as ErrForbidden, because // every data key is bound to its context. ErrAuthentication = guest.ErrAuthentication // ErrEncoding is a malformed input: a value, ciphertext, plan, context, // selector or config the guest refused before any cryptography. ErrEncoding = guest.ErrEncoding // ErrState is a call on a client that has been closed. ErrState = guest.ErrState // ErrInternal is a guest panic or any other unexpected guest failure. ErrInternal = guest.ErrInternal // token is invalid, expired, or for another workspace. ErrUnauthorized = guest.ErrUnauthorized // ErrForbidden is ZeroKMS refusing the request (HTTP 403): the token is // valid but not permitted, or a data key's bound context did not match // the one presented — the production form of a wrong-AAD open. ErrForbidden = guest.ErrForbidden // ErrNotFound is ZeroKMS reporting a missing resource (HTTP 404): an // unknown keyset name or id, or a data key that does not exist. ErrNotFound = guest.ErrNotFound // ErrConflict is ZeroKMS reporting a resource conflict (HTTP 409). ErrConflict = guest.ErrConflict // ErrTransport is a failure to reach ZeroKMS or to read its response: // the transport returned an error, or the endpoint could not be resolved. ErrTransport = guest.ErrTransport // ErrKMS is any other ZeroKMS failure: an unparseable response, invalid // key material, or an unclassified server error. ErrKMS = guest.ErrKMS // ErrTerm is a term derivation the scheme could not perform for the // given input, such as match text that yields no tokens. ErrTerm = guest.ErrTerm // ErrForeignKeyset is a keyset-bound Cipher refusing a ciphertext sealed // under another keyset, before any key is retrieved. Open it through the // Client, which is not bound to one keyset. ErrForeignKeyset = guest.ErrForeignKeyset // ErrContextMismatch is a row whose context field, stored in the clear // beside its sealed fields, is not the context named with // [Cipher.Context]. Refused before any key is retrieved; a row stored // under another tenant is a mismatch, never a decrypted value. ErrContextMismatch = guest.ErrContextMismatch // ErrMemoryLock is guest memory that could not be locked in RAM (or, // on Linux, excluded from core dumps). NewClient returns it when // WithRequireLockedMemory is given, and so does any later call under // that setting whose growth of the guest's memory could not be locked; // otherwise Client.MemoryLockError reports it and the client works on // with unlocked memory. The wrapped error names the limit that refused // the lock and the size the guest holds: on Linux, RLIMIT_MEMLOCK // (ulimit -l, a systemd LimitMEMLOCK=, or a pod's securityContext). ErrMemoryLock = guest.ErrMemoryLock )
Failure kinds surfaced across the boundary: the guest reports a status code, and these are what the codes decode to. They separate a tampered ciphertext from a bad token from a malformed input, and reveal nothing about plaintext or key material. Check one with errors.Is; the detail behind it is a Diagnostic.
They are the sentinels every guest package shares (one status table for every guest, decoded once), exposed here under this package's names: an error from auth is the same value, so errors.Is holds across the two.
var ErrCredentialsConsumed = errors.New("encrypt: the credentials' client key was already consumed by an earlier NewClient")
ErrCredentialsConsumed is NewCredentials given to a second NewClient: the first consumed its key — whether it made a client or refused its config — so there is nothing left to give. Build new credentials, with a new key, for another client.
var ErrEQLGuestNotBuilt = errors.New("encrypt: guest module with the EQL types not built — run `mise run wasm:guest:build:eql`")
ErrEQLGuestNotBuilt is returned by NewClient when the program links package eql (its generated code names an EQL type) and the guest build with the EQL types is not embedded. There is no fallback to the build without them: every encrypt_into call would fail there, so the program fails at startup instead.
var ErrGuestNotBuilt = errors.New("encrypt: guest module not built — run `mise run wasm:guest:build`")
ErrGuestNotBuilt is returned by NewClient when no guest module is embedded.
var ErrNoCredentials = errors.New("encrypt: no credentials")
ErrNoCredentials is AutoCredentials finding no token strategy or no client key in either place it looks. The wrapped error says which, and what to set.
var ErrNonFiniteDecimal = vcvalue.ErrNonFiniteDecimal
ErrNonFiniteDecimal is the error ParseDecimal returns for NaN or an infinity.
var ErrStorageVersion = vcffi.ErrUnsupportedVersion
ErrStorageVersion is an opaque struct sealed as a storage blob of a version this release does not read: a later release wrote it. Its error wraps ErrEncoding too. It is vcffi.ErrUnsupportedVersion, named here so a program can check for it without importing vitaminc's bindings.
Functions ¶
func DecodeJSON ¶
func DecodeJSON[T any](text json.RawMessage) (T, error)
DecodeJSON reads JSON text a read opened, a JSONValue's JSON or one value of JSONRead.Open, into a value of the caller's type T, with encoding/json's rules: a number reads into a declared integer field exactly, int64 and uint64 included, and into an interface (any) as a json.Number, never a float64 that rounds it. A member T does not declare is skipped, as encoding/json skips it. Text that is not one JSON value (empty, as an unfound JSONValue's is, or followed by more) is refused.
An error wraps ErrEncoding and names T and the kind of JSON value that did not fit, never the value: what a read opened is plaintext.
Types ¶
type Bucket ¶
Bucket is one bar of a histogram of the stored match filters' set-bit counts: Rows rows whose filter sets Bits bits.
type Cause ¶
Cause is one error in a Diagnostic's cause chain: a Code and a Message. A cause from a library outside the stack crates has no Code, and its Message is a description the guest vouches for, never that library's own text.
type Checker ¶
type Checker struct {
// contains filtered or unexported fields
}
Checker asks the embedded engine about declarations, with no credentials, no keyset and no network: what stashgen uses to refuse a declaration the engine would refuse, and to learn which EQL types the engine produces. It holds a guest instance that was never given a client key, so every other operation on it is ErrState.
func NewChecker ¶
NewChecker instantiates the embedded guest for the generator's questions.
func (*Checker) Check ¶
Check refuses a plan the engine would refuse: an index its field's type does not admit, a context that is not a label, two fields under one identity. The error is ErrEncoding. A refusal from the engine is a *Diagnostic that names the field with [Diagnostic.Field] and the rule with [Diagnostic.Reason].
func (*Checker) Operators ¶
func (k *Checker) Operators(ctx context.Context, plan *record.Plan, field string) ([]record.Operator, error)
Operators lists the operators one field of a plan answers, in the engine's order, each with its fidelity, its arity and the index that answers it: exactly the operators Cipher.QueryTerm derives a term for on that field, so stashgen writes one query method for each and holds no copy of which index answers which operator. A field the plan does not have is ErrEncoding.
type Cipher ¶
type Cipher struct {
// contains filtered or unexported fields
}
Cipher is a Client bound to one keyset, and to any extension of the context: the Go form of the Rust crate's KeysetCipher. Generated code seals values, derives terms and opens records through it; a program never calls the engine directly. A Cipher opens only its own keyset's ciphertexts — a leaf sealed under another keyset is refused as ErrForeignKeyset before any key is retrieved. To open ciphertexts from any keyset, pass the Client where a Decrypter is taken. A Client opens only rows that a cipher with no extension encrypted; rows sealed through Cipher.Extend open only through a cipher with the same extension.
A Cipher holds no guest state: the keyset is selected on every call, and loaded by the guest on first use, so one is cheap to make per request or per tenant.
func (*Cipher) Context ¶
Context returns a cipher that names the context every row through it is under, for a type whose context is one of its own fields (a field tagged `context_field`): the Rust chain's `.context(..)`. Through such a cipher, Decrypt checks each row's stored context field against the label before any key is retrieved and refuses a row that names another with ErrContextMismatch; Encrypt refuses a value whose context field says otherwise the same way, before anything is sent; a query derives its term under the label, which such a type cannot derive otherwise; and a field's own Encrypt seals the one value under it. Without a Context, Encrypt and Decrypt take each row's context from the row, and a query has no context to derive under. A type with a `context=` tag has its context already, so every call through a cipher with a Context refuses it as ErrEncoding. The label is segments separated by '/', such as "tenants/acme"; one that is not a plain label is reported by the first call, as ErrEncoding.
func (*Cipher) ContextLabel ¶
ContextLabel is the label Cipher.Context named, or "" for a cipher that names none.
func (*Cipher) Extend ¶
Extend returns a cipher that extends the context of every field, in every call through it: the write, the query and the read. It appends to the context the tags declare and never replaces it, so a field sealed through cipher.Extend("tenant-42") opens and matches only through a cipher with the same extension. A part is a string, a byte slice or an integer (int32, int64, uint32, uint64; Go's int is sent as int64); a byte slice is copied. Several parts nest in order: Extend(a, b) is Extend(a).Extend(b). An unsupported part type is reported by the first call through the cipher, as ErrEncoding.
func (*Cipher) Extension ¶
Extension is the parts the cipher extends every field's context by, in order. Empty for a cipher straight from Client.Keyset.
func (*Cipher) JSONOpen ¶
func (cph *Cipher) JSONOpen(ctx context.Context, plan *record.Plan, field string, request vcvalue.Object, rows [][][]byte) ([][][]byte, error)
JSONOpen opens the rows a read of a field that stores a JSON document returned (the guest's se_json_open), against the request that prepared the read: {"operation": "selector" | "subtree", "path", "wildcard_bound"}. rows holds one group per database row: for selector, the entry col -> selector returned, or none for SQL NULL; for subtree, the rows eql_v4.json_tree_subtree returned. Each entry is the public.eql_v4_json_entry JSON the database sent.
The engine derives the selector or the read's key again from the request, the field's declaration and this cipher's keyset, and checks every row against it; the rows never say which path they are for. A row that is not one the read asked for is ErrEncoding; a wildcard read of an array longer than its bound is too, and so is a selector entry that does not open alone: a container with members, whose entry commits to children col -> does not return, or an altered entry, which the client cannot tell apart (code stack_encrypt::json_not_a_leaf). Read a container with a subtree read. What comes back is, per group, the JSON text of each value the read reached, in document order: plaintext, which the caller owns. One ZeroKMS request for every group. For generated code.
Opening does not prove the database returned every row the read asked for, or rows of the database row the caller meant: see the EQL json_tree leakage reference.
func (*Cipher) Keyset ¶
func (cph *Cipher) Keyset() KeysetSelector
Keyset is the selector this cipher is bound to.
func (*Cipher) KeysetID ¶
KeysetID resolves the cipher's keyset to its id: Rust's KeysetCipher::keyset_id, and in Go the one explicit resolution point, since a Cipher is made without a request. The first use of a name or id on the client is one ZeroKMS round trip, later uses come from the guest's cache; the default keyset never makes a request. Use it at boot to validate a tenant's keyset and learn its id.
func (*Cipher) Open ¶
func (cph *Cipher) Open(ctx context.Context, plan *record.Plan, records []record.Sealed) ([]record.Source, error)
Open decrypts records sealed under the plan: every row, in order, with one ZeroKMS request for each 500 sealed values. A record from another keyset is ErrForeignKeyset; under Cipher.Context, a record whose stored context field names another context is ErrContextMismatch. Both are refused before any key is retrieved. For generated code.
func (*Cipher) QueryTerm ¶
func (cph *Cipher) QueryTerm(ctx context.Context, plan *record.Plan, field, operator string, input any) (QueryTerm, error)
QueryTerm derives the query term for one field of the plan, an operator and its input under this cipher's keyset: one value for every operator but between, whose input is a two-element slice, low then high, and a document's operators (contains, eq_at, selector, ...), whose input is a vcvalue.Object of named inputs with JSON values as JSON text. The engine reads the input as the field's declared type, finds the index that answers the operator, and derives that index's term under the field's context; for a field that names an EQL type it runs the type's query. An operator the field does not answer, or one the engine does not know, is ErrEncoding naming the field, as a mistyped value is; match text that cuts no token is ErrTerm. A range the wrong way round is not refused: it matches no row, as SQL's BETWEEN does. No data key is minted. For generated code.
func (*Cipher) Seal ¶
func (cph *Cipher) Seal(ctx context.Context, plan *record.Plan, rows []record.Source) ([]record.Sealed, error)
Seal encrypts rows under the plan, through this cipher's keyset and with its extension: the sealed fields of every row, in order, with one ZeroKMS request for each 500 sealed values. For generated code.
func (*Cipher) TargetLimit ¶
func (cph *Cipher) TargetLimit(ctx context.Context, plan *record.Plan, field, operator string, term []byte, want uint64, stats Stats) (Limit, error)
TargetLimit is how many rows to fetch so that, at the confidence stats names, want of them verify: term is the probabilistic term's bytes as Cipher.QueryTerm returned them. An exact operator has no fetch to size and is ErrEncoding, as are bytes the field's index would not derive. Uses no key. For generated code.
func (*Cipher) Verify ¶
func (cph *Cipher) Verify(ctx context.Context, plan *record.Plan, field, operator string, input any, values []any) ([]bool, error)
Verify reports, for each of a page of decrypted values, whether it really matches a probabilistic query: one call into the guest for the page. The query is named as it was derived, by the field, the operator and the input; the engine cuts the input's tokens again with the field's own tokenizer, so nothing here holds a copy of it. A nil value is false. An exact operator has nothing to verify and is ErrEncoding. Uses no key. For generated code.
type Ciphertext ¶
type Ciphertext []byte
Ciphertext is one sealed field as a database column holds it: the frozen stack-encrypt leaf encoding (version, keyset id, IV, ZeroKMS tag, ciphertext). A generated type holds one for each field sealed in separate columns, beside that field's terms. It is a distinct type from vitaminc's own leaf on purpose: a stack-encrypt leaf is not decryptable by vitaminc-encrypt and must never scan or marshal where one belongs.
A sealed field is one scalar — a string, a number, a bool or a []byte, or a type defined over one, or a pointer to either, whose nil is sealed as a null as long as a value or stored as SQL NULL — and seals as vitaminc's tagged leaf, which a Rust record opens when its field is a Value (not a bare u32 or String). A struct, slice or map seals only as part of an opaque struct, which crosses as one storage blob and is one leaf; stashgen refuses it anywhere else, because the engine would seal it as a tree of leaves and a column holds one.
func (*Ciphertext) Scan ¶
func (c *Ciphertext) Scan(src any) error
Scan implements sql.Scanner, loading a leaf from a byte column.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is one wasm instance holding one ZeroKMS client: its key, its default keyset, and the keysets it has loaded since. It is safe for concurrent use; the guest runs one call's step at a time, because a wasm instance is single-threaded, but never waits on the network inside one: a ZeroKMS request is performed between steps, with the client free for other callers (see step.go). Close it when done, as with any resource.
A call whose context ends fails with the context's error, and only that call: one request's cancellation or deadline never closes the client for the others sharing it. A call waiting its turn leaves the queue when its context ends; a call waiting on ZeroKMS has its request cancelled, and the error matches both the context's error and ErrTransport. The client closes itself, and every later call is ErrState, only when the guest's state is unknown: it trapped, or it went on running long after its caller gave up, which is a bug.
Its key material lives in the guest's linear memory, which this package supplies: reserved once so it never moves, locked in RAM and excluded from core dumps where the platform allows, and wiped before it is released. None of that depends on Close running — no exit path a process can take (a signal with no handler, SIGKILL, the OOM killer, a panic on another goroutine, os.Exit) runs deferred calls, and none of them is where the protection lives. Client.MemoryLocked reports whether the lock was granted.
func NewClient ¶
func NewClient(ctx context.Context, opts ...ClientOption) (_ *Client, err error)
NewClient resolves the credentials, instantiates the guest, loads the client key into it, and loads the default keyset — one ZeroKMS round trip, which is where credentials that resolve but do not work fail: a token that cannot be minted or is refused fails here, not at first use. The returned client is ready to seal.
With no options it is a working client: the credentials are AutoCredentials, and every other setting has a default. Each ClientOption changes one.
Example ¶
// With no options, NewClient uses AutoCredentials. To supply the
// credentials yourself, the token comes from an auth strategy —
// here an access key; live_test.go has a real round trip. The caller
// opened the store and the strategy, and closes them after the client.
ctx := context.Background()
err := func() error {
store, err := auth.OpenWithoutProfile(ctx)
if err != nil {
return err
}
defer store.Close()
strategy, err := store.AccessKey(ctx, "crn:ap-southeast-2.aws:ZVATKW3VHMFG27DY", "CSAK...")
if err != nil {
return err
}
defer strategy.Close()
client, err := NewClient(ctx, WithCredentials(NewCredentials(
"6a70bd18-99ac-4650-b104-37eec3a15b09",
NewClientKey([]byte("...")), // not a real key: NewClient refuses it
strategy,
)))
if err != nil {
return err
}
return client.Close()
}()
fmt.Println(err != nil)
Output: true
func (*Client) Close ¶
Close shuts the guest down — the client key and every loaded index key are wiped inside the instance — and releases the runtime and the guest's memory, which is wiped on the way out. Idempotent. Every call after it fails with ErrState.
It takes no context because it does no I/O and must not be skippable: a deferred Close is ordinary resource hygiene, and the memory's protection (see Client) does not wait on it.
func (*Client) DefaultKeyset ¶
DefaultKeyset binds the client to its default keyset — the one a ZeroKMS administrator set for this client, which is what naming no keyset resolves to: Rust's StackCipher::default_keyset. Loaded at NewClient, so using it never touches ZeroKMS. There is no way to redefine it from here; which keyset is the default is the server's to say. Any other keyset is Client.Keyset.
func (*Client) Keyset ¶
func (c *Client) Keyset(sel KeysetSelector) *Cipher
Keyset binds the client to one keyset, by name or by id: Rust's StackCipher::keyset. No request is made here — Go has no await, so the keyset is resolved by the guest on the cipher's first use (and cached), which makes a Cipher cheap to make per call, per tenant or per request. Cipher.KeysetID is the explicit resolution point. A nil selector gives a cipher whose every call fails with ErrEncoding; the default keyset is Client.DefaultKeyset.
func (*Client) LogValue ¶
LogValue implements slog.LogValuer: a group with memory_locked and, when false, memory_lock_error.
func (*Client) MemoryLockError ¶
MemoryLockError is why MemoryLocked is false: an error wrapping ErrMemoryLock that names what was refused and the limit that refused it, for this guest, the credentials' memory, or both. Nil while every one is locked.
func (*Client) MemoryLocked ¶
MemoryLocked reports whether the memory the client's key material lives in — this guest's, where the client key and every loaded index key are, and whatever the credentials held it in on the way (auth's credential guest, for AutoCredentials) — is locked in RAM and, on Linux, excluded from core dumps. False means a lock was refused (on Linux, most often RLIMIT_MEMLOCK, which defaults to 64 KiB on many hosts) or is not available on this platform, and the client is working on with memory the kernel may swap out. Nothing else changes. A production checklist should assert this, or set WithRequireLockedMemory and let NewClient refuse. Client.MemoryLockError says why.
func (*Client) Open ¶
func (c *Client) Open(ctx context.Context, plan *record.Plan, records []record.Sealed) ([]record.Source, error)
Open decrypts records sealed under the plan by any keyset of this client: each record is opened under the keyset that sealed it, with one ZeroKMS request for each 500 sealed values from each keyset. For generated code.
func (*Client) String ¶
String implements fmt.Stringer so that a Client printed with %v or %s shows its memory state: "locked", or the refusal. Nothing secret is printed. The state is what an operator reading a startup log needs to see, and Client.LogValue gives it structured form.
type ClientKey ¶
ClientKey is the ZeroKMS client key: long-lived key material that lives for the process. It is opaque — it prints a redaction under every verb and hands its bytes to no caller — and it is wiped once consumed.
It is the one type both guest packages share: auth reads one out of the developer profile, and this package consumes it. The alias is what makes a key read there the type taken here without auth importing this package, so a binary that only wants the profile does not carry the crypto guest.
func NewClientKey ¶
NewClientKey wraps key material — the CS_CLIENT_KEY hex form, or the base64 of secretkey.json — as a ClientKey. It takes ownership of b: the caller must not keep or reuse the slice, which is wiped along with the key.
type ClientOption ¶
type ClientOption func(*clientOptions)
ClientOption configures NewClient. Each one sets one thing; a later option setting the same thing wins. NewClient with none is a working client, with its credentials from AutoCredentials.
func WithCredentials ¶
func WithCredentials(c Credentials) ClientOption
WithCredentials supplies the client id, the client key and the auth strategy the token comes from. The default, and what nil means, is AutoCredentials: the environment, then the developer profile. NewCredentials takes the three explicitly, and OIDCFederation mints tokens from an identity provider's.
The client key is consumed: NewClient marshals it into the config buffer, wipes the key, and wipes the buffer once the guest has the key, so after NewClient returns — whatever the outcome, a configuration it refused included — the key is empty and the bytes it was built from are zero. A key is for one client. That holds for credentials a later WithCredentials replaces as well: they are consumed, not left holding a live key.
func WithKeysetCacheSize ¶
func WithKeysetCacheSize(n int) ClientOption
WithKeysetCacheSize sets how many keysets beyond the default the guest keeps loaded. Zero means the crate default (1024); negative is refused.
func WithRequireLockedMemory ¶
func WithRequireLockedMemory() ClientOption
WithRequireLockedMemory makes NewClient fail with ErrMemoryLock when the guest's memory cannot be locked in RAM or, on Linux, excluded from core dumps, instead of continuing with memory that may be swapped or dumped and reporting so through Client.MemoryLocked. It holds for the life of the client: a later growth of the guest's memory that cannot be locked is refused too, and what the guest already holds stays locked. When the growth was for a buffer the host is staging, the call fails with ErrMemoryLock and the client goes on. When it was for the guest's own allocation, the guest cannot report it: it aborts, and the client is closed with its keys wiped, the call still failing with ErrMemoryLock. Set it where swap is a real exposure and the deployment grants a lock limit with room for the guest to grow (RLIMIT_MEMLOCK on Linux; the error names the size held so far); see Client.MemoryLocked.
It covers the credential guest too, where the token strategy lives and the client key may have passed through. AutoCredentials and OIDCFederation open that guest under the same policy, so it is refused at NewClient and on every later growth alike. NewCredentials' guest is the auth store the caller opened: NewClient fails with ErrMemoryLock if that store's memory is unlocked when it is asked, but only the store's own policy governs its later growth, so open it with auth.RequireLockedMemory to hold it locked for the life of the client. Client.MemoryLocked reports the store's state live either way.
func WithTransport ¶
func WithTransport(rt http.RoundTripper) ClientOption
WithTransport performs the client's HTTP requests: to ZeroKMS, and, for AutoCredentials and OIDCFederation, the authentication requests auth's credential guest makes to CTS: an access-key exchange, a device-session refresh, a federation exchange. A RoundTripper scoped to the ZeroKMS host alone (a pinned client certificate, an egress allowlist) refuses those; the failure then surfaces as the token strategy's. Under NewCredentials the token exchange runs in the store the caller opened, not through this RoundTripper: pass auth.WithRoundTripper to that store instead. Nil means http.DefaultTransport, the default.
type Credentials ¶
type Credentials interface {
// contains filtered or unexported methods
}
Credentials is where a Client's ZeroKMS credentials come from: the client id, the client key, and the auth strategy that supplies the bearer token. NewClient resolves them once, host-side — the crypto guest is never given the environment or a filesystem to look them up itself — and hands the key to the guest.
AutoCredentials is the default: the environment, then the developer profile, in the Rust client's order. NewCredentials takes a client id, a client key and a strategy explicitly; OIDCFederation mints the token from an identity provider's. Those three are the only implementations: the interface is sealed, so a bearer token always comes from an auth strategy. A raw token cannot be refreshed when it expires, and a source outside the strategies would bypass the cross-process refresh lock the device session shares with the CLI (a refresh token used twice gets the whole chain revoked).
func AutoCredentials ¶
func AutoCredentials() Credentials
AutoCredentials is Credentials from the environment first, then the developer profile `stash auth login` writes — the order the Rust client (stack-encrypt's StackCipher::builder().init()) resolves them in, so one set of variables configures a service in either language:
- The token, by stack-auth's AutoStrategy order: CS_CLIENT_ACCESS_KEY (with CS_WORKSPACE_CRN, which is then required) exchanged for a token; else the current workspace's stored device session, refreshed under the same cross-process lock as the CLI. CS_CTS_HOST overrides the authentication endpoint.
- The client key: CS_CLIENT_ID and CS_CLIENT_KEY when both are set (the hex form, or the base64 of secretkey.json); else the current workspace's secretkey.json. Only one of the two set is the same as neither. Set but empty is an error, not a fall-through.
The profile is CS_CONFIG_PATH, else ~/.cipherstash; a profile that cannot be opened is not an error — an environment-only deployment has none — it just leaves the environment as the only source, as the Rust client does. Nothing found in either place is ErrNoCredentials, and when the profile would have been consulted, that error says why it could not be: a directory that does not exist, one that cannot be read, a path that is not a directory.
The profile and the token strategies run in auth's credential guest, not in the crypto guest, which still sees no environment and no filesystem. The credential guest lives as long as the client, and Client.Close releases it.
func NewCredentials ¶
func NewCredentials(clientID string, key *ClientKey, strategy *auth.Strategy) Credentials
NewCredentials is Credentials from explicit values: a client id, a client key (from NewClientKey, or auth's typed read), and the auth strategy that supplies the bearer token (ProfileStore's AccessKey, DeviceSession, OIDC or Auto). The key is consumed by the first NewClient given these credentials; a second is refused with ErrCredentialsConsumed, as a key is for one client. A nil strategy is refused by NewClient.
The caller opened the strategy's ProfileStore and the strategy, and keeps them: the client asks the strategy for a token on every ZeroKMS request but never closes it or the store. Both must stay open until Client.Close has returned, and are the caller's to close after it — the strategy, then the store (closing the store closes its strategies too). A token asked of a closed strategy is an error from the operation that needed it.
func OIDCFederation ¶
func OIDCFederation(crn string, provider auth.OIDCProvider, opts ...auth.StrategyOption) Credentials
OIDCFederation is Credentials whose token is minted by federation: CTS exchanges a token from the application's own identity provider (Clerk, Auth0, Okta, a cloud workload identity) for a CipherStash one in the workspace crn names. provider is asked on every token fetch for the IdP token of the user the call is for, and each distinct IdP token is exchanged once while its CipherStash token lasts, so one client serves many users without one user ever riding another's token; auth.OAuth2TokenSource adapts a golang.org/x/oauth2 source. The client key is resolved as AutoCredentials resolves it: CS_CLIENT_ID and CS_CLIENT_KEY, else the developer profile.
opts configure the federation strategy as they would auth.ProfileStore.OIDC: auth.WithBaseURL pins the CTS endpoint for these credentials alone (without it, CS_CTS_HOST overrides the endpoint, else it is discovered), and auth.WithCacheCapacity sets how many users' tokens are kept (1024 unless set).
type Date ¶
Date is a calendar date, with no time of day and no time zone: Year, Month and Day, in the proleptic Gregorian calendar, years -262143 to 262142. Validate reports a date that does not exist (February 30).
The zero Date (year 0, month 0, day 0) is not a date: a Date field must be set before it is encrypted or queried, and a zero one is refused with ErrEncoding, naming the field. An optional date is a *Date field: nil is stored as a sealed null as long as a date, or, with the tag's null=sql, as SQL NULL (see stashgen's README, "Optional fields").
type Decimal ¶
Decimal is a finite decimal number with its scale: "1.50" is not "1.5", and opens as it was sealed, except that a zero has no sign: -0.00 opens as 0.00, at the same scale. Its mantissa fits 96 bits and its scale is at most 28, rust_decimal's range. There is no NaN and no infinity: the type cannot hold one, and ParseDecimal refuses one. An equality term normalises the scale, so 1.5, 1.50 and 1.500 match each other.
func ParseDecimal ¶
ParseDecimal reads a decimal from its fixed-point text ("-1234.5600"), keeping its scale, without rounding. NaN and the infinities are ErrNonFiniteDecimal; a value with more than 28 digits after the point or a mantissa past 96 bits is refused too.
type Decrypter ¶
type Decrypter interface {
Open(ctx context.Context, plan *record.Plan, records []record.Sealed) ([]record.Source, error)
}
Decrypter opens records: a *Cipher, which refuses a record sealed under another keyset before any key is retrieved, or a *Client, which opens each record under the keyset that sealed it. A *Client opens only records that a cipher with no extension sealed, and checks no stored context field against a label: that is Cipher.Context. Generated Decrypt functions take one. Its method is for generated code; a program does not call it.
type Diagnostic ¶
type Diagnostic = guest.Diagnostic
Diagnostic is the full error behind a failure the guest reports, beside the kind: every such failure is a *Diagnostic wrapping one of the sentinels above, so errors.Is matches the kind and errors.As reads the rest:
var d *encrypt.Diagnostic
if errors.As(err, &d) {
log.Printf("%s: %s (%s)", d.Code, d.Message, d.Help)
}
Its fields are Code ("stack_encrypt::foreign_keyset", "stack_kms::keyset_not_found", ...; stable), Message (what Error returns after the sentinel's text), Help, URL, Severity, Fields (the structured fields, by name) and Causes (the errors behind it, outermost first). Accessors read the fields a caller branches on: ExpectedKeyset and FoundKeyset on an ErrForeignKeyset (each a KeysetID's bytes); Field and Reason on a refused plan, record or value; and Construct and At on a refused Match predicate, the construct in the grammar's words and the byte offset in the predicate where it starts.
What one may carry is fixed: keyset ids and names, field names, counts, index kinds, ZeroKMS request kinds and HTTP statuses, and a refused predicate's construct and offset. Never plaintext, key material, tokens, ciphertext or term bytes, or the values of an encryption context.
Errors the client raises itself carry none: a closed client (ErrState), a guest that did not return, ErrMemoryLock, and an argument refused before it reached the guest. A guest built before the detail existed returns the bare sentinel too.
It is the same type as auth.Diagnostic, by identity.
type EQLMatchQuery ¶
EQLMatchQuery is a matches query on a field that names an EQL type with a match index, such as TextMatch: the type's query value to bind in SQL (EQLMatchQuery.Query, cast to the type's query domain), and the same two questions a MatchQuery leaves, EQLMatchQuery.Verify and EQLMatchQuery.TargetLimit.
Like a MatchQuery it keeps the text it was derived from, for Verify, and never prints it. Q is the EQL query type (eql.TextMatchQuery), T the field's Go type. The zero EQLMatchQuery holds no query, and its methods return ErrEncoding.
func NewEQLMatchQuery ¶
func NewEQLMatchQuery[Q ~[]byte, T ~string](ctx context.Context, cph *Cipher, plan *record.Plan, field string, text T) (EQLMatchQuery[Q, T], error)
NewEQLMatchQuery derives the matches query for text on one field of the plan that names an EQL type, under this cipher's keyset, through Cipher.QueryTerm. For generated code: a program calls the field's Matches method.
func (EQLMatchQuery[Q, T]) Format ¶
func (q EQLMatchQuery[Q, T]) Format(f fmt.State, _ rune)
Format prints EQLMatchQuery.String for every verb fmt hands it.
func (EQLMatchQuery[Q, T]) GoString ¶
func (q EQLMatchQuery[Q, T]) GoString() string
GoString is what %#v prints: EQLMatchQuery.String.
func (EQLMatchQuery[Q, T]) LogValue ¶
func (q EQLMatchQuery[Q, T]) LogValue() slog.Value
LogValue is what slog records: the field, the domain and the value's size.
func (EQLMatchQuery[Q, T]) Query ¶
func (q EQLMatchQuery[Q, T]) Query() Q
Query is the EQL query value to bind in SQL, cast to its query domain. Nil for the zero EQLMatchQuery.
func (EQLMatchQuery[Q, T]) String ¶
func (q EQLMatchQuery[Q, T]) String() string
String names the field, the query domain and the value's size, never the text.
func (EQLMatchQuery[Q, T]) TargetLimit ¶
func (q EQLMatchQuery[Q, T]) TargetLimit(ctx context.Context, want uint64, stats Stats) (Limit, error)
TargetLimit is how many rows to fetch so that, at the confidence stats names, want of them verify: the LIMIT to send with the query. With no measured Stats.Histogram the limit is unbounded, and a caller pages with Limit.FetchOr. Uses no key.
func (EQLMatchQuery[Q, T]) Value ¶
func (q EQLMatchQuery[Q, T]) Value() (driver.Value, error)
Value implements driver.Valuer: the query value's JSON text. The zero EQLMatchQuery returns ErrEncoding, as the zero MatchQuery does.
func (EQLMatchQuery[Q, T]) Verify ¶
func (q EQLMatchQuery[Q, T]) Verify(ctx context.Context, values []T) ([]bool, error)
Verify reports, for each of a page of decrypted values of the field, whether it really holds the query's text, in order: one call into the engine for the page, with no key, under the EQL type's match options. Keep the rows it reports true.
type ElementPredicate ¶
type ElementPredicate[E any] struct { // contains filtered or unexported fields }
ElementPredicate is one test on a member of the elements of the array E, for that array's generated Match. Make one with AtPath, or with the generated array accessor's Member.
Its member names one field of each element, relative to the element: relationship, or address.city, written as the field's ordered path is written below the array. Its full path must be one the field orders.
func (ElementPredicate[E]) Format ¶
func (p ElementPredicate[E]) Format(f fmt.State, _ rune)
Format prints ElementPredicate.String for every verb fmt hands it.
func (ElementPredicate[E]) GoString ¶
func (p ElementPredicate[E]) GoString() string
GoString is what %#v prints: ElementPredicate.String.
func (ElementPredicate[E]) LogValue ¶
func (p ElementPredicate[E]) LogValue() slog.Value
LogValue is what slog records: the member and the comparison, and [redacted] for the values.
func (ElementPredicate[E]) Member ¶
func (p ElementPredicate[E]) Member() string
Member is the member the predicate tests, relative to an element.
func (ElementPredicate[E]) Op ¶
func (p ElementPredicate[E]) Op() string
Op is the comparison, as the engine names it: eq, lt, lte, gt, gte or between; "" for a zero ElementPredicate.
func (ElementPredicate[E]) String ¶
func (p ElementPredicate[E]) String() string
String names the member and the comparison, and hides the values.
func (ElementPredicate[E]) Values ¶
func (p ElementPredicate[E]) Values() []any
Values are a copy of the predicate's values: one, or for between the low bound then the high. They are the plaintext the caller gave. The slice and the bytes of a json.RawMessage or []byte in it are copies, so changing them does not change the predicate; a value of another reference type is the caller's own. For generated code.
type EqualityTerm ¶
type EqualityTerm []byte
EqualityTerm is a PRF equality term. Two terms derived under the same keyset and context from equal values are equal bytes; nothing else about the value is revealed.
func (EqualityTerm) Equal ¶
func (t EqualityTerm) Equal(other EqualityTerm) bool
Equal compares two equality terms in constant time.
type Index ¶
type Index interface {
// Output is the index's wire key.
Output() record.Output
// String is the index's tag word.
String() string
}
Index is one index on a field, as a generated declaration names it: Equality, Ore, Ope, Match or JSON. The words are the Rust API's words for the same behaviour, and a declaration in any language spells them the same way.
var ( // Equality is a PRF equality term: 32 bytes, compared with // [EqualityTerm.Equal]. Defined for integers, strings and bytes. Equality Index = index(record.Equality) // Ore is an order-revealing (CLLW ORE) term over any scalar. Ore Index = index(record.Ore) // Ope is an order-preserving (CLLW OPE) term over any scalar. Ope Index = index(record.Ope) )
The indexes that take no options.
func JSON
deprecated
func JSON(options ...JSONOption) Index
JSON is the index over a JSON document as an output beside a ciphertext, which the engine does not derive: a declaration that names it is refused by stashgen, and at run time by the engine.
Deprecated: store a JSON document as a struct, map, slice or json.RawMessage field with `encrypt_into=JsonSearch`, whose EQL type carries the JSON index.
func Match ¶
func Match(options ...MatchOption) Index
Match is a full-text match index: the tokenized positions of a string. Strings only.
type Int128 ¶
Int128 is a signed 128-bit integer, two's complement, little-endian. ParseInt128 reads one from its decimal text, and String writes it.
func ParseInt128 ¶
ParseInt128 reads a signed 128-bit integer from decimal text, refusing a value outside its range.
type JSONOption ¶
type JSONOption interface {
// contains filtered or unexported methods
}
JSONOption is an option of the json index.
type JSONRange ¶
type JSONRange[Q ~[]byte] struct { Low Q High Q }
JSONRange is both bounds of a between at a path, each an ordering operand Q, low then high, inclusive.
type JSONRead ¶
type JSONRead[E ~[]byte] struct { // contains filtered or unexported fields }
JSONRead is a prepared subtree read of a JSONSearch field: the tokens to bind, and what opens the rows eql_v4.json_tree_subtree returns for them. It keeps the request it was prepared from (the field's declaration, the path, the bound and the cipher's keyset); the engine derives the read again from it, and checks every row against it. E is the column's entry type, eql.JSONEntry.
func NewJSONRead ¶
func NewJSONRead[E ~[]byte](c *Cipher, declaration *record.Plan, field string, input vcvalue.Object, tokens []string) JSONRead[E]
NewJSONRead is a read prepared by Cipher.QueryTerm's subtree operator on field of the record type that declaration declares, from input, its {"path", "wildcard_bound"?} object. For generated code.
func (JSONRead[E]) Format ¶
Format prints JSONRead.String for every verb fmt hands it.
func (JSONRead[E]) GoString ¶
GoString is what %#v prints: JSONRead.String.
func (JSONRead[E]) LogValue ¶
LogValue is what slog records: the field, [redacted] for the path, and how many tokens.
func (JSONRead[E]) Open ¶
Open opens the rows the read returned, one group per database row, with one ZeroKMS request: per row, the value at each concrete path the read named that the row has, in document order. A row with no node there opens to no value. Past its walk budget the database returns the sealed root instead, and the read's paths are evaluated over the whole document to the same values; only those values come back. A row that is not one the read asked for, or a wildcard read of an array longer than the bound, is ErrEncoding.
Opening checks each row against the read, not that the database returned every row it should have, nor that the rows came from the row the caller meant.
func (JSONRead[E]) OpenOne ¶
OpenOne opens one database row's entries to the one value at the read's path: Found with its JSON, or not Found where the row has no node there. A read that reaches more than one value (a wildcard) is ErrEncoding; open it with JSONRead.Open.
func (JSONRead[E]) String ¶
String names the field and counts the tokens, and hides the path and the tokens: a path's keys can be a customer's data, and a token is a capability.
func (JSONRead[E]) Tokens ¶
func (r JSONRead[E]) Tokens() JSONTokens
Tokens are the tokens to bind: `eql_v4.json_tree_subtree(col, $1::text[], 64)`.
type JSONReadOptions ¶
type JSONReadOptions struct {
// WildcardBound is how many elements each [*] in the path is expanded
// to; 0 is the engine's default, 64. A read of an array longer than the
// bound fails rather than open to part of it ([ErrEncoding], with the
// diagnostic code stack_encrypt::json_wildcard_truncated). Two wildcard
// levels send bound² + bound + 1 tokens, and the engine refuses a read
// past 8,192.
WildcardBound uint32
}
JSONReadOptions are a subtree read's options.
type JSONSelection ¶
type JSONSelection[E ~[]byte] struct { // contains filtered or unexported fields }
JSONSelection is a prepared `->` on a JSONSearch field: the selector to bind, and what opens the entries the database returns for it. It keeps the request it was prepared from (the field's declaration, the path and the cipher's keyset), so the entries open only as answers to it: the engine derives the selector again and refuses an entry that does not carry it. E is the column's entry type, eql.JSONEntry.
func NewJSONSelection ¶
func NewJSONSelection[E ~[]byte](c *Cipher, declaration *record.Plan, field, path, selector string) JSONSelection[E]
NewJSONSelection is a selection prepared by Cipher.QueryTerm's selector operator on field of the record type that declaration declares, at path. For generated code.
func (JSONSelection[E]) Format ¶
func (s JSONSelection[E]) Format(f fmt.State, _ rune)
Format prints JSONSelection.String for every verb fmt hands it.
func (JSONSelection[E]) GoString ¶
func (s JSONSelection[E]) GoString() string
GoString is what %#v prints: JSONSelection.String.
func (JSONSelection[E]) LogValue ¶
func (s JSONSelection[E]) LogValue() slog.Value
LogValue is what slog records: the field, and [redacted] for the path and the selector.
func (JSONSelection[E]) Open ¶
func (s JSONSelection[E]) Open(ctx context.Context, rows []*E) ([]JSONValue, error)
Open opens the entries `col -> selector` returned, one per database row, nil for a row where it returned SQL NULL, with one ZeroKMS request. Each opens to the scalar it holds (Found), or for the sealed root at "$" the whole document; a nil opens to no value. A container's entry is refused: read a container with a subtree read. An entry that is not the selector's is ErrEncoding.
func (JSONSelection[E]) Selector ¶
func (s JSONSelection[E]) Selector() JSONSelector
Selector is the selector to bind: `col -> $1::text`.
func (JSONSelection[E]) String ¶
func (s JSONSelection[E]) String() string
String names the field and hides the path and the selector: a path's keys can be a customer's data.
type JSONSelector ¶
type JSONSelector string
JSONSelector is a JSON path's selector: what `->`, `->>` and the path functions of a JSONSearch column take. It binds as SQL text; cast it, `col -> $1::text`, so PostgreSQL picks EQL's operator and not jsonb's. It is column-wide: the same path has the same selector in every row.
type JSONTerm
deprecated
type JSONToken ¶
type JSONToken string
JSONToken is one node's token: what eql_v4.json_tree_children and eql_v4.json_tree_child_count take. It binds as SQL text. A token is a capability over the node's subtree in every row of the column: send it to the database for a query, and nowhere else. It prints and logs redacted.
func (JSONToken) Format ¶
Format prints JSONToken.String for every verb fmt hands it.
func (JSONToken) GoString ¶
GoString is what %#v prints: JSONToken.String.
type JSONTokens ¶
type JSONTokens []string
JSONTokens are a subtree read's tokens: what eql_v4.json_tree_subtree takes. They bind as a PostgreSQL text[] literal; cast the parameter, `$2::text[]`. Like a JSONToken, each is a capability, and they print and log redacted.
func (JSONTokens) Format ¶
func (t JSONTokens) Format(f fmt.State, _ rune)
Format prints JSONTokens.String for every verb fmt hands it.
func (JSONTokens) GoString ¶
func (t JSONTokens) GoString() string
GoString is what %#v prints: JSONTokens.String.
func (JSONTokens) LogValue ¶
func (t JSONTokens) LogValue() slog.Value
LogValue is what slog records: how many tokens, never the tokens.
func (JSONTokens) String ¶
func (t JSONTokens) String() string
String counts the tokens and hides them: each is a capability.
func (JSONTokens) Value ¶
func (t JSONTokens) Value() (driver.Value, error)
Value binds the tokens as a text[] literal. A token is base64, so it needs no escape inside the quotes. Empty JSONTokens (a zero JSONRead's) are refused, as a zero JSONToken is: bound as {}, they would read nothing from every row.
type JSONValue ¶
type JSONValue struct {
Found bool
JSON json.RawMessage
}
JSONValue is one value a read reached, or none. Found is false for no value at the path (the row has no node there, or SQL NULL); a JSON null that is there is Found, with JSON "null". JSON is the value's JSON text: plaintext, which the caller owns. JSONValue prints its JSON in the clear: it has no String or LogValue method, so do not log one.
type KeysetID ¶
type KeysetID [16]byte
KeysetID is a keyset's UUID, the identity a sealed leaf carries. It selects a keyset by id; ids are never re-resolved.
func ParseKeysetID ¶
ParseKeysetID parses a canonical hyphenated UUID.
type KeysetName ¶
type KeysetName string
KeysetName selects a keyset by name. Its first use on a client is one ZeroKMS round trip; the binding is cached by the guest for a bounded window, after which the name is resolved again.
type KeysetSelector ¶
type KeysetSelector interface {
// contains filtered or unexported methods
}
KeysetSelector names the keyset a call binds to, as Rust's IdentifiedBy does: KeysetName or KeysetID. Every variant is spelled, there is no empty-string or nil sentinel; the default keyset is not a selector but Client.DefaultKeyset, exactly as it is a method and not an IdentifiedBy variant in Rust. Names follow ZeroKMS's rules (non-empty, at most 64 bytes, of A-Z a-z 0-9 _ - /), checked by the guest before any request is made; ZeroKMS itself never issues a UUID-shaped name, so a name and an id cannot be confused.
type Limit ¶
type Limit struct {
// contains filtered or unexported fields
}
Limit is how many rows a probabilistic query should fetch: a number, or unbounded when the statistics say nothing of the stored filters or the confidence is not below 1, in which case a caller pages with a cursor.
type MatchOption ¶
type MatchOption interface {
// contains filtered or unexported methods
}
MatchOption is an option of the match index. None is defined yet: the record plan can carry match options, but the guest derives a query term under the default options only, so a stored term with other options would match no query.
type MatchQuery ¶
type MatchQuery[T ~string] struct { // contains filtered or unexported fields }
MatchQuery is a matches query on a field declared with a match index: the term to bind in SQL, and the two questions its probabilistic answer leaves, MatchQuery.Verify and MatchQuery.TargetLimit. The server answers a matches query with every row whose stored filter holds the query's bits, which can include rows that do not hold the query's text, so a caller decrypts the rows it fetched and verifies them.
A MatchQuery keeps the text it was derived from, because Verify cuts that text's tokens again in the engine, with the field's own tokenizer: no term carries them, as they are plaintext. Go cannot wipe a string, so the text stays in memory until the garbage collector reclaims it, as the string the caller passed in does. A MatchQuery never prints it: fmt, slog and %#v show the field and the term's size only, and the text sits behind a pointer, so a verb fmt reports as wrong (%p) or a MatchQuery in an unexported struct field, which fmt prints by reflection, shows an address.
T is the field's Go type. The zero MatchQuery holds no query, and its methods return ErrEncoding.
func NewMatchQuery ¶
func NewMatchQuery[T ~string](ctx context.Context, cph *Cipher, plan *record.Plan, field string, text T) (MatchQuery[T], error)
NewMatchQuery derives the matches query for text on one field of the plan under this cipher's keyset, through Cipher.QueryTerm. For generated code: a program calls the field's Matches method.
func (MatchQuery[T]) Format ¶
func (q MatchQuery[T]) Format(f fmt.State, _ rune)
Format prints MatchQuery.String for every verb fmt hands it.
func (MatchQuery[T]) GoString ¶
func (q MatchQuery[T]) GoString() string
GoString is what %#v prints: MatchQuery.String.
func (MatchQuery[T]) LogValue ¶
func (q MatchQuery[T]) LogValue() slog.Value
LogValue is what slog records: the field and the term's size.
func (MatchQuery[T]) String ¶
func (q MatchQuery[T]) String() string
String names the field and the term's size, never the text.
func (MatchQuery[T]) TargetLimit ¶
TargetLimit is how many rows to fetch so that, at the confidence stats names, want of them verify: the LIMIT to send with the query. A field declared with tags states no expected token count, so with no measured Stats.Histogram the limit is unbounded, and a caller pages with Limit.FetchOr. Uses no key.
func (MatchQuery[T]) Term ¶
func (q MatchQuery[T]) Term() MatchTerm
Term is the match term to bind in SQL: the bits the query sets in the field's filter, which a stored filter must hold for the row to match. Nil for the zero MatchQuery.
func (MatchQuery[T]) Value ¶
func (q MatchQuery[T]) Value() (driver.Value, error)
Value implements driver.Valuer: the term's bytes. The zero MatchQuery returns ErrEncoding rather than binding NULL, which would match no row without an error.
func (MatchQuery[T]) Verify ¶
func (q MatchQuery[T]) Verify(ctx context.Context, values []T) ([]bool, error)
Verify reports, for each of a page of decrypted values of the field, whether it really holds the query's text, in order: one call into the engine for the page, with no key. Keep the rows it reports true; the others are the false positives of the server's answer.
type MatchTerm ¶
type MatchTerm []byte
MatchTerm is a full-text match term: the positions of the value's tokens in the keyset's token space, as little-endian uint16s.
type MemberTest ¶
type MemberTest[E, T any] struct { // contains filtered or unexported fields }
MemberTest makes the predicates on one member of an array's elements. Make one with AtPath, or with the generated array accessor's Member for a member named at run time, whose T is any.
Each comparison is of the member's ordering term, as the orderings at a path compare: numbers order as float64, so integers past 2^53 can tie, and text orders case- and accent-folded. Equality is both inclusive bounds at one value, so MemberTest.Eq is an equality of those folded values.
A value is a Go number or string, or a json.RawMessage of JSON text, written to JSON as the orderings at a path write theirs. It is plaintext: an ElementPredicate prints and logs its member and comparison, and never its values. A predicate copies the bytes of a json.RawMessage or a []byte value, so changing the caller's bytes does not change it. A value of any other reference type, such as a map, a slice or a pointer, is not copied: changing what it refers to changes the predicate.
func AtPath ¶
func AtPath[E, T any](p Path[E, T]) MemberTest[E, T]
AtPath is the comparisons of the member at p, for the element match. Each returns an ElementPredicate of p's array, so only that array's Match takes it, and each takes a value of the member's Go type:
encrypt.AtPath(Paths.Profile.Visits.At).Gt(15)
AtPath is a function, not a method of Path, because a Go method cannot take type parameters of its own.
func (MemberTest[E, T]) Between ¶
func (m MemberTest[E, T]) Between(lo, hi T) ElementPredicate[E]
Between is the test that the member is from lo to hi, inclusive.
func (MemberTest[E, T]) Eq ¶
func (m MemberTest[E, T]) Eq(v T) ElementPredicate[E]
Eq is the test that the member equals v, compared by its ordering term: text is case- and accent-folded, so "sibling" equals "Sibling", and integers past 2^53 can tie. For an exact equality of one member, use the field's EqAt at the member's full path, which any element can answer.
func (MemberTest[E, T]) Gt ¶
func (m MemberTest[E, T]) Gt(v T) ElementPredicate[E]
Gt is the test that the member is greater than v.
func (MemberTest[E, T]) Gte ¶
func (m MemberTest[E, T]) Gte(v T) ElementPredicate[E]
Gte is the test that the member is at least v.
func (MemberTest[E, T]) Lt ¶
func (m MemberTest[E, T]) Lt(v T) ElementPredicate[E]
Lt is the test that the member is less than v.
func (MemberTest[E, T]) Lte ¶
func (m MemberTest[E, T]) Lte(v T) ElementPredicate[E]
Lte is the test that the member is at most v.
type OpeTerm ¶
type OpeTerm []byte
OpeTerm is an order-preserving term (CLLW OPE): the raw term bytes, ordered as the values they encode under plain byte order, so a database compares them with no custom operator.
type OreTerm ¶
type OreTerm []byte
OreTerm is an order-revealing term (CLLW ORE): the raw term bytes. Two terms derived under the same keyset and context order as their plaintexts through Compare and Less. Plain byte order is almost always the same order, and sometimes not, with no error: do not compare ORE terms in SQL (ORDER BY, <, >) on a byte column. Compare them in Go, or store the field as an EQL type, which the database compares correctly.
func (OreTerm) Compare ¶
Compare orders two ORE terms as their plaintexts: -1, 0 or +1. Ports the CLLW comparison of the Rust crate (constant time over the term bytes): at the first differing byte the two sides share the PRF block, so they differ by exactly the plaintext bit, and the side one greater is the greater plaintext. Terms of different lengths (strings, byte slices) compare on their common prefix, then the shorter is less.
type Path ¶
type Path[E, T any] struct { // contains filtered or unexported fields }
Path is one ordered member of the elements of an array in a JSONSearch field, for the element match: the generated Fields.<Field>.Elements.<Array>.Match. Read one from the generated Paths, which names each ordered member as the JSON nests it: Paths.Profile.Visits.Place is the member place of each element of $.visits[*].
E is the array: a type the generator writes for each array, which the array's Match takes predicates of. T is the member's Go type in the document's struct, or any when the generator cannot name one (a json.RawMessage document, say). Pass a Path to AtPath for a predicate.
type QueryTerm ¶
type QueryTerm struct {
// Operator is the operator the term answers: eq, lt, lte, gt, gte,
// between or matches.
Operator string
// Fidelity is how far the server's answer can be trusted. A
// [record.Probabilistic] term's rows are checked with [Cipher.Verify]
// after decrypt, and its fetch sized with [Cipher.TargetLimit].
Fidelity record.Fidelity
// Index is the field's index that answers the operator.
Index record.Output
// Domain is the PostgreSQL domain the term is cast to, for a field
// that names an EQL type; "" for a field with indexes, whose terms are
// bytes.
Domain string
// Terms is the term: one, or for between two, the low bound's then the
// high's. For a field that names an EQL type it is the query value's
// JSON. Empty when the term is Text.
Terms [][]byte
// Encoding says what the term holds: bytes (an index term), eql (an EQL
// query value's JSON), text (a JSON path's selector or one node's
// token, in Text[0]) or text_array (a read's tokens, in Text).
Encoding string
// Text is the term as text, for the text and text_array encodings: what
// a document's selector, children, child_count and subtree operators
// derive. It binds as SQL text, or text[].
Text []string
}
QueryTerm is the query term the engine derives for one field, an operator and its input.
type Range ¶
type Range[T OreTerm | OpeTerm] struct { // Low is the low bound's term. Low T // High is the high bound's term. High T }
Range is a between query on a field declared with an order index: the term of the low bound and the term of the high bound, both inclusive, derived under one context. Its answer is exact, so it has nothing to verify. A range binds as two parameters, one for each bound, and each bound implements driver.Valuer.
An OreTerm does not order as its plaintext under plain byte order: a Range of them is compared in Go, with OreTerm.Compare, not in SQL on a byte column. A Range of OpeTerm values orders under byte order, so a database compares it with no custom operator.
type Stats ¶
type Stats struct {
// Rows is the rows the query scans, such as reltuples from pg_class
// read with a reltuples >= 0 filter. reltuples is -1 for a table never
// vacuumed or analysed: that is an unknown count, not an empty table,
// and a count of 0 would promise a fetch of exactly want. While the
// count is unknown, call no TargetLimit and page with a cursor, or run
// ANALYZE first.
Rows uint64
// Histogram is a measured histogram of the stored filters' set-bit
// counts. With none, the engine uses the index's declared prior, and
// a field declared as data has none, so the limit is unbounded.
Histogram []Bucket
// Confidence is the probability that the fetch holds the rows wanted
// when the table has that many; 0 is the engine's default, 99%.
Confidence float64
}
Stats is what Cipher.TargetLimit knows of the table: how many rows the query scans, how full their stored filters are, and the confidence to size the fetch at. None of it is secret: the server holds the filters and the catalog holds the row count.
type Uint128 ¶
Uint128 is an unsigned 128-bit integer, little-endian. ParseUint128 reads one from its decimal text, and String writes it.
func ParseUint128 ¶
ParseUint128 reads an unsigned 128-bit integer from decimal text, refusing a negative value or one outside its range.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package eql holds the EQL types a program stores: one Go type per EQL type, each holding the EQL value as the JSON bytes PostgreSQL stores in the type's domain, and the query types that match them.
|
Package eql holds the EQL types a program stores: one Go type per EQL type, each holding the EQL value as the JSON bytes PostgreSQL stores in the type's domain, and the query types that match them. |
|
Command example encrypts a batch of users against real ZeroKMS, using the credentials `stash auth login` leaves in the developer profile (or the CS_* environment variables, which win), and reads them back.
|
Command example encrypts a batch of users against real ZeroKMS, using the credentials `stash auth login` leaves in the developer profile (or the CS_* environment variables, which win), and reads them back. |
|
json
command
Command json demonstrates a typed encrypted JSON document in PostgreSQL.
|
Command json demonstrates a typed encrypted JSON document in PostgreSQL. |
|
Package gensupport holds what only code written by stashgen calls.
|
Package gensupport holds what only code written by stashgen calls. |
|
internal
|
|
|
eqlguest
Package eqlguest is where package eql hands the guest build with the EQL types to package encrypt.
|
Package eqlguest is where package eql hands the guest build with the EQL types to package encrypt. |
|
genmember
command
Command genmember writes testpolicy's generated file from a policy: the stashgen.Generate path, over testmember.Member, with the declaration testusers.User carries as tags.
|
Command genmember writes testpolicy's generated file from a policy: the stashgen.Generate path, over testmember.Member, with the declaration testusers.User carries as tags. |
|
testmember
Package testmember holds a struct the encrypt tests encrypt through the two generator paths that take a type the program does not own: the `-for` flag (testusers declares the tags for Member in a struct of its own) and stashgen.Generate from a policy (genmember writes testpolicy).
|
Package testmember holds a struct the encrypt tests encrypt through the two generator paths that take a type the program does not own: the `-for` flag (testusers declares the tags for Member in a struct of its own) and stashgen.Generate from a policy (genmember writes testpolicy). |
|
testpolicy
Package testpolicy holds the file stashgen.Generate writes for testmember.Member from a policy (see internal/genmember): the generated Encrypt, Decrypt and Fields over *testmember.Member, with the declaration testusers.User carries as tags.
|
Package testpolicy holds the file stashgen.Generate writes for testmember.Member from a policy (see internal/genmember): the generated Encrypt, Decrypt and Fields over *testmember.Member, with the declaration testusers.User carries as tags. |
|
testusers
Package testusers holds the tagged structs the encrypt tests encrypt through the generated API: a record in separate columns, an opaque struct, and a probe struct with one field of each scalar type for the term ordering tests.
|
Package testusers holds the tagged structs the encrypt tests encrypt through the generated API: a record in separate columns, an opaque struct, and a probe struct with one field of each scalar type for the term ordering tests. |
|
Package policy decides how a type that cannot carry stash tags is encrypted, from what its schema says about each field.
|
Package policy decides how a type that cannot carry stash tags is encrypted, from what its schema says about each field. |
|
protosource
Package protosource reads the facts about a protobuf message's fields: each field's name, its Go name, its kind, and its options as annotations.
|
Package protosource reads the facts about a protobuf message's fields: each field's name, its Go name, its kind, and its options as annotations. |
|
protosource/internal/testpb
Package testpb is protoc-gen-go output for the protosource tests: the two messages of docs/plans/2026-10-04-plan-builder/proto, where classification.data_categories is a repeated string field option.
|
Package testpb is protoc-gen-go output for the protosource tests: the two messages of docs/plans/2026-10-04-plan-builder/proto, where classification.data_categories is a repeated string field option. |