docs: add credential capture design spec
Experimental feature for auto-detecting login form submissions and prompting to save/update credentials. Configurable bar or toast prompt style, off by default, with per-site blacklist. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
# idfoto — Credential Capture Design
|
||||
|
||||
Experimental feature that detects login form submissions and prompts the user to save or update credentials in the vault. Configurable prompt style (notification bar or toast). Off by default.
|
||||
|
||||
## Scope
|
||||
|
||||
- Content script: detect form submissions with password fields, capture credentials
|
||||
- Prompt UI: injected notification bar or floating toast (user-configurable)
|
||||
- Dedup: check manifest before prompting — skip if already saved, offer update if password changed
|
||||
- Blacklist: "Never for this site" option, persisted in `chrome.storage.local`
|
||||
- Settings: enable/disable capture, choose prompt style
|
||||
- Popup: settings view accessible from unlock screen
|
||||
|
||||
## Trigger
|
||||
|
||||
The content script listens for two events on forms that contain a password field:
|
||||
|
||||
1. `submit` event on the `<form>` element
|
||||
2. `click` event on submit buttons (`button[type=submit]`, `input[type=submit]`, or buttons inside the form)
|
||||
|
||||
When triggered:
|
||||
1. Read the username value from the detected username field (same detection priority as `detector.ts`)
|
||||
2. Read the password value from the password field
|
||||
3. If either is empty, skip
|
||||
4. Send `{ type: 'check_credential', url, username, password }` to the service worker
|
||||
|
||||
## Service Worker: `check_credential` Message
|
||||
|
||||
New message type added to the Request union:
|
||||
|
||||
```typescript
|
||||
{ type: 'check_credential'; url: string; username: string; password: string }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```typescript
|
||||
{ ok: true; data: { action: 'save' | 'update' | 'skip'; entryId?: string; entryName?: string } }
|
||||
```
|
||||
|
||||
Logic:
|
||||
1. If vault is locked, respond `skip`
|
||||
2. Check `captureBlacklist` in `chrome.storage.local` — if the URL's hostname is blacklisted, respond `skip`
|
||||
3. Check `captureEnabled` setting — if false, respond `skip`
|
||||
4. Search manifest entries by hostname match (same as `findByUrl`)
|
||||
5. If no match: respond `{ action: 'save' }`
|
||||
6. If match with same username and same password: respond `{ action: 'skip' }` (already saved)
|
||||
7. If match with same username but different password: respond `{ action: 'update', entryId, entryName }` (password changed)
|
||||
8. If match with different username: respond `{ action: 'save' }` (new account on same site)
|
||||
|
||||
To compare passwords in step 6/7, the service worker must decrypt the matched entry to read the stored password. This is acceptable because it only happens on form submission, not on every page load.
|
||||
|
||||
## Prompt UI
|
||||
|
||||
Two styles, user-configurable:
|
||||
|
||||
### Bar Mode (default)
|
||||
|
||||
A fixed-position bar at the top of the page, injected into the DOM:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ idfoto: Save login for github.com? (alee) [Save] [Never] [✕] │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Background: #161b22, border-bottom: 1px solid #30363d
|
||||
- Text: #c9d1d9, monospace font
|
||||
- Slides down from top with CSS transition
|
||||
- z-index: 2147483647 (max, above everything)
|
||||
- Save button: #1f6feb, Never button: #21262d, Dismiss: ✕ icon
|
||||
- For updates: "Update password for github.com? (alee)" with [Update] button
|
||||
|
||||
### Toast Mode
|
||||
|
||||
A floating element in the bottom-right corner:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ idfoto │
|
||||
│ Save login for github.com? │
|
||||
│ alee │
|
||||
│ [Save] [Never] [✕] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Position: fixed, bottom: 16px, right: 16px
|
||||
- Same color scheme as bar mode
|
||||
- Border: 1px solid #30363d, border-radius: 4px
|
||||
- Auto-dismiss after 15 seconds if not interacted with
|
||||
- For updates: same layout with "Update password?" text
|
||||
|
||||
### Prompt Behavior
|
||||
|
||||
When user clicks:
|
||||
- **Save:** Content script sends `add_entry` message to service worker with `{ name: hostname, url: full_url, username, password }`. On success, prompt shows brief "Saved" confirmation then disappears.
|
||||
- **Update:** Content script sends `update_entry` message with the existing entry ID and new password. Brief "Updated" confirmation.
|
||||
- **Never:** Content script sends `{ type: 'blacklist_site', hostname }` to service worker, which appends to `captureBlacklist` in `chrome.storage.local`. Prompt disappears.
|
||||
- **Dismiss (✕):** Prompt disappears. No action taken. Will prompt again next time.
|
||||
|
||||
## Settings
|
||||
|
||||
Stored in `chrome.storage.local` under key `settings`:
|
||||
|
||||
```typescript
|
||||
interface IdfotoSettings {
|
||||
captureEnabled: boolean; // default: false
|
||||
captureStyle: 'bar' | 'toast'; // default: 'bar'
|
||||
}
|
||||
```
|
||||
|
||||
Plus a separate key `captureBlacklist: string[]` (array of hostnames).
|
||||
|
||||
### Settings View in Popup
|
||||
|
||||
Accessible from the unlock screen via a "settings" link (already exists as a button). New popup view `settings` that shows:
|
||||
|
||||
```
|
||||
← back
|
||||
|
||||
SETTINGS
|
||||
|
||||
CREDENTIAL CAPTURE (experimental)
|
||||
[toggle] Auto-detect logins
|
||||
Style: [bar ▾] / [toast]
|
||||
|
||||
BLACKLISTED SITES
|
||||
github.com [✕]
|
||||
netflix.com [✕]
|
||||
```
|
||||
|
||||
The toggle and style selector write to `chrome.storage.local`. Blacklist entries can be removed individually.
|
||||
|
||||
## New Message Types
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
| { type: 'check_credential'; url: string; username: string; password: string }
|
||||
| { type: 'blacklist_site'; hostname: string }
|
||||
| { type: 'get_settings' }
|
||||
| { type: 'update_settings'; settings: Partial<IdfotoSettings> }
|
||||
| { type: 'get_blacklist' }
|
||||
| { type: 'remove_blacklist'; hostname: string }
|
||||
|
||||
// Response for check_credential
|
||||
{ ok: true; data: { action: 'save' | 'update' | 'skip'; entryId?: string; entryName?: string } }
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
### New files
|
||||
|
||||
```
|
||||
extension/src/content/capture.ts # Form submission listener + prompt injection
|
||||
extension/src/popup/components/settings.ts # Settings view
|
||||
```
|
||||
|
||||
### Modified files
|
||||
|
||||
```
|
||||
extension/src/content/detector.ts # Import and init capture module
|
||||
extension/src/service-worker/index.ts # Handle new message types
|
||||
extension/src/shared/messages.ts # Add new Request/Response types
|
||||
extension/src/shared/types.ts # Add IdfotoSettings interface
|
||||
extension/src/popup/popup.ts # Add 'settings' view to state machine
|
||||
extension/src/popup/components/unlock.ts # Wire up settings button
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Credentials are captured from the DOM only on form submission — no keylogging, no continuous monitoring
|
||||
- Captured credentials are sent to the service worker via `chrome.runtime.sendMessage` (same secure channel as autofill)
|
||||
- The prompt UI is injected into the page DOM but styled with inline styles and high z-index to avoid CSS conflicts
|
||||
- The "Never" blacklist prevents unwanted prompting but doesn't affect manual autofill
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Detecting password change forms (change old → new password flows)
|
||||
- Capturing credentials from non-standard login flows (OAuth redirects, SSO)
|
||||
- Syncing settings across devices
|
||||
Reference in New Issue
Block a user