feat(web): mask secrets/codes by default with an explicit reveal (BUNYIP-371) #449

Merged
nrupard merged 3 commits from feat/BUNYIP-371-mask-secrets-screenshare into main 2026-08-03 21:48:36 +02:00
Owner

BUNYIP-371. Best-effort privacy masking so secrets/codes are not exposed during casual screen sharing.

Context

Vas asked for KeePassXC-style privacy (hide secrets during a screen share). A web app CANNOT mark itself hidden to native screen capture the way a native app does, and a CSS blur is defeated by disabling GPU compositing - so this is best-effort in-app masking that stops ACCIDENTAL exposure, not a determined viewer. That limitation is documented in the code (the sensitive_reveal doc comment + the app.js handler comment).

Change

New sensitive_reveal(label, content) Maud helper (bunyip-web/src/handlers/dashboard.rs): wraps a rendered secret in a block that is blurred (blur-sm) by default, with an eye button that toggles the blur. A delegated [data-sensitive-toggle] handler in assets/js/app.js flips the class + aria-pressed + icon. The value stays in the DOM (visual masking, not removal, so it is still copyable once revealed).

Applied to the user-facing plaintext-secret sites:

  • TOTP enrollment: the QR + the base32 manual key (both expose the secret).
  • Recovery codes: the code grid, on both the 2FA-enabled and regenerate pages.

Out of scope (already handled or lower risk, per the scope decision): the Stripe secret key + webhook secret already render masked to last-4 (BUNYIP-432/443); password inputs already have a show/hide toggle; the admin Stripe webhook signing secret (shown once) is admin-only and was left out of this user-facing pass.

Verification

  • just check-container (fmt + clippy -D warnings + workspace tests) passes.
  • New tests: sensitive_reveal_blurs_content_but_keeps_it_in_the_dom (the component masks by default, keeps content in the DOM) + twofa_qr_view_masks_the_setup_key_by_default (applied to the TOTP view).

🤖 Generated with Claude Code

BUNYIP-371. Best-effort privacy masking so secrets/codes are not exposed during casual screen sharing. ## Context Vas asked for KeePassXC-style privacy (hide secrets during a screen share). A web app CANNOT mark itself hidden to native screen capture the way a native app does, and a CSS blur is defeated by disabling GPU compositing - so this is best-effort in-app masking that stops ACCIDENTAL exposure, not a determined viewer. That limitation is documented in the code (the `sensitive_reveal` doc comment + the app.js handler comment). ## Change New `sensitive_reveal(label, content)` Maud helper (`bunyip-web/src/handlers/dashboard.rs`): wraps a rendered secret in a block that is blurred (`blur-sm`) by default, with an eye button that toggles the blur. A delegated `[data-sensitive-toggle]` handler in `assets/js/app.js` flips the class + `aria-pressed` + icon. The value stays in the DOM (visual masking, not removal, so it is still copyable once revealed). Applied to the user-facing plaintext-secret sites: - **TOTP enrollment**: the QR + the base32 manual key (both expose the secret). - **Recovery codes**: the code grid, on both the 2FA-enabled and regenerate pages. Out of scope (already handled or lower risk, per the scope decision): the Stripe secret key + webhook secret already render masked to last-4 (BUNYIP-432/443); password inputs already have a show/hide toggle; the admin Stripe webhook signing secret (shown once) is admin-only and was left out of this user-facing pass. ## Verification - `just check-container` (fmt + clippy `-D warnings` + workspace tests) passes. - New tests: `sensitive_reveal_blurs_content_but_keeps_it_in_the_dom` (the component masks by default, keeps content in the DOM) + `twofa_qr_view_masks_the_setup_key_by_default` (applied to the TOTP view). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(web): mask secrets/codes by default with an explicit reveal
Some checks failed
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 15s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
8e0d2e7ac7
BUNYIP-371: so a casual screen share does not expose the TOTP enrollment key or recovery codes. New sensitive_reveal Maud helper blurs a rendered secret (Tailwind blur-sm) by default with an eye button that toggles the blur (delegated [data-sensitive-toggle] handler in app.js); the value stays in the DOM, so it is still copyable once revealed. Applied to the TOTP QR + base32 manual key and the recovery-code grids (2FA-enabled + regenerate pages). Best-effort by design: a web page cannot hide from native screen capture like a native app, and the blur is defeated by disabling GPU compositing - this stops accidental exposure, not a determined viewer (documented in the helper + the JS handler). Stripe secrets already render masked (BUNYIP-432/443) and password inputs already have a show/hide toggle, so they are untouched.

#BUNYIP-371

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(web): index the sensitive-reveal handler in the app.js header
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 15s
Check / fmt + clippy + build + tests (pull_request) Successful in 12m27s
19a23edc4a
Add [data-sensitive-toggle] to the app.js data-* attribute index (comment only) so the reveal handler is discoverable alongside the others.

#BUNYIP-371

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(web): address BUNYIP-371 review - stronger blur, no-JS reveal, layout
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 22s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m36s
Create release / Create release from merged PR (pull_request) Has been skipped
bf2cdf701b
Code-review follow-ups on the secret masking:
- Blur strength: blur-sm (4px) barely obscured a QR (error-corrected, still scannable) or small text; use blur-xl (24px).
- No-JS fallback: the reveal was JS-only, so a no-JS user was permanently locked out of the setup key + recovery codes (can't finish enrolling). Rebuilt sensitive_reveal as a CSS-only checkbox toggle (peer-checked:blur-none) that reveals without JS; dropped the app.js handler entirely.
- Layout: the reveal control moved from an absolute button over the QR's corner (could cover a finder pattern) to a header row above the block.

sensitive_reveal now takes a unique id (binds the checkbox to its label); the three call sites pass distinct ids. Tests updated to assert the checkbox + blur-xl + peer-checked reveal. just check-container green.

#BUNYIP-371

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/BUNYIP-371-mask-secrets-screenshare 2026-08-03 21:48:36 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!449
No description provided.