|
| 1 | +# Dual-Key Form Sharing — Implementation Plan (Final) |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Secure form sharing with dual links, encrypted responses, and creator-only response viewing. When a `{{Form:}}` doc is shared, generate two links: a **creator link** (with `&rk=` for viewing responses) and a **respondent link** (`&m=fill`, preview-locked, fill-only). |
| 6 | + |
| 7 | +## Architecture |
| 8 | + |
| 9 | +``` |
| 10 | +Creator writes {{Form:}} → clicks Share |
| 11 | + ↓ |
| 12 | +createCompactShare() detects form → generates rk + SHA-256(rk) = rkHash |
| 13 | + ↓ |
| 14 | +Stores in Firestore: { d, k, t, wt, rkHash } |
| 15 | + ↓ |
| 16 | +Produces TWO links: |
| 17 | + Creator: #s=<id>&rk=<rkString> (rkHash validates ownership) |
| 18 | + Respondent: #s=<id>&m=fill (preview-locked, no rk) |
| 19 | + ↓ |
| 20 | +On load: hash URL's rk → compare with stored rkHash → show/hide Responses button |
| 21 | +``` |
| 22 | + |
| 23 | +> [!NOTE] |
| 24 | +> `rk` is NOT stored in Firestore (only its SHA-256 hash `rkHash` is). This preserves zero-knowledge: even inspecting the Firestore doc doesn't reveal the key. |
| 25 | +
|
| 26 | +--- |
| 27 | + |
| 28 | +## Changes Made |
| 29 | + |
| 30 | +### Security Fixes (Critical) |
| 31 | + |
| 32 | +| # | Issue | File | Fix | |
| 33 | +|---|-------|------|-----| |
| 34 | +| 1 | XSS in response table | `form-engine.js` | Added `escapeHtml()` for all cell values | |
| 35 | +| 2 | Plain-text response fallback | `form-engine.js` | Removed `storeResponsePlain()` — encryption-only | |
| 36 | +| 3 | Unvalidated `rk` parameter | `cloud-share.js` | SHA-256 `rkHash` stored in Firestore, validated on load | |
| 37 | +| 4 | Submit blocked on creator view | `cloud-share.js` | Exempted `.form-dg-submit` from read-only interceptor | |
| 38 | + |
| 39 | +### Reliability & UX Fixes |
| 40 | + |
| 41 | +| # | Issue | File | Fix | |
| 42 | +|---|-------|------|-----| |
| 43 | +| 5 | Columns from first response only | `form-engine.js` | Collect column keys from ALL responses | |
| 44 | +| 6 | Stale comment | `form-engine.js` | Removed | |
| 45 | +| 7 | No re-submit prevention | `form-docgen.js` | Button disabled + "⏳ Submitting…" | |
| 46 | +| 8 | Missing form title in viewer | `form-engine.js` | Extracted from response data or DOM | |
| 47 | +| 9 | Help/demo missing for Forms | `help-mode.js` | Added `32_form_sharing.webp` demo | |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +## Files Modified |
| 52 | + |
| 53 | +#### [MODIFY] [cloud-share.js](file:///Users/jyotibose/textagent.github.io/js/cloud-share.js) |
| 54 | +- `hashResponseKey()` — SHA-256 hash of rk using `crypto.subtle.digest` |
| 55 | +- `doQuickShare()` / `doSecureShare()` — detect forms, generate rk, pass rkHash |
| 56 | +- `createCompactShare()` — store `rkHash` in Firestore doc |
| 57 | +- `loadSharedMarkdown()` — validate URL's rk against stored rkHash |
| 58 | +- Read-only interceptor — exempt `.form-dg-submit` and `.form-dg-responses-btn` |
| 59 | + |
| 60 | +#### [MODIFY] [form-engine.js](file:///Users/jyotibose/textagent.github.io/js/form-engine.js) |
| 61 | +- `escapeHtml()` — XSS prevention for response table |
| 62 | +- Removed `storeResponsePlain()` — encryption-only storage |
| 63 | +- Dynamic column collection from all responses |
| 64 | +- Form title in response viewer header |
| 65 | + |
| 66 | +#### [MODIFY] [form-docgen.js](file:///Users/jyotibose/textagent.github.io/js/form-docgen.js) |
| 67 | +- Re-submit prevention (disabled button + status text) |
| 68 | + |
| 69 | +#### [MODIFY] [firestore.rules](file:///Users/jyotibose/textagent.github.io/firestore.rules) |
| 70 | +- `rkHash` field allowed in share document schemas (both quick and secure share) |
| 71 | + |
| 72 | +#### [MODIFY] [help-mode.js](file:///Users/jyotibose/textagent.github.io/js/help-mode.js) |
| 73 | +- Form help entry → dedicated `32_form_sharing.webp` demo |
| 74 | + |
| 75 | +#### [NEW] [32_form_sharing.webp](file:///Users/jyotibose/textagent.github.io/public/assets/demos/32_form_sharing.webp) |
| 76 | +- Demo recording of full form sharing flow |
| 77 | + |
| 78 | +--- |
| 79 | + |
| 80 | +## Firestore Rules (Deployed) |
| 81 | + |
| 82 | +``` |
| 83 | +rkHash — optional string field in share documents |
| 84 | +responses subcollection — world-writable (create), world-readable, immutable (no update) |
| 85 | +``` |
| 86 | + |
| 87 | +## Verification Results |
| 88 | + |
| 89 | +| Test | Result | |
| 90 | +|------|--------| |
| 91 | +| Form creation + sharing | ✅ Dual links generated | |
| 92 | +| Respondent submission | ✅ Success message shown | |
| 93 | +| Re-submit prevention | ✅ Button disabled after click | |
| 94 | +| Creator response viewer | ✅ Form title + correct columns | |
| 95 | +| Valid rk → Responses button | ✅ Appears correctly | |
| 96 | +| Fake rk → rejected | ✅ No Responses button (new shares only) | |
| 97 | +| `npm run build` | ✅ Clean build | |
| 98 | + |
| 99 | +> [!IMPORTANT] |
| 100 | +> Legacy shares (created before rules deployment) don't have `rkHash` stored. The code safely rejects `rk` for legacy docs. Only newly shared forms get full rk validation. |
0 commit comments