Skip to content

Seal copilot own API keys under the owner's key passphrase - #166

Merged
sh1pt-actions-fleet[bot] merged 1 commit into
masterfrom
feat/sealed-user-secrets
Oct 28, 2026
Merged

sh1pt-actions-fleet[bot] merged 1 commit into
masterfrom
feat/sealed-user-secrets

Conversation

@sh1pt-actions-fleet

Copy link
Copy Markdown
Contributor

copilot_settings.anthropic_key / openai_key / elevenlabs_key (meeting copilot BYOK) were AES-256-GCM under the server env key COPILOT_KEYS_SECRET: root on the box could open every user's keys. This moves them onto the seal v2 pieces from #165 (same key passphrase, same user_key_seals row, same unlock store), no second scheme.

  • Save: each key is sealed to the owner's public key (v2:), AAD binds user + field. 409 before a key passphrase exists; Settings > Copilot can set one (recovery key shown once).
  • Use: the copilot runs server-side (shared room transcription + LLM), so the browser/desktop derives the data key from the passphrase and sends it with Start (never the passphrase). The private key is held in memory as copilot:<user>:<session> (TTL default 8h) and dropped when the copilot stops, the meeting ends/is abandoned, on Lock (/api/keys/lock), or on restart. Opened keys are not cached past that.
  • Locked: free self-hosted models only; never our paid provider in place of the owner's key. Panel shows an unlock box (before Start and mid-meeting after a restart). Status API returns ownKeys.state.
  • Legacy v1: never used for provider calls. Re-sealed once in the owner's own request (key setup/unlock, copilot settings GET, copilot start with dek) and overwritten in place; LEGACY_KEY_MIGRATION=off ends it. Passphrase reset clears keys sealed to the old key.
  • Migration 20261028230000_copilot_keys_sealed.sql: adds copilot_settings.seal_version. Prod had 0 copilot_settings rows.

Tests: new keys.test.ts (seal/unseal, AAD, per-meeting unlock TTL/lock/restart, separate from YouTube unlock, no env fallback, migration + cutoff, reset, no paid fallback while locked); full web suite 1225 pass, i18n catalogs (19 locales), desktop CLI tests, typecheck, web + desktop builds.

🤖 Generated with Claude Code

copilot_settings.anthropic_key / openai_key / elevenlabs_key were AES-GCM
under the server env key COPILOT_KEYS_SECRET, so anyone with the box could
open every user's BYOK keys. They now use the seal v2 pieces from #165 (same
key passphrase and user_key_seals row as the YouTube token):

- Saving seals each key to the owner's public key (v2:), 409 before a key
  passphrase is set; the settings page can set one (recovery key shown once).
- Keys open only for a meeting the owner unlocked: Start sends the data key
  derived in the browser/desktop (never the passphrase); the private key is
  held in memory under copilot:<user>:<session> with a TTL and dropped when
  the copilot stops, the meeting ends, on Lock, or on restart.
- Locked keys mean free models only, never our paid provider in their place;
  the panel shows an unlock box (also mid-meeting after a restart).
- Legacy v1 values are never used for provider calls; they are re-sealed once
  in the owner's own request (key setup/unlock, copilot settings, start) and
  overwritten in place. LEGACY_KEY_MIGRATION=off is the cutoff.
- Migration adds copilot_settings.seal_version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

const USER = '11111111-1111-4111-8111-111111111111';
const SESSION = 'sess-1';
const ANTHROPIC = 'sk-ant-api03-abcdefghijklmnopqrstuv';
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

54 finding(s)

HIGH/CRITICAL: 4 | MEDIUM: 29 | LOW: 21

Severity Rule Location
HIGH sh-eval-expansion .githooks/pre-commit:33
HIGH js-electron-node-integration apps/desktop/src/main/window.ts:49
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:717
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:921
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:693
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:824
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:826
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:1084
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:1086
MEDIUM sh-remote-script-execution apps/livekit/setup-livekit-server.sh:93
MEDIUM sh-remote-script-execution apps/turn/deploy-droplet.sh:62
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:52
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:79
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:199
MEDIUM js-open-redirect apps/web/src/app/cli/authorize/consent.tsx:68
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:146
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:230
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:150
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:154
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:98
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:262
MEDIUM js-open-redirect apps/web/src/app/settings/copilot/copilot-settings.tsx:144
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:285
MEDIUM js-open-redirect apps/web/src/components/billing/useCheckout.ts:81
MEDIUM js-unescaped-html-sink apps/web/src/components/pit-footer.tsx:24
MEDIUM js-open-redirect apps/web/src/hooks/useDesktopHandoff.ts:24
MEDIUM redos-nested-quantifier apps/web/src/lib/deliverable.ts:11
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:124
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:393
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:396
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:405
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:608
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:138
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:139
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:340
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:341
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:371
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:372
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:383
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:397
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:398
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:405
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:415
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:416
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:453
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:454
LOW secret-generic-credential apps/desktop/src/main/agent-cli/copilot.test.ts:92
LOW secret-generic-credential apps/livekit/fly.toml:12
LOW secret-generic-credential apps/turn/fly.toml:11

…and 4 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@sh1pt-actions-fleet
sh1pt-actions-fleet Bot merged commit 3cfc62a into master Oct 28, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants