# 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.