Skip to content

feat(web): support separate decryption keys - #139

Merged
dillonstreator merged 4 commits into
osbytes:mainfrom
snowyukitty:feat/separate-decryption-key
Aug 9, 2026
Merged

dillonstreator merged 4 commits into
osbytes:mainfrom
snowyukitty:feat/separate-decryption-key

Conversation

@snowyukitty

@snowyukitty snowyukitty commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Always show the combined URL, key-free URL, and decryption key after creation, while keeping the QR code on the combined URL.
  • Remove automatic clipboard copying so the sender explicitly chooses which URL or key to share.
  • Hard-break legacy ?key= links by scrubbing every key parameter before router initialization, warning the recipient, and falling back to manual key entry.
  • Keep raw keys out of React Query cache identities and create mutation results, and reset key-bearing local state when navigating between vault IDs.
  • Remove the deployment flag, legacy bootstrap, router-private remount machinery, Docker/README configuration, clipboard-helper changes, and unrelated generic error changes called out in review.

Fixes #54

Security

  • Application code checks only whether a query-string key exists, deletes every occurrence, and replaces the current history entry before importing the module that creates the router. It never retrieves or stores the query value.
  • A visible warning explains that the unsafe query-string form may already have reached server logs; scrubbing cannot undo an earlier disclosure.
  • Missing or rejected keys return to explicit manual entry. The password input remains controlled, and key/password errors are exposed to assistive technology.
  • Decryption keys are absent from React Query cache identities and create mutation results. Key-bearing values used by the UI live in component refs, and the related mutations use gcTime: 0.
  • Combined fragment links remain bearer capabilities and can leak through anyone or anything that receives the combined URL. This implementation does not claim that JavaScript can erase secret material from memory.

Validation

  • Frozen pnpm install with generation-local package storage
  • Web: 2 files / 10 tests, TypeScript, ESLint, and production build
  • Core: build, TypeScript, and ESLint (0 errors; one existing no-explicit-any warning in unchanged packages/core/src/client.ts)
  • Workspace: full typecheck and production build
  • Targeted Prettier check over all 19 cumulative changed paths, Changesets status, and git diff --check
  • Browser QA for repeated-query scrubbing, warning visibility, preserved unrelated query/hash state, back/forward history, manual and wrong-key recovery, controlled password retry, three-field create output, combined QR, no automatic clipboard access, and SPA navigation between vault IDs
  • Independent sealed review of the exact base/head diff: Grok 4.5 approved with no material findings; the only low-severity note was optional deeper automated UI coverage

Known limitations

  • Remote CI results were not part of the frozen local validation evidence and must be evaluated separately.
  • Workspace-wide lint and format checks remain red on unchanged CRLF-formatted server/source files; the affected packages and every cumulative changed path pass their focused checks.
  • The established core Jest command discovers no tests from this hidden Windows worktree and exits 1, so core tests are not claimed as passing.
  • Vite reports its existing warning for chunks larger than 500 kB after the successful production build.

AI assistance

AI assistance: OpenAI Codex implemented and reviewed the maintainer-requested follow-up. Grok 4.5 provided sealed security/design analysis and an independent packet-only review of the exact final diff. I reviewed the final patch and validation results and take responsibility for the submission.

@dillonstreator dillonstreator left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough work here — the receiver-side flow (missing key → manual entry prompt) is exactly the right shape, and keeping the raw key out of React Query cache identities is a good catch. However, I'd like to take a different overall approach, which should significantly shrink this PR:

1. No configuration — always show both options

I don't want this to be deployment/config-driven. Instead, the create-success view should always show three fields:

  1. The combined URL (key in fragment) — current behavior
  2. The key-free URL
  3. The decryption key on its own

The sender chooses how to share at share time, rather than the deployer choosing at build time. A URL without a key already implicitly tells the recipient to enter it separately — the key prompt you built renders whenever the fragment is missing, so no query parameter and no flag are needed. This removes VITE_SEPARATE_DECRYPTION_KEY and with it the Dockerfile.web, docker-compose.yaml, README, config.ts, and config.test.ts changes.

2. Hard-break ?key= support instead of the compatibility layer

Rather than carrying the deprecated query-param format forward, let's remove support for it entirely (planned as its own release/changeset for changelog clarity — happy to coordinate sequencing). Rationale: secrets are TTL-bound so there's no long tail of old links, and a ?key= link has already leaked the key to server logs by the time the client sees it — reading it client-side only masks that. With the new key prompt, an old ?key= link degrades gracefully: the recipient lands on the prompt and can still enter the key manually.

That means legacyKeyBootstrap.ts and the legacy branches in resolveDecryptionKey can go. The one piece worth keeping is a small scrub: if a key query param is present, history.replaceState it away and show a warning toast that the link format is unsafe — without ever reading the value into app state. That's a few lines in main.tsx, not a module.

3. Please trim the unrelated changes

A few things in here are separate concerns that make the diff hard to review — details inline, but broadly: the clipboardCopy rework, the router remount machinery, and the generic createFailed error handling. Happy to see any of those as their own PRs with their own rationale. (Rewording existing locale strings is fine where this feature changes their meaning — e.g. success.description.main must change once auto-copy is removed — just avoid general copy edits beyond that.)

One change I do want to keep from your approach: dropping the automatic copy-to-clipboard on create. Letting the user explicitly choose what to copy is safer and fits the three-field design.

Core of what survives: buildSecretLinks (simplified to always return both URLs), the DecryptionKeyPrompt component and missing-key flow in View.tsx, the new locale keys, and keeping the key out of mutation cache identity. That's a much smaller, focused PR.

Comment thread packages/web/src/config.ts Resolved
Comment thread packages/web/src/lib/clipboardCopy.ts Resolved
Comment thread packages/web/src/routes/$id.tsx Resolved
Comment thread packages/web/src/lib/secretUrl.ts Resolved
Comment thread packages/web/src/pages/Create.tsx Resolved
Comment thread packages/web/src/pages/Create.tsx Resolved
Comment thread packages/core/src/i18n/locales/en.ts Resolved
Comment thread packages/web/src/pages/View.tsx Resolved
Comment thread packages/web/src/pages/View.test.tsx Resolved
@snowyukitty

Copy link
Copy Markdown
Contributor Author

Thanks — I rebuilt the follow-up around the requested shape. The create-success view now always shows the combined URL, key-free URL, and decryption key; QR remains combined; and automatic clipboard copying is gone. Legacy ?key= values are no longer accepted: every occurrence is scrubbed before the router module loads, the user gets a warning after the Toaster mounts, and the value is never retrieved into application state.

I also removed the deployment/config/Docker/README work, legacy bootstrap, router-private remount machinery, clipboard-helper changes, and unrelated generic error change. The password input remains controlled with the accessibility additions, locale edits are limited to affected behavior, missing/wrong-key recovery remains available, and raw keys stay out of React Query cache identities and create mutation results.

Local validation passed for the frozen install; all 10 web tests; web/core build, typecheck, and lint; full workspace build and typecheck; targeted Prettier; Changesets; diff checks; and browser QA covering query scrubbing/history, warning visibility, manual and wrong-key flows, password retry, the three share fields, combined QR, no automatic clipboard access, and cross-vault SPA navigation. The exact diff also passed an independent sealed Grok 4.5 review with no material findings.

Known baseline limitations are recorded in the updated PR body: workspace-wide lint/format remain red on unchanged CRLF-formatted files, core Jest discovers no tests from this hidden Windows worktree, Vite emits its existing chunk-size warning, and remote CI results were not part of the frozen local validation evidence. I also updated the AI-assistance disclosure in the PR body.

@dillonstreator

Copy link
Copy Markdown
Member

Maintainer follow-up

I applied a few polish changes directly on this branch:

Create success

  • Consistent actions on every field: reveal, copy, share (combined also has QR)
  • Values masked by default
  • Combined URL separated from the key-free URL + decryption key group

Read flow

  • Key and/or password collected in one on-page form (no modal)
  • Removed "Enter a different key"
  • Exists (HEAD) check always runs before credentials / "View Secret"
  • 429s get a dedicated rate-limit message instead of the connection-error copy

Legacy ?key= links

  • Migrate ?key= into the fragment (when missing), strip it from the query, and keep the link usable
  • Toast asks the recipient to have the sender upgrade an outdated client

Other

  • Removed resolveDecryptionKey in favor of direct hash parsing
  • Restored footer tagline: "Built with security and privacy in mind, because ignorance can be bliss"
  • Dark-mode --input / --border tokens adjusted so borders remain visible on muted panels
  • Dropped View.test.tsx

Align create/read flows for separate decryption keys, migrate legacy ?key= into the fragment with an upgrade warning, consolidate the changeset, and drop brittle View tests.
@dillonstreator
dillonstreator force-pushed the feat/separate-decryption-key branch from d9bf370 to c22072d Compare August 9, 2026 17:40
@dillonstreator
dillonstreator merged commit 2dbb6c5 into osbytes:main Aug 9, 2026
2 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 9, 2026
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.

add configuration to remove key from url hash

2 participants