Skip to content

Keyset mismatch produces valid-looking ciphertext that is silently un-searchable (rc.3 F4) #735

Description

@coderdan

Summary

stash env and stash auth login can resolve different keysets. EQL v3 index terms (hm / bf / op) are keyset-specific, so data encrypted under one keyset and queried under another is decryptable but completely un-searchable — every search returns 0 rows, with no error anywhere.

This is a silent-data-corruption-class footgun: nothing fails, nothing warns, and the data looks correct at rest and on decrypt. Only the search results are wrong.

Source

rc.3 skilltester run (2026-07-19/20), finding F4 — one of four GA blockers. Hit on the supabase-edge surface. The assessor reproduced the mechanism directly: the same plaintext "Netflix" yielded hm=f5a3… as stored, but hm=587b… when a fresh query term was minted under the other keyset.

Verified in tree

The two credential paths genuinely differ:

  • stash auth login binds the device to the workspace's default keyset — stated in packages/cli/src/cli/registry.ts:270: "Your device is bound to the workspace's default keyset, so later…".
  • stash env (packages/cli/src/commands/env/index.ts:40-55) mints a new ZeroKMS client via POST {zerokms}/create-client, then an access key via POST {cts}/api/access-keys. Its docblock covers workspace CRN, client id/key, access key, and the member role — keyset selection is never mentioned, so the minted client's keyset is whatever create-client assigns rather than the workspace default the device is bound to.

So the two commands are documented to bind to different things, and nothing reconciles them.

Impact

Proposed fix

Two independent halves, either of which removes the silent failure:

  1. stash env mints against the workspace default keyset — or fails loudly when it cannot, rather than minting a client on a different one.
  2. encryptQuery signals a keyset mismatch instead of returning terms that match nothing. A query term minted under a keyset that does not match the column's stored terms should be an error, not an empty result set.

(2) is the more durable guarantee — it catches every route to the mismatch, not just the CLI one.

Notes

Adjacent but not a duplicate of #561 (allow passing a keyset to encrypt/decrypt operations, not just at init), which is about the API surface for selecting a keyset. This issue is about two CLI paths silently disagreeing on which keyset is in play, and the failure being invisible.

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions