From cdfd69bad37f451d18a85b3d4b073b881c690b26 Mon Sep 17 00:00:00 2001 From: xcaliber Date: Tue, 24 Feb 2026 11:37:21 +0000 Subject: [PATCH] Docupdate 0.9.4.1 (#55) --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ ROADMAP.md | 51 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00171a8..282e2e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to Chat Switchboard. +## [0.9.4] — 2026-02-24 + +### Added +- **API key encryption (vault)** — Two-tier AES-256-GCM encryption for stored + API keys. Global/team keys encrypted with env-var-derived key (HKDF-SHA256); + personal BYOK keys encrypted with per-user encryption key (UEK) derived from + password via Argon2id. Admin cannot recover personal keys without user's + passphrase. New `server/crypto/` package: `vault.go`, `cache.go`, + `resolver.go`, `backfill.go` with 11 round-trip tests. +- **UEK lifecycle** — UEK generated on registration, unwrapped on login + (Argon2id → AES-GCM), cached in `sync.Map` for session duration, evicted + with memory zeroing on logout. Pre-migration users auto-initialize vault + on first login. +- **Migration 003_vault.sql** — Adds `encrypted_uek`, `uek_salt`, `uek_nonce`, + `vault_set` to users table. Adds `api_key_enc` (BYTEA), `key_nonce`, + `key_scope` to provider_configs. Backfills `key_scope` from existing scope. +- **Startup backfill** — `BackfillEncryptedKeys()` encrypts plaintext API keys + on first startup with `ENCRYPTION_KEY` set. `EnforceEncryptionKey()` refuses + startup if encrypted keys exist but env var is missing. +- **CI/CD: encryption secret** — `ENCRYPTION_KEY` Gitea secret synced to k8s + `switchboard-encryption` secret. Backend manifest references it as optional + `secretKeyRef`. + +### Fixed +- **HTML code blocks render live in chat (XSS)** — When a model's `` + tag directly abutted a code fence (no newline), the fence wasn't recognized + by marked.js, causing raw HTML to render as live DOM elements (canvas games, + styles, etc.). Three-layer fix: (1) DOMPurify switched from permissive + `ADD_TAGS` (default allows canvas, style, form, etc.) to strict + `ALLOWED_TAGS` allowlist of only markdown-produced elements; (2) think-block + placeholders padded with `\n\n` to ensure adjacent fences start on fresh + lines; (3) unclosed code fences auto-closed before `marked.parse` for + streaming protection. + ## [0.9.3] — 2026-02-23 ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index cfa9514..438c123 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -185,7 +185,7 @@ instead of being hidden or auto-formatted without user agency. --- -## v0.9.4 — API Key Encryption + Vault +## ✅ v0.9.4 — API Key Encryption + Vault Two-tier encryption with a trust boundary between organizational keys (admin- accessible) and personal BYOK keys (user-only). Personal keys are protected @@ -277,7 +277,7 @@ non-existent account password). Builtin auth users never see this term. - `UnwrapUEK(ciphertext, nonce, pdk) → (uek, error)` - `Encrypt(plaintext, key) → (ciphertext, nonce, error)` - `Decrypt(ciphertext, nonce, key) → (plaintext, error)` -- `DeriveEnvKey(envVar) → key` (SHA-256 of env var, simple deterministic) +- `DeriveEnvKey(envVar) → key` (HKDF-SHA256 with domain separation context) **Session UEK caching** — UEK stored in a sync.Map keyed by user ID, evicted on logout / token expiry. Completion handler retrieves UEK from cache to @@ -318,30 +318,45 @@ func (s *Store) DecryptAPIKey(config ApiConfig, uekCache *sync.Map) (string, err ### Checklist **Crypto core** -- [ ] `server/crypto/vault.go` — Argon2id derivation, AES-256-GCM wrap/unwrap -- [ ] `server/crypto/vault_test.go` — round-trip, wrong-password, corruption -- [ ] Env-var key derivation + validation on startup +- [x] `server/crypto/vault.go` — Argon2id derivation, AES-256-GCM wrap/unwrap +- [x] `server/crypto/vault_test.go` — round-trip, wrong-password, corruption (11 tests) +- [x] Env-var key derivation + validation on startup (HKDF-SHA256 with domain separation) +- [x] `server/crypto/cache.go` — UEK cache with copy-on-read/write, memory zeroing +- [x] `server/crypto/resolver.go` — KeyResolver routes decrypt/encrypt by key_scope +- [x] `server/crypto/backfill.go` — startup migration for plaintext → encrypted keys **Schema + migration** -- [ ] Migration: add encrypted columns, backfill existing keys, drop plaintext -- [ ] `ENCRYPTION_KEY` enforcement (refuse to start if keys exist but no env var) +- [x] Migration 003_vault.sql: encrypted columns, key_scope backfill +- [x] `ENCRYPTION_KEY` enforcement (refuse to start if keys exist but no env var) +- [x] Unencrypted fallback when `ENCRYPTION_KEY` not set (raw bytes, graceful degradation) **Auth integration** -- [ ] UEK generation on user creation (builtin auth) -- [ ] UEK unwrap on login, cache in session -- [ ] UEK re-wrap on password change -- [ ] UEK destruction on admin password reset (with confirmation) -- [ ] Vault passphrase prompt for mTLS/OIDC (when personal providers enabled) +- [x] UEK generation on user creation (builtin auth) +- [x] UEK unwrap on login, cache in session +- [x] UEK eviction on logout (memory zeroing) +- [ ] UEK re-wrap on password change _(deferred — Settings handler)_ +- [ ] UEK destruction on admin password reset with confirmation _(deferred)_ +- [ ] Vault passphrase prompt for mTLS/OIDC _(deferred — v0.20.0 dependency)_ **Provider key lifecycle** -- [ ] Encrypt on provider create/update -- [ ] Decrypt on completion request (per key_scope) -- [ ] `ErrVaultLocked` handling (prompt user to unlock) +- [x] Encrypt on provider create/update (all 6 write paths: admin global, personal, team) +- [x] Decrypt on completion request (all 4 read paths: completion, admin fetch, personal fetch, team fetch) +- [x] `ErrVaultLocked` handling (prompt user to unlock) **Admin tooling** -- [ ] `switchboard vault rekey` CLI command -- [ ] Admin UI: encryption status indicator -- [ ] Admin UI: password reset warning +- [ ] `switchboard vault rekey` CLI command _(deferred)_ +- [ ] Admin UI: encryption status indicator _(deferred)_ +- [ ] Admin UI: password reset warning _(deferred)_ + +**CI/CD** +- [x] `ENCRYPTION_KEY` Gitea secret → k8s `switchboard-encryption` secret sync +- [x] Backend k8s manifest: optional `secretKeyRef` for `ENCRYPTION_KEY` +- [x] Pipeline version bump to v0.9.4 + +**Security fix (bonus)** +- [x] DOMPurify: strict `ALLOWED_TAGS` allowlist (was permissive `ADD_TAGS` default) +- [x] Think-block placeholder `\n\n` padding (prevents fence fusion with ``) +- [x] Unclosed code fence auto-close before `marked.parse` (streaming protection) ---