From e99a1c109ad30be9a91633807abaee567dff7bca Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Sat, 2 May 2026 14:46:19 +0200 Subject: [PATCH] docs: design bitwarden cache --- .../2026-05-02-bitwarden-cache-design.md | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-02-bitwarden-cache-design.md diff --git a/docs/superpowers/specs/2026-05-02-bitwarden-cache-design.md b/docs/superpowers/specs/2026-05-02-bitwarden-cache-design.md new file mode 100644 index 0000000..2459467 --- /dev/null +++ b/docs/superpowers/specs/2026-05-02-bitwarden-cache-design.md @@ -0,0 +1,217 @@ +# Bitwarden Cache Design + +Date: 2026-05-02 + +## Context + +The `secretstore` package supports a `bitwarden-cli` backend. Each secret read can call the Bitwarden CLI several times: + +- `bw list items --search /` +- `bw get item ` for each candidate item + +These calls can take several seconds when commands such as MCP `setup`, `doctor`, or generated config resolution read multiple secrets or are run repeatedly. + +The framework already requires an unlocked Bitwarden session for this backend and restores a persisted `BW_SESSION` into the process environment when available. The cache must use that runtime session as the trust root without embedding a static key in the binary or repository. + +## Goals + +- Avoid repeated Bitwarden CLI calls for short-lived and long-running framework processes. +- Keep the feature portable across Windows, macOS, Linux, and WSL. +- Enable the encrypted disk cache by default when `BW_SESSION` is available. +- Allow projects and operators to disable the cache. +- Never make cached secrets decryptable with only the installed binary, cache files, and repository content. + +## Non-Goals + +- Protect against an attacker who can read the process memory or environment while the process is running. +- Add an OS keyring dependency for the first implementation. +- Cache non-Bitwarden secret backends. +- Persist decrypted secrets on disk. + +## Configuration + +`manifest.SecretStore` gains: + +```toml +[secret_store] +backend_policy = "bitwarden-cli" +bitwarden_cache = true +``` + +`bitwarden_cache` defaults to `true` when omitted. + +The generated helper options continue to flow through `OpenFromManifest` and `DescribeRuntime`. The runtime option is represented in `secretstore.Options` and `secretstore.OpenFromManifestOptions` so generated packages can still override it programmatically if needed. + +An environment variable can force-disable the cache without editing `mcp.toml`: + +```text +MCP_FRAMEWORK_BITWARDEN_CACHE=0 +``` + +Accepted false values are `0`, `false`, `no`, `off`, and `disabled`, case-insensitive. Any other value leaves the manifest/default behavior in place. + +## Architecture + +The cache is internal to the `bitwarden-cli` backend. + +`bitwardenStore.GetSecret(name)` resolves secrets in this order: + +1. In-memory cache. +2. Encrypted disk cache, only if a cache key can be derived from the effective `BW_SESSION`. +3. Bitwarden CLI lookup. + +After a successful CLI lookup, the secret is written to the memory cache and, when available, the encrypted disk cache. + +`SetSecret` and `DeleteSecret` update Bitwarden first. After success, they invalidate the cache entry for the affected secret. `SetSecret` may repopulate the memory and disk cache with the new value after the Bitwarden write succeeds, but it must not expose a value that failed to persist to Bitwarden. + +## Cache Contents + +Memory entries store: + +- secret value +- expiration timestamp + +Disk entries store encrypted JSON: + +```json +{ + "version": 1, + "service_name": "graylog-mcp", + "secret_name": "profile/prod/api-token", + "scoped_name": "graylog-mcp/profile/prod/api-token", + "created_at": "2026-05-02T10:00:00Z", + "expires_at": "2026-05-02T10:10:00Z", + "value": "secret" +} +``` + +The plaintext exists only before encryption and after decryption in memory. + +## Key Derivation + +Disk cache is enabled only when the effective `BW_SESSION` is non-empty. The effective session is the value available after `EnsureBitwardenSessionEnv`, so it can come from either the process environment or the framework-restored session file. + +The cache derives dedicated keys with HKDF-SHA256: + +```text +master_key = HKDF-SHA256( + input key material: BW_SESSION, + salt: "mcp-framework bitwarden cache salt v1", + info: "mcp-framework bitwarden cache v1" +) +``` + +Separate subkeys are derived from `master_key`: + +- encryption key: `info = "mcp-framework bitwarden cache encryption v1"` +- entry ID key: `info = "mcp-framework bitwarden cache entry id v1"` + +`BW_SESSION` is never used directly as an AES key. + +## Disk Encryption + +Disk entries use AES-256-GCM from the Go standard library. + +Each write generates a fresh random nonce with `crypto/rand`. The file format is JSON with metadata needed to decrypt: + +```json +{ + "version": 1, + "algorithm": "AES-256-GCM", + "nonce": "", + "ciphertext": "" +} +``` + +Authenticated additional data includes stable, non-secret cache context: + +```text +mcp-framework bitwarden cache v1 +service= +secret= +scoped=/ +``` + +If decryption fails, the entry is treated as a cache miss. The framework may remove the unusable file as best effort. + +## Entry Identity + +Disk file names do not expose secret names or Bitwarden item refs. The file name is: + +```text +hex(HMAC-SHA256(entry_id_key, cache_context)) + ".json" +``` + +The cache context includes: + +- cache format version +- service name +- raw secret name +- scoped Bitwarden item name +- backend scope marker + +Because the HMAC key is derived from `BW_SESSION`, changing or losing `BW_SESSION` makes existing file names undiscoverable and existing entries undecryptable. + +## TTL and Invalidation + +Default TTL: 10 minutes. + +TTL applies to both memory and disk entries. Expired entries are treated as misses and may be deleted best-effort. + +The first implementation uses a constant default TTL. It keeps room for a future `bitwarden_cache_ttl` option, but does not add that option now to keep scope tight. + +Invalidation rules: + +- `SetSecret` invalidates the affected entry after the Bitwarden write succeeds. +- `DeleteSecret` invalidates the affected entry after the Bitwarden delete succeeds or confirms the item is already absent. +- `BW_SESSION` change implicitly invalidates disk cache through key derivation. +- `bitwarden_cache = false` disables both memory and disk cache for the backend instance. +- `MCP_FRAMEWORK_BITWARDEN_CACHE=0` disables both memory and disk cache. + +## Storage Location and Permissions + +Disk cache path: + +```text +os.UserCacheDir()/serviceName/bitwarden-cache +``` + +If `os.UserCacheDir` fails, disk cache is disabled and secret reads continue through memory cache and Bitwarden CLI. + +The cache directory is created with `0700` and cache files with `0600` where the platform supports Unix-style permissions. Permission setting errors disable disk cache for that operation rather than failing secret resolution, because Bitwarden remains the source of truth. + +## Error Handling + +Cache failures must not make a healthy Bitwarden backend unusable. + +- Memory cache errors are not expected. +- Disk cache read/decrypt/parse errors are treated as misses. +- Disk cache write errors are ignored after optional debug logging. +- Bitwarden CLI errors keep their current behavior and typed error classification. +- Malformed or expired cache entries are never returned. + +## Tests + +Unit tests should cover: + +- repeated `GetSecret` hits memory and avoids a second CLI item read +- reopened store can read from encrypted disk cache without calling `bw get item` +- disk cache file does not contain the secret value or clear secret name +- cache is disabled by `bitwarden_cache = false` +- cache is disabled by `MCP_FRAMEWORK_BITWARDEN_CACHE=0` +- expired entries are missed and refreshed from Bitwarden +- `SetSecret` invalidates or refreshes stale cache data +- `DeleteSecret` removes cached data +- missing `BW_SESSION` disables disk cache +- changed `BW_SESSION` cannot decrypt a previous disk entry +- manifest parsing preserves the default enabled behavior when the field is omitted + +## Documentation + +Update: + +- `docs/manifest.md` for `[secret_store].bitwarden_cache` +- `docs/secrets.md` for cache behavior, TTL, disable controls, and threat model +- generated/scaffolded `mcp.toml` examples only if the option should be visible by default + +The preferred scaffold output should omit `bitwarden_cache` because the default is enabled. Documentation should show it as the explicit disable knob.