Add autofill for desktop browser login forms#12
Merged
Merged
Conversation
Adds a bookmarklet-based autofill flow: a javascript: URL (installable from VaultSheet) opens Portpass as a popup, performs an ECDH key exchange, and returns AES-GCM-encrypted credentials to a page overlay that types them into the focused form field per a per-record autotype sequence. - pwsafe: read/write Autotype field (UUID 0x0025) alongside existing fields - RecordEdit/RecordRead: autotype sequence input with validation and read view - Dashboard: postMessage handler (hello/query protocol, gated on isPopup) - App: popup mode detection, locked-vault error handler, window.name for reuse - StartPage: "Unlock to use Autofill" hint when opened as popup - VaultSheet: AUTOFILL section with bookmarklet chip and copy-link button - bookmarklet.js: self-contained IIFE; sendRetry covers WASM load time - Tests: autofill.spec.ts, autofill_popup.spec.ts, bookmarklet.spec.ts, vault_sheet.spec.ts (bookmarklet UI tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
window.open() with a named target cannot cross browsing-context-group boundaries in modern browsers, so every bookmarklet click opened a fresh locked popup requiring re-unlock. Fix: when a popup detects an already-open unlocked Portpass tab via a BroadcastChannel ping, it enters relay mode — skipping WASM and the vault UI entirely, and instead bridging the bookmarklet's postMessage protocol to the main tab over BroadcastChannel. The main tab's Dashboard listens for relay-ping/relay-hello/relay-query and responds in kind. The relay popup auto-closes after forwarding the record or an error. Falls back to the original unlock-in-popup flow when no main tab responds within 300 ms (e.g. Portpass not yet open). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…flashes The bookmarklet previously opened relay.html twice as a full tab (once to fetch matching records, once to fetch credentials), causing two disruptive tab switches per autofill. Now relay.html opens once as a small popup window (popup=yes) and handles the entire flow itself: ECDH key exchange with Dashboard, URL search, picker UI, credential decryption, and fill delivery. The bookmarklet is reduced to: open popup → wait for ready → send init → wait for fill → execute autotype. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reset __ppRunning immediately when user closes the relay popup (was stuck for 60s, making re-click a no-op) - Extend fill-wait timeout to 1 hour (pp.closed polling handles dismissal) - Save full origin+pathname URL when replacing record URL, not canonical form - Show existing URL verbatim in Replace URL label, not canonicalized Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dling New codes: \m email, \2 TOTP, \s Shift-Tab, \fN custom field (N=1–9), \wNNN wait NNN ms, \WNNN wait NNN s, \\ literal backslash. Literal (non-backslash) text between codes is typed into the current field. Unknown codes no longer block save — they show a warning in both the edit form and read view instead. Portpass silently skips them at fill time so sequences written for the official Password Safe app still save correctly. Fixed: adjacent literal segments separated by \\ or an unknown code were incorrectly emitted as separate fillField calls, causing each to overwrite the previous. They now accumulate into one token. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…marklet chip - relay.html: Pkey favicon + brand bar (logo + "Portpass" heading) - bookmarklet fill overlay: brand bar with 16px logo above record title - VaultSheet bookmarklet chip: replace bookmark SVG with Pkey icon Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard now computes sensitiveCodes (p, 2, sensitive custom fields) alongside encrypted credentials. relay.html checks the autotype sequence against sensitiveCodes using a proper parser — literal \\ no longer causes false positives — and errors if any sensitive field would be filled on a non-HTTPS page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ile autofill) - delegates.js: new IDB module — per-vault ECDSA P-256 delegate storage with getDelegates/addDelegate/revokeDelegate/verifyAndUpdate - bookmarklet.js: replace insecure v1 bookmarklet with delegate variant; makeDelegateBookmarkletUrl embeds ECDSA private key in javascript: URL; relay.html receives privKey via targetOrigin postMessage - VaultSheet: replace static bookmarklet chip with delegate list (name/created/ uses/last-used/Revoke) and "New bookmarklet" modal; key generation happens at create time, private key shown once in draggable chip, never stored Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…op (steps 2–4)
relay.html:
- Mode detection now based on BC ping result, not privKey presence; after init
arrives, waits 200ms for a BC pong — same-profile if pong comes, cross-profile
if it times out (privKey always present for delegate bookmarklets)
- Cross-profile path: import ECDSA privKey, derive pubkey SPKI from JWK, sign
{url,nonce,ecdh,ts}, fire web+portpass:// via anchor click, poll
127.0.0.1:7677/pick/{nonce}, ECDH-decrypt blob, show picker
- Same-profile path: unchanged BC flow; onHelloResponse sends query with
currentUrl directly (pendingSameProfileUrl buffer removed)
- processRelayBlob handles {error} blobs from Portpass
App.svelte (handleIntent):
- Parses web+portpass://autofill?url=&sig=&pub=&ecdh=&nonce=&ts= params
- Rejects if vault locked, params missing, or ts outside ±60s/5s window
- Verifies ECDSA signature via verifyAndUpdate (increments useCount/lastUsed)
- On success sets pendingIntent state, passed as prop to Dashboard
Dashboard.svelte:
- New intent/onclearintent props
- buildRecordFields: extracts plain credential fields for cross-profile blob
- processAutofillIntent: finds matching records, ECDH-encrypts array for
relay.html's pubkey, POSTs {ephPub,iv,ciphertext} blob to relay server;
POSTs {error} blob on failure so relay.html shows error immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… verification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace web+portpass:// protocol handler approach with relay server as two-way
channel. Chrome routes web+portpass:// to the active profile rather than the
profile where the PWA is installed, making the LaunchQueue approach unreliable
for cross-profile use on Linux.
New cross-profile flow:
- relay.html signs the request and POSTs {url,nonce,ecdh,ts,sig,pub} to
/drop/{delegateId} on portpass-relay, then polls /pick/{nonce} for credentials
- Dashboard checks /pick/{delegateId} for each registered delegate on window focus;
verifies ECDSA signature, increments useCount/lastUsed, encrypts credentials,
POSTs blob to /drop/{nonce} — no protocol handler or LaunchQueue needed
- delegateId embedded in bookmarklet URL alongside privKeyJwk
- index.html CSP: add http://127.0.0.1:7677 to connect-src
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…timeouts relay.html: relay.c makes a single recv() call and memcpy's from that buffer using Content-Length — when the POST body hasn't fully arrived, the stored blob is zeroed/garbage. Fix: pollRelayServer detects bad blobs (empty, unparseable, or missing ephPub/error keys) and calls an onBadBlob callback; startCrossProfile passes a callback that re-posts the signed request so Portpass re-processes and re-drops the correct credentials. Retry adds ~2s on the bad-blob path, which occurs intermittently on loopback. index.html: add http://127.0.0.1:7677 to connect-src CSP directive so Dashboard can fetch the relay server. relay.html: error close extended to 30s, fill close to 10s for debugging; detailed processRelayBlob and pollRelayServer logging retained. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
relay.html doHello() now signs {relayNonce, ecdhSpki} with the ECDSA
private key (already in scope from the bookmarklet URL) and includes
sig/pub/ecdhSpki in the relay-hello BC message.
Dashboard relay-hello handler verifies the signature via verifyAndUpdate
before completing the ECDH exchange. Rejects unsigned or unverified
hellos with relay-error. This closes the masquerade attack on the
same-profile path: a content script at the Portpass origin can observe
the BC channel but cannot forge the signature.
Side effect: useCount/lastUsed now increment for same-profile requests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bookmarklet.spec.ts: - Replace makeBookmarkletUrl with makeDelegateBookmarkletUrl - Add createDelegateBookmarklet() helper that opens VaultSheet, creates a delegate via the UI, and captures the bookmarklet href — no production code exposure needed; also exercises the delegate creation flow itself - setupAutofillTest() now creates a delegate and returns bookmarkletUrl - activateBookmarklet() takes bookmarkletUrl as a parameter - All 9 tests updated to pass the bookmarklet URL - Timeout bumped to 30s to accommodate delegate creation UI steps autofill_popup.spec.ts: - Remove the 4 "Autofill relay mode" tests: they drove App.svelte's tryRelay() bridge (Portpass as popup), which is no longer the production autofill path and is now broken because Dashboard requires ECDSA on relay-hello. The same-profile relay flow is covered by bookmarklet.spec.ts. - Query protocol and UI tests (9 tests) unchanged — they test direct postMessage to Portpass popup, which is unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add login page's #__pp to the Promise.race so the test proceeds as soon as the bookmarklet renders the error overlay (~2s), rather than waiting for the popup to close. With the extended debug close timeout (30s), the popup-close path never wins within 10s, causing the overlay to be auto-dismissed before the assertion ran. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use localhost:7677 instead of 127.0.0.1:7677 for portpass-relay URLs in relay.html, Dashboard.svelte, and index.html CSP — localhost has a stronger loopback exemption in Firefox's mixed-content rules; relay.c binds to INADDR_LOOPBACK which handles both - relay.html postRequest(): catch TypeError and surface the clear "portpass-relay is not running — start it first" message instead of the raw "NetworkError when attempting to fetch resource." that was previously shown when the relay server was not running Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove all [portpass] and [portpass relay] console.log statements added during cross-platform debugging. Revert relay.html close delays: fill popup back to 100ms, sendError back to 3000ms. Fix syntax error in delegates.js verifyAndUpdate introduced by log removal (missing try/catch closing braces). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The cleanup agent removed console.log lines but also accidentally: - Stripped the closing }) and .catch of postError in processAutofillIntent, leaving it as an unclosed arrow function (Vite compile error) - Removed the timestamp expiry check (age > 60s) in checkPendingAutofillRequests along with its log line — requests would never expire - Dropped try/catch closing braces in delegates.js verifyAndUpdate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update SECURITY.md: - Autofill section under dedicated-profile mitigation: describe both same-profile and cross-profile modes; explain delegate model + portpass-relay allows cross-profile without sacrificing extension isolation - Add "Autofill security" section: trusted islands, ECDSA authentication, ECDH+AES-GCM credential encryption, relay server as dumb pipe, limitations (credential in DOM, bookmarklet theft, install-time boundary) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…extensions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The clean profile must be free of extensions to ensure the bookmarklet dragged to the destination browser is genuine. Extensions in the destination browsing profile cannot intercept it because the key arrives already stored in the bookmark store. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
README.md: - Add Autofill to feature list - Update Password Safe comparison: autofill into native apps is still missing, but web form autofill via bookmarklet is now a Portpass strength - New Autofill section: how it works, same-profile vs cross-profile, setup steps, autotype codes, best practices; references SECURITY.md for delegate model and threat details SECURITY.md: - Already committed in prior commits this session Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Firefox doesn't implement the File System Access API, so the app previously showed a dead-end error. Now it falls back to <input type="file">, opens the vault with readonly:true, and skips features that need a persistent handle (recent vaults, secondary auto-unlock, biometric offer). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ict) Updates all hardcoded port references in Dashboard.svelte, relay.html, and the CSP connect-src in index.html. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
delegates.js:
- Replace useCount/lastUsed with bcCount/bcLastUsed and relayCount/relayLastUsed
- verifyAndUpdate() takes a channel arg ('bc' or 'relay') to increment the right pair
- Migration zeros counters on old delegate records
- getRelayUrl/setRelayUrl: per-vault relay URL stored in IDB (default http://localhost:7577)
store.js: add relayUrl writable store
Dashboard.svelte:
- Init relayUrl store from IDB on vault open
- Use relayUrl store for all relay server fetch URLs (replaces hardcoded localhost:7577)
- Pass 'bc' or 'relay' channel to verifyAndUpdate at each call site
VaultSheet.svelte:
- Delegate rows show total uses (bc + relay) and max last-used timestamp
- Advanced section: relay URL input with Save/Cancel when dirty, 3-state status
indicator (blank/ok/error) probed on open and every 5s, count of cross-profile
autofill uses across all delegates
- Probe uses AbortSignal.timeout(500ms); status blank until first probe completes
relay.c: add GET /status → 200 "ok" for VaultSheet probe
vault_sheet.spec.ts: fix 5 pre-existing failures — bookmarklet chip tests now go
through the + New bookmarklet flow before asserting chip visibility
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Relay now returns 204 No Content instead of 404, suppressing Chrome's automatic console error logging on every poll cycle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chrome logs connection errors at the network layer before JS sees them — they can't be suppressed. Backoff reduces the noise from once per 2s to once per 30s when portpass-relay is not running. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
concurrent_save: c3a13c3 shortened the conflict dialog message but didn't update the test assertion — "modified by another Portpass instance" → "modified since it was loaded". clipboard: the intermediate readText() assertion at line 99 raced with the 200ms mock timer; the timer could clear the clipboard between waitForFunction resolving and the next page.evaluate call. The assertion was redundant (copyPassword already verified content via waitForFunction); removed it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, remove dead base32 code - pseudoRandomBytes: replace silent zero-padding fallback with panic; crypto/rand failure is an environment invariant violation, not a recoverable condition - refreshEncryptedKeys: derive new EncryptionKey/HMACKey into locals and write back to db only after all operations succeed, preventing partial struct mutation on error - RecordEdit: remove dead base32Encode/base64ToBase32 helpers; TwoFactorKey is always withheld (null) in the record view, so totpSecret initialises directly to '' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…version Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tab interception Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use `npm ci` instead of `npm install` in `make setup` to enforce lock file and prevent silent dependency drift in CI and local builds - Run `npm audit fix`: upgrade Svelte 5.55.5→5.55.9 and devalue 5.8.0→5.8.1 to clear two reported vulnerabilities (both SSR-only, not exploitable here) - Reject opening a secondary vault whose UUID matches an already-open secondary vault, preventing silent eviction of a legitimate vault from WASM memory Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per the pwsafe v3 spec, unknown property IDs within field 0x30 must be retained when the database is saved. Adds UnknownProps storage to CustomField, populates it in parseCustomFields, writes it back in marshalCustomFields, and propagates it through the WASM UpdateRecordFields path. Adds round-trip unit tests at both the parse/marshal and file level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ss API - Fix showOpenFilePicker detection to use typeof check (iOS has the property but it's not a callable function) - Open vaults via fallback <input type="file"> on iOS; mark them read-only since createWritable() isn't supported - Enable Edit button for read-only vaults (primary and secondary), showing a 'read-only' chip next to it - Show 'Save as' instead of 'Save' when editing a read-only vault; triggers showSaveFilePicker (Firefox) or blob download (iOS) - After Save As, update selectedFile so the vault becomes writable going forward - Keep FAB and desktop New button visible regardless of read-only status - Enable biometric/PIN unlock on fallback (iOS) path: offer after password unlock, show button on file re-selection when enrolled - Support 'Unlock additional vault' on iOS via hidden file input fallback - Handle read-only secondary vaults with the same Save As flow - Add readonly_vault.spec.ts (10 tests); update multi_vault and unlock specs to reflect new read-only Edit/chip behaviour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bare URLs (e.g. yahoo.com) were treated as relative paths, routing to /portpass/yahoo.com. Prepend https:// when no scheme is present in all three code paths: RecordRead link, RecordList context menu, and Dashboard Enter-key handler. Tests cover all three paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(pointer: fine) checks only the primary pointer — on Windows laptops with touchscreens, the touchscreen is primary so the check fails even with a mouse/trackpad connected. Switch to (any-pointer: fine) so the autofill fields appear whenever any fine pointer is available. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Opening the same vault file as a secondary (or re-adding an already-open secondary) caused closeDatabase() to remove the live vault from WASM memory, breaking all subsequent record reads and vault detail lookups. Fix 1 (JS): remove the closeDatabase() calls in the duplicate-detection early-return paths in confirmSecondarySetup() — the vault is already in the WASM map under that UUID, so closing it would destroy the live instance. Fix 2 (Go/WASM): openDB() now skips the map store if the UUID already exists, preserving any unsaved in-memory edits if the same file is opened again. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- autofill.html: onOpenerMessage now checks event.source === window.opener before processing init messages, closing a theoretical spoofing path (matches the guard already present in onBookmarkletMessage) - biometric.js: remove leftover [DEBUG] console.error in unlockWithBiometric catch block that was leaking WebAuthn error details in production Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
autofill.html was missing the same baseline security headers present in index.html. Adds a tight CSP (no WASM eval needed, inline styles required since styles are embedded, ws://localhost:7577 for switchboard) and no-referrer referrer policy to prevent leaking the current page URL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- autofill.html: remove || '*' fallback on all outgoing postMessage calls; fill message (which carries credential data) and cancel messages now require bookmarkletOrigin to be set, matching the guard on event.source added previously. ready signal legitimately keeps '*' (fired before init). - index.html: base-uri 'self' → 'none' to block <base> tag injection entirely, consistent with autofill.html which already had 'none'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
script-src 'self' blocks inline scripts; autofill.html is entirely inline JS with no build tooling to generate script hashes, so 'unsafe-inline' is required. The CSP still restricts object-src, base-uri, connect-src, and img-src. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace regex/startsWith scheme checks with url.includes('://')
which handles any scheme (http, https, ftp, ssh, etc.). Also convert
the external-link icon in RecordRead from a bare <a href> to an
onclick handler with window.open(), so the browser never resolves the
URL as a relative path regardless of template-expression evaluation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Try keyboard events first (for SPA Enter-key listeners), then look for [type=submit] or [default-button] within the closest form, then fall back to the sole button in the form if there is exactly one. Page-level button searches are intentionally excluded to avoid clicking unrelated buttons. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Vite transform plugin that runs terser (compress + mangle) on the DELEGATE_BOOKMARKLET_IIFE source at build time, replacing the .toString() call with the pre-minified string literal. Bookmarklet source stays readable; generated javascript: URLs are properly minified in both dev and production. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Firefox lacks showOpenFilePicker and can't serialize FileSystemFileHandle to IDB, so no handle was ever stored and the unlock screen always showed blank. Three changes: - recentHandles: always store `name` alongside handle; if IDB write fails (handle not serializable), retry storing name only - StartPage: load remembered name from IDB even when supportsFilePicker is false; show it on unlock screen and open file picker on Unlock click - StartPage: guard requestPermission/queryPermission calls (Chrome-only API extensions) so Firefox 111+ doesn't throw on the handle path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uppercase text, slightly heavier weight, text-soft color, and vertical alignment fix for the chip alongside the Edit button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(any-pointer: fine) returns false on Windows touch devices (Surface, tablet mode) even though bookmarklets work fine on any desktop OS. Replace with UA-based mobile detection so Windows+Edge and Windows+Firefox always show the autofill/delegate UI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a bookmarklet-based autofill feature for use in desktop browsers (mobile not supported). Autofill simplifies the process to get data from the vault to the webpage form by filling login forms directly without using the clipboard. Each bookmarklet is a cryptographic delegate (an ECDSA P-256 key pair generated at install time) so credentials are authenticated and encrypted in transit.
Supports two modes:
What's included
Autotype sequence
Sequences describe what to type into login forms: \u\t\p\n (fill username → Tab → fill password → Enter) is the default. Extended codes: \m email, \2 TOTP, \fN custom field N, \wNNN/\WNNN wait, \s Shift-Tab, \ literal backslash. Unknown codes produce a warning (amber banner) but don't block save; structural errors block save.
Tests
persistence, read view
Known issue
Bookmarklet shows a globe icon in Chrome's bookmarks bar (root cause: App.svelte replaces the favicon with a data: URL, which overrides the bookmark icon). Workaround: bookmark Portpass normally first, then edit the URL to the javascript: code.