Skip to content

Support specifying a keyset per operation (encrypt/query), not only per client #832

Description

@coderdan

Summary

@cipherstash/stack has no way to specify a keyset for a single operation. The keyset is fixed at client construction (config.keyset), so encrypt and query are always bound to that keyset for the client's lifetime, and multi-tenant work requires one Encryption() client per tenant.

The underlying Rust SDK already supports per-operation keysets — this is purely an FFI/JS plumbing gap:

  • ScopedCipher::decrypt (cipherstash-client 0.42.0) takes DecryptOptions { keyset_id: Option<Uuid>, … } and resolves opts.keyset_id.or(Some(self.keyset_id)).
  • The unscoped ZeroKMSWithClientKey::{encrypt, decrypt, decrypt_single} all take a per-call keyset_id.
  • @cipherstash/protect-ffi (0.31.0) exposes none of this: the JS EncryptOptions / DecryptOptions / EncryptQueryOptions have no keyset field, and every native call site passes None (client's bound keyset) or goes through the scoped cipher.

Current behaviour (for context)

  • Encrypt + query: strictly the client's bound keyset. The search index key is loaded per keyset at client init, so query terms cannot be produced for another keyset without a second client.
  • Decrypt: already effectively per-payload — every EncryptedRecord embeds the keyset id it was encrypted under, and the FFI decrypts through the unscoped client, so key retrieval routes to each payload's own keyset (subject to the client holding a grant for it). No API change needed for cross-keyset reads.

Proposed change

Expose a per-operation keyset in the JS surface, e.g.:

await client.encrypt(value, { table, column, keyset: { name: 'tenant-b' } })
await client.encryptQuery(value, { table, column, keyset: { name: 'tenant-b' } })

Rough shape of the work:

  1. @cipherstash/protect-ffi: add keyset?: KeysetIdentifier to the operation option types and thread it through to the SDK's per-call keyset_id (decrypt is trivial; encrypt/query need the target keyset's index key, so the FFI would need to load/cache index keys per keyset rather than only the bound one).
  2. @cipherstash/stack: surface the option on the operation builders and pass it through; document the interaction with config.keyset (per-op wins, falls back to the client keyset).
  3. Update skills/stash-zerokms/SKILL.md, which currently (correctly) documents that no per-operation keyset exists, plus the Encryption() JSDoc keysets section.

The index-key point in (1) is the substantive part: per-operation encrypt/query implies the client can hold index keys for multiple keysets, which changes the current one-ScopedCipher-per-client model in the FFI.

Priority

Not a release blocker; should be addressed soon after.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions