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 or later):
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,index=equality;match"`
Age uint32 `stash:"age,encrypt,index=equality;ore"`
}
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.Equality(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.
6. Store the encrypted type. Each sealed field is one or more columns of bytes (Ciphertext and the term types), each implementing driver.Valuer and sql.Scanner, so a database library binds and scans each one as bytes; map each one to its own column.
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.
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 time.Time, a driver.Valuer). An opaque struct crosses as one JSON document and is one column.
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 needs text with at least one token: the engine derives no match term for an empty string, separator-only text, or text shorter than the n-gram length (3 characters), because an empty term would match every row. Encrypt then fails for the whole batch with ErrTerm, naming the row, the field and the index, so an optional or short value does not belong under match.
Transport and auth ¶
The guest imports exactly two host functions: an HTTP send, served by any net/http.RoundTripper, and a bearer-token fetch, served by the credentials' auth strategy. 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: 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
- 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) Derive(ctx context.Context, plan *record.Plan, field string, output record.Output, ...) ([]byte, error)
- func (cph *Cipher) Extend(parts ...any) *Cipher
- func (cph *Cipher) Extension() []any
- 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) Query(ctx context.Context, plan *record.Plan, field string, value any) ([]byte, error)
- func (cph *Cipher) Seal(ctx context.Context, plan *record.Plan, rows []record.Source) ([]record.Sealed, 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 Decrypter
- type Diagnostic
- type EqualityTerm
- type Index
- type JSONOption
- type JSONTerm
- type KeysetID
- type KeysetName
- type KeysetSelector
- type MatchOption
- type MatchTerm
- type OpeTerm
- type OreTerm
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.
Functions ¶
This section is empty.
Types ¶
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].
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) Derive ¶
func (cph *Cipher) Derive(ctx context.Context, plan *record.Plan, field string, output record.Output, value any) ([]byte, error)
Derive derives one index term for one value of a field, under the plan's context for that field and this cipher's extension: the term a query compares against the stored one. A plan that takes its context from a field has none of its own, so the term derives under the context named with Cipher.Context, and a cipher that names none is refused as ErrEncoding. For generated code.
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) 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) Query ¶
func (cph *Cipher) Query(ctx context.Context, plan *record.Plan, field string, value any) ([]byte, error)
Query derives the EQL query value of a field that names an EQL type, for one value: the operand that matches stored values of the field, as the JSON bytes the field's eql_v4.query_* domain takes. The engine runs the EQL type's own query plan; 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.
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 — 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 JSON document 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; calls are serialised internally, because a wasm instance is single-threaded. Close it when done, as with any resource.
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 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), and Field and Reason on a refused plan, record or value.
What one may carry is fixed: keyset ids and names, field names, counts, index kinds, ZeroKMS request kinds and HTTP statuses. 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 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 ¶
func JSON(options ...JSONOption) Index
JSON is the index over a JSON document. The engine does not derive it yet: a declaration that names it is refused by stashgen, and at run time by the engine.
func Match ¶
func Match(options ...MatchOption) Index
Match is a full-text match index: the tokenized positions of a string. Strings only.
type JSONOption ¶
type JSONOption interface {
// contains filtered or unexported methods
}
JSONOption is an option of the json index.
type JSONTerm ¶
type JSONTerm []byte
JSONTerm is the term of the json index. The engine does not derive it yet; the type exists so a generated declaration that names JSON has a Go type to fail into.
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 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 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 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.
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. |
|
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. |