Skip to content

qc keeps keys and tokens sealed at rest (qc 0.3.0) - #280

Merged
ralyodio merged 1 commit into
masterfrom
feat/qc-keys-at-rest
Oct 6, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/qc-keys-at-rest

Conversation

@ralyodio

@ralyodio ralyodio commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Problem: ~/.config/qc/session.json was plaintext, protected only by file mode 0600. Both private ML-KEM keypairs and the refresh token were readable by anything that could read that file.

Fix: the session file is now v2.

  • Only the server URL and your username stay readable, so qc whoami still works without unlocking.
  • The tokens and keypairs are sealed with ChaCha20-Poly1305 under a 32-byte key. The key comes from, in order:
    • QC_PASSPHRASE, stretched with scrypt (N=65536, r=8, p=1), for scripts and qc mcp
    • the OS keychain (macOS Keychain, or libsecret's secret-tool), so a desktop never asks
    • a passphrase prompt with hidden input, asked once per run
  • Refreshed tokens are re-sealed under the same key, so a refresh never prompts again.
  • A v1 plaintext session is sealed the first time it's used.

Tests: 6 new tests covering round-trip, wrong passphrase, re-seal, v1 upgrade, keychain use with no prompt, and passphrase confirmation.

  • test:ci: 642/642.
  • Smoke test of the built package: a plaintext session was sealed on first use and the private key no longer appears in the file.

🤖 Generated with Claude Code

session.json was plaintext (mode 0600 only): both private ML-KEM keys and
the refresh token readable by anything that could read the file. It is
now v2: only the server and who you are stay open (qc whoami); tokens and
keypairs are sealed with ChaCha20-Poly1305 under a 32-byte key from
QC_PASSPHRASE (scrypt), the OS keychain (macOS Keychain or secret-tool),
or a passphrase asked once per run. Refreshed tokens are re-sealed under
the same key. A v1 file is sealed on first use.

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

13 finding(s)

MEDIUM: 9 | LOW: 4

Severity Rule Location
MEDIUM redos-nested-quantifier src/app/api/profile/update/route.js:73
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:66
MEDIUM js-unescaped-html-sink src/app/faq/page.jsx:57
MEDIUM js-unescaped-html-sink src/app/layout.jsx:137
MEDIUM js-unescaped-html-sink src/app/layout.jsx:141
MEDIUM js-unescaped-html-sink src/app/page.jsx:47
MEDIUM redos-nested-quantifier src/lib/auth/dns-name.js:88
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:99
LOW secret-generic-credential src/app/api/auth/register-anon/route.test.js:32
LOW secret-jwt tests/debug-sms.js:10
LOW secret-generic-credential tests/private-key-import-export.test.js:252
LOW secret-generic-credential tests/private-key-import-export.test.js:264

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 504d6bc into master Oct 6, 2026
11 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.

1 participant