Repository navigation
Seal user secrets under each user's passphrase (no server master key) - #399
Merged
Merged
Conversation
| smtp_user: "enc_smtp_user", | ||
| smtp_pass: "enc_smtp_pass", | ||
| api_key: "enc_api_key", | ||
| auth_token: "enc_auth_token", |
ThreatCrush Security Scan47 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 29 | LOW: 16
Snippets are redacted; ThreatCrush never prints matched credential material. |
| const r = await unlockWithDataKey(payload.owner_id, payload.dek ?? "", payload.ttl_seconds); | ||
| res.writeHead(200, { "content-type": "application/json" }); | ||
| res.end(JSON.stringify({ ok: true, expires_at: r.expiresAt })); | ||
| console.log(`[worker] sealed keys unlocked for ${payload.owner_id} until ${r.expiresAt}`); |
…nlocked
One SOCIAL_VAULT_KEY in app.env used to open every user's social OAuth
tokens, app passwords, Discord/Telegram bot tokens, cookies, outreach SMTP
passwords and seed logins. Root on the box = all of them.
Now (seal v2, lib/sealed):
- Each user sets a key passphrase at /dashboard/keys (browser WebCrypto:
PBKDF2-SHA256 600k + HKDF -> KEK wraps a random DEK; recovery key shown
once). The DEK seals an ECDH P-256 private key; the public key is stored.
- Every secret is sealed to the owner's PUBLIC key (ECDH + HKDF + AES-256-GCM,
AAD = owner + field), so OAuth callbacks and token refreshes can write while
the owner is locked, but nothing can read without their unlock.
- Unlock sends the DEK (never the passphrase); the Next server and the worker
each keep the opened private key in process memory only, TTL default 8h,
max 7d, Lock button/endpoint/CLI. Restart = locked.
- Locked owners' scheduled work waits (feed autopost, promote campaigns,
browser posts, cookie refresh, reply scans) with a visible "locked:" status
and one email nudge per lock episode. Autoblog is untouched.
- Org secrets are sealed to organizations.owner_id.
- Operator keys: COINPAY_API_KEY (payouts) and X402_PRIVATE_KEY are read via
lib/sealed/operator; an admin seals the env values under their own key, after
which env is never read for them; locked = payouts stay queued / crawls do
not pay, plus one ALERT log line (and smshub call if OPERATOR_ALERT_PHONE).
- Migration: legacy v1 rows are re-sealed in the owner's own setup/unlock
request; LEGACY_KEY_MIGRATION=off is the cutoff switch; view
public.sealed_keys_status counts what is left.
- API: GET /api/keys, POST /api/keys/{setup,unlock,lock,passphrase,operator};
CLI: crawlproof keys status|unlock|lock.
- UI copy that said "the key lives in the application environment" fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sh1pt-actions-fleet
Bot
force-pushed
the
feat/sealed-social-secrets
branch
from
October 28, 2026 20:21
e56fc75 to
2451e75
Compare
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.
Replaces the single
SOCIAL_VAULT_KEYthat opened every user's social tokens, app passwords, bot tokens, cookies, outreach SMTP passwords and seed logins.How it works
/dashboard/keys: the user sets a key passphrase in the browser (PBKDF2-SHA256 600k + HKDF, random salt, AES-256-GCM, recovery key shown once). The server stores only the seal (wrapped DEK, key check, public key, sealed private key).locked:status and sends one email per lock episode. Autoblog is unaffected.COINPAY_API_KEY(payouts) andX402_PRIVATE_KEYgo throughlib/sealed/operator.ts. An admin seals the current env values with POST/api/keys/operator(a button on the Keys page). After that, env is not read for them. While locked, payouts stay queued and crawls don't pay; oneALERT crawlproof: operator keys locked, unlock to resumelog line is written.LEGACY_KEY_MIGRATION=offis the cutoff.select * from sealed_keys_statusshows what is left.crawlproof keys status|unlock|lock.Not changed:
sp_mastodon_app.enc_client_secret(our own Mastodon app registrations, an operator app credential like META_APP_SECRET) stays on the legacy key. Checkout creation still uses envCOINPAY_API_KEY(same key as payouts). Splitting it needs a checkout-only key from CoinPay.Tests:
tests/sealed-keys.test.tscovers seal/unseal, AAD, rewrap, TTL/lock/restart, no env fallback, and migration (including the off switch). Full suite 2915 passed, typecheck clean,next buildok.🤖 Generated with Claude Code