Repository navigation
Seal the YouTube refresh token under the user's passphrase - #165
Merged
Merged
Conversation
…ive only while unlocked youtube_credentials.refresh_token was stored in PLAINTEXT (RLS-protected, but readable by anything with the service role or the database). Now (seal v2): - Settings > YouTube: the user sets a key passphrase in the browser (PBKDF2-SHA256 600k + HKDF -> KEK wraps a random DEK; recovery key shown once; the DEK seals an ECDH P-256 private key whose public half is stored). - The OAuth callback seals the refresh token to the public key and refuses to store it without one: no new plaintext. - Unlock sends the DEK (never the passphrase); the server keeps the opened key in memory only (TTL up to 7 days, Lock, restart = locked). Auto go-live opens the token on use and skips with a log line while locked. Plaintext tokens are never used. - Migration: setting the passphrase seals a legacy plaintext token in the same request (prod has none today). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| }); | ||
|
|
||
| it('never uses a plaintext token (no server-side fallback)', async () => { | ||
| mockMaybeSingle.mockResolvedValue({ data: { refresh_token: 'plaintext-rt' } }); |
ThreatCrush Security Scan53 finding(s) HIGH/CRITICAL: 4 | MEDIUM: 29 | LOW: 20
…and 3 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sh1pt-actions-fleet Bot
added a commit
that referenced
this pull request
Oct 28, 2026
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: Anthony Ettinger <anthony@profullstack.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
youtube_credentials.refresh_tokenwas stored in plaintext.src/lib/sealed/crypto.ts(WebCrypto) runs PBKDF2-SHA256 600k + HKDF; the recovery key is shown once. The server stores only the seal (user_key_seals).?youtube=seal_required, so no new plaintext is ever written.POST /api/keys/unlock) sends the data key, never the passphrase. The server keeps the opened key in memory only, with a TTL (default 7d in the UI, max 7d) and Lock. A restart locks every user.autoTransitionYouTubeopens the token on use. While locked it skips with a log line and never fails. Plaintext tokens are never used.Tests:
youtubeAutoLive.test.tscovers the sealed token path, locked and restart, no plaintext fallback, and TTL clamp/expiry. 6/6 pass.🤖 Generated with Claude Code