Skip to content

feat(settings): add encrypted key download to settings - #257

Merged
ralyodio merged 1 commit into
masterfrom
worktree-key-export-ui
Aug 16, 2026
Merged

feat(settings): add encrypted key download to settings#257
ralyodio merged 1 commit into
masterfrom
worktree-key-export-ui

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Problem

There was no way to download your encrypted keys from https://qrypt.chat/settings. The Private Keys section offered:

  • Import keys from a file
  • Back up to the server (PIN-encrypted)
  • Restore from the server

…but no export-to-disk. So a user could import a backup file they already had, yet never produce one in the first place.

Cause

The crypto layer was already complete. src/lib/crypto/private-key-manager.js implements exportPrivateKeys(password), generateExportFilename(), supportsFileDownload() and downloadExportedKeys(). exportPrivateKeys() was called in three places — all of which PUT the ciphertext to /api/auth/key-backup. downloadExportedKeys() had zero callers anywhere in the repo. This was a missing button, not a missing feature.

Change

Adds a Download Keys to File card to PrivateKeyManager.jsx, placed above the existing import card so export/import read as a pair:

  • Password + confirmation, minimum 6 characters, with a show/hide toggle
  • Uses the same ChaCha20-Poly1305 + HKDF envelope as the server backup, so a downloaded file is accepted by the existing import flow
  • Filename comes from generateExportFilename() (qryptchat-pq-keys-<timestamp>.json)
  • Hidden behind a hint when no keys exist on the device
  • Success message names the file and states plainly that the password is unrecoverable

No changes to the crypto layer, and no new API surface — the file is generated and encrypted entirely client-side.

Verification

  • pnpm build — clean, /settings compiles (8.16 kB)
  • pnpm vitest run tests/ — 30 files, 295 tests, all passing
  • oxlint on the changed file — no findings

🤖 Generated with Claude Code

The settings page could import a key backup file, back up to the server,
and restore from the server, but there was no way to download an
encrypted copy of your keys to disk.

privateKeyManager already implemented exportPrivateKeys(),
generateExportFilename() and downloadExportedKeys() — nothing in the UI
ever called the download path. This wires them to a "Download Keys to
File" card that sits above the existing import card, with password +
confirmation (min 6 chars) and a note that the password is unrecoverable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

91 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 10 | LOW: 76

Severity Rule Location
HIGH secret-generic-credential scripts/supabase-email.sh:21
HIGH secret-generic-credential scripts/supabase-twilio.sh:32
HIGH secret-generic-credential scripts/supabase-twilio.sh:47
HIGH js-host-header-trust src/lib/websocket/middleware/auth.js:31
HIGH js-cors-origin-reflected src/lib/websocket/server.js:121
MEDIUM manifest-install-lifecycle-script package.json:33
MEDIUM js-shell-exec-interpolation scripts/convert-routes.mjs:9
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:138
MEDIUM js-unescaped-html-sink src/app/layout.jsx:142
MEDIUM js-unescaped-html-sink src/app/page.jsx:47
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:60
LOW secret-generic-credential src/app/api/auth/invite-anon/route.test.js:24
LOW secret-generic-credential src/app/api/auth/register-anon/route.test.js:32
LOW secret-jwt src/app/api/auth/upload-avatar/route.test.js:27
LOW secret-generic-credential src/app/api/auth/upload-avatar/route.test.js:27
LOW secret-generic-credential src/app/api/profile/update/route.test.js:63
LOW secret-generic-credential src/lib/websocket/middleware/auth.test.js:47
LOW secret-generic-credential src/lib/websocket/middleware/auth.test.js:64
LOW secret-generic-credential tests/auth-key-generation.test.js:109
LOW secret-generic-credential tests/auth-key-generation.test.js:110
LOW secret-generic-credential tests/auth-key-generation.test.js:157
LOW secret-generic-credential tests/auth-key-generation.test.js:158
LOW secret-generic-credential tests/auth-key-generation.test.js:207
LOW secret-generic-credential tests/auth-key-generation.test.js:208
LOW secret-generic-credential tests/auth-key-generation.test.js:250
LOW secret-generic-credential tests/auth-key-generation.test.js:251
LOW secret-generic-credential tests/auth-key-generation.test.js:293
LOW secret-generic-credential tests/auth-key-generation.test.js:294
LOW secret-generic-credential tests/auth-key-generation.test.js:340
LOW secret-generic-credential tests/auth-key-generation.test.js:341
LOW secret-generic-credential tests/auth-key-generation.test.js:381
LOW secret-generic-credential tests/auth-key-generation.test.js:382
LOW secret-generic-credential tests/chat-archive.test.js:22
LOW secret-jwt tests/debug-sms.js:10
LOW secret-generic-credential tests/gpg-private-key-export.test.js:63
LOW secret-generic-credential tests/gpg-private-key-export.test.js:64
LOW secret-generic-credential tests/nuclear-delete.test.js:19
LOW secret-generic-credential tests/nuclear-delete.test.js:89
LOW secret-generic-credential tests/nuclear-delete.test.js:97
LOW secret-generic-credential tests/password-security-fix-verification.test.js:51
LOW secret-generic-credential tests/password-security-fix-verification.test.js:75
LOW secret-generic-credential tests/password-security-fix-verification.test.js:76
LOW secret-generic-credential tests/password-security-fix-verification.test.js:102
LOW secret-generic-credential tests/password-security-fix-verification.test.js:156
LOW secret-generic-credential tests/password-security-fix-verification.test.js:174
LOW secret-generic-credential tests/phone-auth-fix.test.js:49

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 270f847 into master Aug 16, 2026
9 checks passed
@ralyodio
ralyodio deleted the worktree-key-export-ui branch August 16, 2026 03:46
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