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:
@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).
@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).
- 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.
Summary
@cipherstash/stackhas 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 oneEncryption()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) takesDecryptOptions { keyset_id: Option<Uuid>, … }and resolvesopts.keyset_id.or(Some(self.keyset_id)).ZeroKMSWithClientKey::{encrypt, decrypt, decrypt_single}all take a per-callkeyset_id.@cipherstash/protect-ffi(0.31.0) exposes none of this: the JSEncryptOptions/DecryptOptions/EncryptQueryOptionshave no keyset field, and every native call site passesNone(client's bound keyset) or goes through the scoped cipher.Current behaviour (for context)
EncryptedRecordembeds 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.:
Rough shape of the work:
@cipherstash/protect-ffi: addkeyset?: KeysetIdentifierto the operation option types and thread it through to the SDK's per-callkeyset_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).@cipherstash/stack: surface the option on the operation builders and pass it through; document the interaction withconfig.keyset(per-op wins, falls back to the client keyset).skills/stash-zerokms/SKILL.md, which currently (correctly) documents that no per-operation keyset exists, plus theEncryption()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.