summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md21
-rw-r--r--TODO.md120
-rw-r--r--architecture.md128
-rw-r--r--message-catalog.md152
-rw-r--r--telemetry.md146
-rw-r--r--trust-boundaries.md169
-rw-r--r--webauthn-analysis.md160
8 files changed, 898 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d0bf9d5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+1pass-xpi/
+.pi/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8ad8c35
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+# 1Password Firefox Extension Security Review
+
+Static analysis of the 1Password browser extension for Firefox (`1pass-xpi`), version **8.12.2.38** (stable channel, built 2026-02-11).
+
+## Documentation
+
+| File | Description |
+|------|-------------|
+| [architecture.md](architecture.md) | Runtime topology, boot pipeline, permission model, WASM modules |
+| [message-catalog.md](message-catalog.md) | Complete message handler map extracted from source |
+| [webauthn-analysis.md](webauthn-analysis.md) | WebAuthn monkey-patching, page-world IPC protocol, attack surface |
+| [trust-boundaries.md](trust-boundaries.md) | Trust zones, dataflow, sensitive data classes, attack surfaces |
+| [telemetry.md](telemetry.md) | Snowplow analytics, Sentry error reporting, DNS privacy proxy |
+| [TODO.md](TODO.md) | Planned future work and open questions |
+
+## Source Material
+
+- `1pass-xpi/` — Extracted extension package (XPI/ZIP), Mozilla-signed
+- Extension ID: `{d634138d-c276-4fc8-924b-40a0ea21d284}`
+- Manifest version: 2 (Firefox)
+- Gecko minimum: 128.0
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..751f9ad
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,120 @@
+# TODO — Future Work
+
+## High Priority
+
+### Deep Reverse Engineering of background.js
+- [ ] Beautify/pretty-print the 2.9MB background.js for readable analysis
+- [ ] Map all `m5({...})` handler implementations — trace what each handler does after receiving a message
+- [ ] Determine if message schema validation exists at the background router level (critical for security posture)
+- [ ] Identify the `sdj(wd)` call — appears to register additional handlers or middleware
+- [ ] Trace the `VA()` function used for broadcasting events to tabs
+
+### Native Messaging Protocol
+- [ ] Identify the native app ID used with `chrome.runtime.sendNativeMessage`
+- [ ] Map the command/response schema for native messaging
+- [ ] Determine what operations are delegated to the native app vs handled in-extension
+- [ ] Analyze the 6 localhost ports (12519, 40978, 52115, 22287, 60685, 22322) — what protocol, what data
+- [ ] Check if localhost connections use any authentication/signing
+
+### Frame Relay Security
+- [ ] Trace `relay-message-to-frames` handler (`eij`) — does it validate sender frame? Does it restrict what messages can be relayed?
+- [ ] Trace `targeted-message-to-inline-menu` handler (`tij`) — same questions
+- [ ] Determine if a compromised iframe can spoof messages to another frame's inline menu
+- [ ] Check origin validation on all frame-relay paths
+
+### WebAuthn Protocol Hardening Assessment
+- [ ] Test if a malicious page can inject fake `op-window-syn-ack` before the real extension responds
+- [ ] Test the 100ms re-patching race condition — can a page reliably intercept credentials between overwrite and re-patch?
+- [ ] Analyze what happens if two extensions both try to intercept WebAuthn
+- [ ] Check if the `stopImmediatePropagation` ordering is reliable at `document_start`
+
+### Save Object Pipeline
+- [ ] Trace the save flow end-to-end: DOM capture → `add-save-object` → public key encryption → storage
+- [ ] Verify the public key used for save object encryption is authenticated (not injectable)
+- [ ] Check what happens if a page injects fake form data before the save prompt
+
+## Medium Priority
+
+### WASM Module Analysis
+- [ ] Analyze exported functions from `op_wasm_b5x_bg` — what crypto primitives are exposed to JS?
+- [ ] Determine if key material ever crosses the WASM→JS boundary in plaintext
+- [ ] Check `confidential_computing_bg` — what confidential computing features are used and where?
+- [ ] Analyze `b5_mycelium_bg` — is this the Mycelium relay for remote autofill? What's the protocol?
+- [ ] Map HPKE usage — what is encrypted with HPKE vs AES-GCM vs RSA-OAEP?
+
+### External Extension Messaging
+- [ ] Trace all 3 `onMessageExternal` handler registrations
+- [ ] Determine what messages are accepted from external extensions
+- [ ] Check if there's any authentication/validation of the sender extension ID
+
+### Large Chunk Analysis
+- [ ] Analyze `chunk-OJR52IF5.js` (1.9MB) — likely contains the bulk of vault/account logic
+- [ ] Analyze `chunk-MSIWLBOQ.js` (804KB) — likely UI component library or crypto support
+- [ ] Analyze `chunk-22IBMJDR.js` (138KB)
+- [ ] Analyze `chunk-QEIU26VY.js` (92KB)
+- [ ] Analyze `chunk-PQH6ALAA.js` (156KB)
+
+### Credential Fill Path
+- [ ] Trace the fill flow: background resolves item → decrypts → sends to content script → injects into DOM
+- [ ] Identify where decrypted password/OTP/card values exist in JS memory and for how long
+- [ ] Check if clipboard operations (`clipboardWrite`) properly clear after timeout
+- [ ] Analyze `fill-generated-password` flow — is the generated password ever in plaintext outside WASM?
+
+### Partner Integration Data Minimization
+- [ ] Privacy.com: what data is sent to `api.privacy.com`? Card params, user identity?
+- [ ] Fastmail: what data is sent to Fastmail JMAP? Email addresses, domain info?
+- [ ] Brex: what data flows to `platform.brexapis.com`?
+- [ ] Kolide: what health/device data is shared? Analyze `kolide.js` (60KB)
+- [ ] Trelica: what app catalog data is exchanged?
+
+### Secure Remote Autofill (director.ai)
+- [ ] Analyze `secure-remote-autofill-start-pairing.js` (57KB) — pairing protocol
+- [ ] Analyze `secure-remote-autofill-complete-pairing.js` (59KB) — completion flow
+- [ ] Determine what crypto is used for the remote autofill channel (likely Mycelium + HPKE)
+- [ ] Check if the pairing is bound to specific devices/sessions
+
+### Shell Plugins / AI Agent Integration
+- [ ] Map the full shell-plugins subsystem — what AI browsing agents are supported?
+- [ ] Analyze credential detection in agent contexts (Browserbase, BrowserUse icons present in chunks)
+- [ ] Check what prompts/confirmations exist before saving agent-detected credentials
+- [ ] Assess risk of AI agents leaking credential data through their own telemetry
+
+## Lower Priority
+
+### Telemetry Deep Dive
+- [ ] Extract all Snowplow event schemas (iglu schema references)
+- [ ] Determine exactly what URL/page data appears in telemetry events
+- [ ] Verify redaction is applied consistently (not just in fill telemetry)
+- [ ] Check if error stack traces sent to Sentry contain sensitive URL parameters
+
+### Popup / App UI
+- [ ] Analyze `popup/index.js` (399KB) — what privileged operations can be triggered from popup?
+- [ ] Analyze `app/app.js` (682KB) — full window UI capabilities
+- [ ] Check for any XSS vectors in UI rendering of vault item data
+
+### Login Detection Heuristics
+- [ ] Analyze `heuristics.js` — what classifies as a login page?
+- [ ] Check for false positive scenarios that could trigger unintended autofill
+- [ ] Verify `LOGIN_EVENT` and `LOGIN_STEP` don't leak sensitive page content
+
+### Internationalization
+- [ ] Check if locale-specific code paths have different security properties
+- [ ] Analyze `assets/js/messages.i18n-*.js` files for any embedded data beyond translations
+
+### Web-Accessible Resource Fingerprinting
+- [ ] Document exactly which resources are web-accessible and can be probed by any site
+- [ ] Assess extension detection/fingerprinting risk from these resources
+- [ ] Check if `*.js.map` being web-accessible leaks useful information to attackers
+
+## Tooling & Infrastructure
+
+### Analysis Setup
+- [ ] Set up JS beautifier pipeline (prettier/esbuild) for all minified files
+- [ ] Build automated string extraction for message names, URLs, feature flags
+- [ ] Set up AST-based analysis for tracing message handler call chains
+- [ ] Create a dynamic analysis harness (load extension in test Firefox profile, intercept messages)
+
+### Documentation
+- [ ] Create a visual architecture diagram (Mermaid/D2)
+- [ ] Build a cross-reference index: message name → handler file → handler function → effects
+- [ ] Document all feature flags and their security implications
diff --git a/architecture.md b/architecture.md
new file mode 100644
index 0000000..986be62
--- /dev/null
+++ b/architecture.md
@@ -0,0 +1,128 @@
+# Architecture
+
+## Runtime Topology
+
+Four execution contexts:
+
+### 1. Background Page (control plane)
+- Entry: `background/background.html` → `background/background.js` (2.9MB minified)
+- Also loads: `background/health-check.js` (responds to `health-check-request`)
+- Central broker for all policy decisions, vault operations, native messaging, sync, telemetry
+- Initializes: database, core interface (WASM), feature flags (Unleash), native app connection, XAM backend, context menus, Watchtower data
+- Event subscriptions handle account changes, lock/unlock, session state transitions
+- Exports `b5xHandlers` for b5 web app integration and `initializeFinishedPromise` for startup gating
+
+### 2. Content Script Bootstrap (every page, every frame)
+- Entry: `inline/inject-content-scripts.js` at `document_start`, `all_frames: true`, `<all_urls>`
+- Guards against double-injection (`injectJsHasStarted` property)
+- Dynamically imports two modules:
+ - `/inline/injected.js` (368KB) — always loaded (page managers, autofill, inline menu, frame management)
+ - `/inline/injected/heuristics.js` — conditionally loaded when `login-detection-is-enabled` returns true from background
+- Import retry logic: 3 attempts with 25ms/50ms delays between retries
+- Error reporting via `report-error` message to background
+- Initializes a `LogReporter` (logger) that forwards all content-script logs to background via `new-tab-log-event`
+
+### 3. Specialized Content Scripts (host-specific)
+Declared in manifest, loaded on matching hosts:
+
+| Script | Hosts | Timing | Purpose |
+|--------|-------|--------|---------|
+| `webauthn.js` + `webauthn-listeners.js` | `https://*/*`, `http://localhost/*` | `document_start` | WebAuthn/passkey mediation (see [webauthn-analysis.md](webauthn-analysis.md)) |
+| `b5.js` | `*.1password.com/ca/eu`, `*.b5dev.*`, `*.b5test.*`, `*.b5local.*`, `*.b5staging.*`, `*.b5rev.*` | `document_idle` | 1Password web app integration, SSO completion, session init |
+| `kolide.js` | `app.kolide.com/ca/eu`, `auth.kolide.com/ca/eu` | `document_start` | Kolide device trust / EPM integration |
+| `secure-remote-autofill-start-pairing.js` | `www.director.ai/?*`, `www.director.ai/` | `document_end` | Remote autofill pairing initiation |
+| `secure-remote-autofill-complete-pairing.js` | `www.director.ai/complete-1password-pairing*` | `document_end` | Remote autofill pairing completion |
+| `autofill.js` | `autofill.me/*` | `document_start` | Test/demo autofill site |
+
+### 4. Extension UI Surfaces
+- `app/app.html` + `app/app.js` (682KB) — main extension window/panel
+- `popup/index.html` + `popup/index.js` (399KB) + `popup/set-popup-width.js` — browser action popup
+- `launcher/apps.html` + `launcher/apps.js` — app launcher
+- `inline/menu/menu.html` — inline autofill suggestion menu (web-accessible)
+- `inline/modal/modal.html` — modal dialogs for Privacy.com, email alias, Brex (web-accessible)
+- `inline/notification/notification.html` — save/update notifications (web-accessible)
+- `inline/universal-sign-on/universal-sign-on.html` — USO banner (web-accessible)
+- `inline/tutorial/tutorial.html` — onboarding tutorial
+- `devtools/devtools.html` + `devtools/panels.html` — DevTools logging panel
+
+## Chunk System
+
+356 chunk files under `chunks/`. Two categories:
+- **Code chunks**: `chunk-{HASH}.js` — shared logic modules (largest: `chunk-OJR52IF5.js` at 1.9MB, `chunk-MSIWLBOQ.js` at 804KB, `chunk-22IBMJDR.js` at 138KB)
+- **Icon/asset chunks**: named by icon (e.g., `icon_creditcard_color_32-HASH.js`, `sso_login_okta_32-HASH.js`)
+
+Semantic chunk name patterns observed: `account-family`, `account-team`, `developer_watchtower`, `browserbase_logo`, `browseruse-icon`, `anchor-browser-icon`, `browser-polyfill`, `import_guide_pen`.
+
+## WASM Modules
+
+Seven WebAssembly modules in `assets/wasm/` (total ~30MB):
+
+| Module | Size | Likely Purpose |
+|--------|------|----------------|
+| `op_wasm_b5x_bg` | 15.6MB | Core vault/crypto operations for b5x (extension) |
+| `op_wasm_xam_bg` | 11.2MB | XAM (cross-app management / device trust) backend |
+| `confidential_computing_bg` | 1.8MB | Confidential computing primitives |
+| `b5_trustlog_bg` | 1.3MB | Trust log generation/verification |
+| `b5_trust-verifier_bg` | 1.0MB | Trust verification |
+| `b5_mycelium_bg` | 319KB | Mycelium relay protocol (P2P communication for remote autofill) |
+| `b5_hpke_bg` | 82KB | Hybrid Public Key Encryption (RFC 9180) |
+
+The background.js initialization calls `rA.init(e)` ("initializeCoreInterface") which loads the main WASM module. CSP allows `wasm-unsafe-eval` for this purpose.
+
+## Permission Profile
+
+### Always granted
+`<all_urls>`, `alarms`, `clipboardWrite`, `contextMenus`, `downloads`, `idle`, `management`, `nativeMessaging`, `notifications`, `privacy`, `scripting`, `storage`, `tabs`, `webNavigation`, `webRequest`, `webRequestBlocking`, `declarativeNetRequestWithHostAccess`
+
+### Optional
+`bookmarks`
+
+### Web-Accessible Resources
+Source maps (`*.js.map`), fonts, images, and critically: `inline/injected.js`, `inline/injected/heuristics.js`, `inline/injected/styles/inline-tooltip.css`, and all inline UI HTML files (menu, notification, modal, universal-sign-on). These can be loaded/detected by any web page.
+
+## Feature Flags
+
+Unleash-based feature flag system with two tiers:
+- **Pre-registration flags**: evaluated before any account is signed in (e.g., `b5x-pre-auth-tracing`)
+- **Account-gated feature trials**: per-account feature flags from server
+
+Background broadcasts `unleash-features-changed` events to all listeners when flags update. Content scripts query individual flags (e.g., `login-detection-is-enabled`).
+
+## Initialization Sequence
+
+From `background.js` main init function (reconstructed):
+
+1. Initialize storage
+2. Initialize feature flags cache
+3. Check for terminated DB, set icon
+4. Get browser language
+5. Initialize Sentry
+6. Init build info
+7. Initialize core interface (WASM load)
+8. Set locale
+9. Initialize database (IndexedDB)
+10. Get device info, sync pre-registration feature flags
+11. Start performance observer (if tracing flag set)
+12. Load all accounts, set up account handlers
+13. Initialize crypto
+14. Subscribe to events: session changes, account updates, lock/unlock
+15. Load Watchtower data
+16. Initialize native app connection
+17. Initialize unlock-with-context cache
+18. Enable insiders (if appropriate)
+19. Initialize XAM backend
+20. Initialize App Launcher
+21. Migrate storage (if needed)
+22. Initialize context menus
+23. Initialize notifications
+
+## External Extension Communication
+
+3 references to `chrome.runtime.onMessageExternal` — the extension accepts messages from other extensions (likely 1Password desktop app or enterprise connectors). No `externally_connectable` manifest key found, so Firefox's default policy applies.
+
+## Build/Signing
+
+- Build channel: `stable`
+- Signed by Mozilla AMO Production Signing Service
+- COSE + RSA signatures in `META-INF/`
+- Sentry debug IDs embedded in every JS file for crash correlation
diff --git a/message-catalog.md b/message-catalog.md
new file mode 100644
index 0000000..db5be14
--- /dev/null
+++ b/message-catalog.md
@@ -0,0 +1,152 @@
+# Message Catalog
+
+All message handlers extracted from static analysis. Messages flow via `chrome.runtime.sendMessage` / `chrome.runtime.onMessage`.
+
+## Background Message Router
+
+The main handler registration (`m5({...})`) in `background.js` registers these content-script → background handlers:
+
+### Core / Account
+| Message | Notes |
+|---------|-------|
+| `get-account-list` | Also exported as `b5xHandlers` for b5 web app |
+| `get-default-account-info` | |
+| `open-extension-welcome-page-for-onboarding` | |
+
+### b5 Web App Integration
+| Message | Notes |
+|---------|-------|
+| `open-swe-native-wrapper` | Open native app wrapper from SWE |
+| `sign-in-succeeded` | b5 web app sign-in completed |
+| `b5-session-init` | Initialize session from b5 web app |
+| `b5x-request-session-init` | Extension requests session from b5 |
+| `b5-sso-complete` | SSO flow completed on b5 |
+| `b5x-credential-hydration` | Hydrate credentials from b5 |
+
+### Frame/Inline Management
+| Message | Notes |
+|---------|-------|
+| `relay-message-to-frames` | **Background relays messages between frames** — attack surface |
+| `targeted-message-to-inline-menu` | Send message to specific inline menu |
+| `refresh-can-request-unlock` | |
+| `get-frame-manager-configuration` | |
+
+### Autofill / Inline Suggestions
+| Message | Notes |
+|---------|-------|
+| `get-inline-suggestions` | Content script requests fill suggestions |
+| `perform-inline-action` | User selected an inline action |
+| `record-inline-menu-render-event-all-accounts` | Telemetry for inline menu render |
+
+### Save Flow
+| Message | Notes |
+|---------|-------|
+| `add-save-object` | Content script submits credential to save |
+| `get-save-object-public-key` | Get public key for encrypting save objects |
+
+### WebAuthn / Passkeys
+| Message | Notes |
+|---------|-------|
+| `create-credential` | Passkey creation request |
+| `get-credential` | Passkey authentication request |
+| `should-intercept-webauthn-request` | Content script asks if it should intercept WebAuthn |
+
+### Privacy.com Integration
+| Message | Notes |
+|---------|-------|
+| `enable-privacy-integration` | |
+| `get-privacy-enable-dialog-configuration` | |
+| `get-privacy-create-dialog-configuration` | |
+| `fill-new-privacy-card` | |
+
+### Email Alias (Fastmail)
+| Message | Notes |
+|---------|-------|
+| `get-email-alias-dialog-configuration` | |
+| `start-email-alias-session` | |
+| `generate-email-alias` | |
+| `cancel-email-alias-session` | |
+| `fill-new-email-alias` | |
+
+### Brex Integration
+| Message | Notes |
+|---------|-------|
+| `get-brex-dialog-configuration` | |
+| `fill-new-brex-card` | |
+
+### Shell Plugins (AI Agent Integration)
+| Message | Notes |
+|---------|-------|
+| `shell-plugins-notification-config` | |
+| `shell-plugins-dismiss-notifications` | |
+| `shell-plugins-save-in-1password-notification` | Credential detected in AI browsing agent |
+| `shell-plugins-fallback-notification` | |
+| `shell-plugins-site-config` | |
+| `shell-plugins-item-saving-prompt` | |
+| `shell-plugins-get-last-detected-credentials` | |
+| `shell-plugins-set-last-detected-credentials` | |
+
+### Error/Logging
+| Message | Notes |
+|---------|-------|
+| `report-error` | Structured error reporting from any context |
+| `health-check-request` | (in health-check.js) Returns `health-check-response` with `"alive"` |
+
+## Content Script → Background Messages (from inline scripts)
+
+### inject-content-scripts.js
+- `login-detection-is-enabled` — query feature flag
+- `new-log-event` — log from popup context
+- `new-tab-log-event` — log from content/extension page context
+- `report-error` — structured error report
+
+### injected.js (page managers)
+Sends all of the `m5` handler messages listed above, plus internal frame management:
+- `forward-active-field-details`
+- `forward-inline-menu-position`
+- `frame-takes-focus`
+- `provide-frame-origin`
+- `filter-inline-menu`
+- `focus-inline-menu`
+- `add-scroll-and-resize-event-listeners`
+- `remove-scroll-and-resize-event-listeners`
+- `edited-state-changed`
+
+### b5.js (1Password web app pages)
+- `b5-session-init`, `b5x-request-session-init`, `b5-sso-complete`
+- `b5x-credential-hydration`
+- `open-swe-native-wrapper`, `sign-in-succeeded`
+- `open-extension-welcome-page-for-onboarding`
+
+### heuristics.js (login detection)
+- `LOGIN_EVENT`, `LOGIN_STEP` — login heuristic detection events
+
+### modal.js (dialogs)
+- `can-request-unlock` — check if unlock is requestable
+- Privacy.com, email alias, Brex dialog messages (listed above)
+
+### notification.js
+- `DeviceTrust` — device trust notification
+- `report-error`
+
+### menu.js (inline menu)
+- `get-inline-suggestions`, `perform-inline-action`
+- `focus-page`, `save-item`, `request-verification-token`
+
+## Background → Content Script Messages
+
+Background uses `chrome.tabs.sendMessage` (7 occurrences) to push messages to tabs. Also uses `chrome.scripting.executeScript` (4 occurrences) for direct injection. Specific message names sent to tabs need deeper analysis of the minified code.
+
+## Background Broadcast Events (internal)
+
+Observed event names used in background's internal pub/sub system:
+- `accounts-and-vaults-changed`
+- `accounts-locked`
+- `can-request-unlock-changed`
+- `unleash-features-changed`
+- `extension-first-survey`
+- `unified-panel-update`
+
+## Message Volume
+
+Total unique `name:"..."` strings in background.js: **829** (includes API method names, crypto algorithm names, vault/group names, etc. — not all are message handler names).
diff --git a/telemetry.md b/telemetry.md
new file mode 100644
index 0000000..8f302f7
--- /dev/null
+++ b/telemetry.md
@@ -0,0 +1,146 @@
+# Telemetry & Error Reporting
+
+## Snowplow Analytics
+
+The extension uses Snowplow for product analytics/telemetry.
+
+### Endpoints
+- `com-1password-prod1.mini.snplow.net/com.snowplowanalytics.snowplow/tp2`
+- `telemetry.1passwordservices.com/com.snowplowanalytics.snowplow/tp2`
+
+### Schema
+Uses standard Snowplow protocol:
+- `iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4`
+- `iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0`
+- `iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0`
+
+### Context Data Attached
+From code analysis, telemetry events include:
+- `accountCreationDate` — when the account was created
+- `accountTier` — subscription tier name
+- `accountType` — account type
+- `userRole` — array of role strings (e.g., "guest")
+- `telemetryStatus` — whether user opted in
+- `billingProvider` (if available)
+- `idp` — identity provider (if SSO)
+- `activeMembers` count (if available)
+
+### Event Types Observed
+- Fill telemetry (`b5x-filling-saving-telemetry` feature flag)
+- USO (Universal Sign-On) telemetry (`reportUsoAction`, dedicated endpoint `/api/v1/uso/telemetry/{action}`)
+- Inline menu render events
+- Pre-auth telemetry (`pre-auth-telemetry-data-collection`)
+- B2B telemetry (`b2b-telemetry-setting-UI`)
+- Activation hub events
+- Iterable quest tracking
+- Performance traces (`sendPerformanceTraces`, `sendPreAuthPerformanceTraces`)
+
+### Opt-Out Controls
+- `telemetryOptOut` — policy flag (can be set by enterprise admin)
+- `telemetry_modal_opt_in` / `telemetry_modal_opt_out` — user choice
+- `essential_setup_telemetry_modal` — first-run consent modal
+- `essential_setup_telemetry_modal_remind_me_later` — defer choice
+- `Cj.isOptedInToTelemetry(user)` — per-user check before sending
+
+### Redaction
+- Fill telemetry errors explicitly use `"Fill telemetry error: <redacted>"` pattern
+- `TelemetryString` type with `assertSafeForTelemetry` — separate type system from `LoggableString` to enforce what can be sent in telemetry vs what can be logged locally
+- `LoggableString` type with `assertLogSafe` — controls what appears in local logs
+
+### High-Volume Event Gating
+- `telemetry-high-volume-events-denied` — mechanism to suppress high-frequency events
+
+## Sentry Error Reporting
+
+### Configuration
+- DSN: `4f3669ef553b434e86845ecd15a92e28:acaac9ba43f34f61917ae843f639604c@b5x-sentry.1passwordservices.com/4505427098533888`
+- Project ID: `4505427098533888`
+- Sample rate: 0.5 (when conditions met) or 1.0
+- App version format: `b5x@{version}-{channel}`
+
+### What's Reported
+- Uncaught exceptions and unhandled promise rejections (automatic)
+- Explicit `report-error` messages from any extension context
+- Stack traces with source file references (source maps available as web-accessible resources)
+- DOM breadcrumbs (console, DOM events, XHR, fetch, history changes)
+- Debug IDs embedded in every JS file for stack trace correlation
+
+### Sentry Debug IDs
+Every JS file starts with a Sentry debug ID registration:
+```javascript
+e._sentryDebugIds = e._sentryDebugIds || {};
+e._sentryDebugIds[stackTrace] = "unique-debug-id";
+```
+This enables source map correlation for crash reports without serving unobfuscated source.
+
+### Privacy Controls
+- `devtools-send-local-sentry` — developer toggle for local Sentry testing
+- `sendLocalSentryEnabled()` — gate check
+- URL sanitization via `rp(url, maxLength)` before sending
+- Exception value sanitization
+- Request URL sanitization
+- Standard Sentry filters: `Script error`, `ResizeObserver loop`, `googletag` excluded
+
+### Sentry Integrations Active
+From code analysis, these Sentry integrations are initialized:
+- Console breadcrumbs
+- DOM event breadcrumbs
+- XHR breadcrumbs
+- Fetch breadcrumbs
+- History breadcrumbs
+- Unhandled rejection capture
+- Global error handler
+
+## Logging Infrastructure
+
+### LogReporter Class
+Every extension context creates a `LogReporter` instance:
+- Generates a session ID (random base-36)
+- Records `performance.timeOrigin` for timing correlation
+- Captures context type (`ContentScript`, `Popup`, `ExtensionPage`)
+- Automatically captures uncaught exceptions and unhandled rejections
+
+### Log Event Structure
+```
+{
+ id: random_uint32_base36,
+ runtime: { context, initializer, session, origin },
+ message: [...],
+ timestamp: performance.now(),
+ fileName, lineNumber, severity, prefix, highlight
+}
+```
+
+### Log Routing
+| Context | Message Name | Destination |
+|---------|-------------|-------------|
+| Popup | `new-log-event` | Background |
+| ContentScript | `new-tab-log-event` | Background |
+| ExtensionPage | `new-tab-log-event` | Background |
+
+### DevTools Panel
+`devtools/panels.html` + `panels.js` (353KB) provides a "Logging" panel in browser DevTools for real-time log inspection. Created via `chrome.devtools.panels.create`.
+
+## DNS-over-HTTPS Privacy Proxy
+
+Watchtower breach-checking queries use DNS-over-HTTPS to check domain breach status. To prevent DNS providers from fingerprinting users:
+
+### Header Modification (rules_1.json)
+On requests to DoH providers:
+- **Removes** `User-Agent` — prevents browser/OS fingerprinting
+- **Removes** `Accept-Language` — prevents locale fingerprinting
+- **Sets** `Origin: null` — prevents referrer-based correlation
+
+### DoH Providers
+| Provider | Endpoint |
+|----------|----------|
+| Mullvad DNS | `dns.mullvad.net` |
+| Cloudflare DNS | `cloudflare-dns.com` |
+| CIRA Canadian Shield | `private.canadianshield.cira.ca` |
+| Quad9 | `9.9.9.10` |
+| joindns4.eu | `unfiltered.joindns4.eu` |
+
+Multiple providers likely used for redundancy and to avoid single-provider correlation.
+
+### pwnedpasswords.com
+Direct HTTPS API calls to `api.pwnedpasswords.com` for Have I Been Pwned password checking. Uses k-anonymity (prefix-based) API — only the first 5 hex characters of the SHA-1 hash are sent.
diff --git a/trust-boundaries.md b/trust-boundaries.md
new file mode 100644
index 0000000..2481460
--- /dev/null
+++ b/trust-boundaries.md
@@ -0,0 +1,169 @@
+# Trust Boundaries & Dataflow
+
+## Trust Zones
+
+### Zone A — Background (highest extension privilege)
+- Full access to all `chrome.*` APIs
+- Holds vault state, account sessions, crypto keys (in WASM memory)
+- Central policy decision point for all sensitive operations
+- Only context that communicates with native host and remote services
+
+### Zone B — Content Scripts (extension context in page)
+- Runs in isolated world alongside untrusted page DOM
+- Can read/modify page DOM but shares no JS state with page
+- Communicates with background via `chrome.runtime.sendMessage`
+- Communicates with page world via `window.postMessage` (WebAuthn only)
+
+### Zone C — Page World (untrusted)
+- `webauthn-listeners.js` runs here — only extension code in this zone
+- Fully hostile environment — page JS can observe, intercept, modify anything
+- Communication with extension only via `window.postMessage` protocol
+
+### Zone D — Native Host (desktop app)
+- Connected via `nativeMessaging` permission
+- `chrome.runtime.sendNativeMessage` (2 occurrences in background.js)
+- Native app connection initialized during background startup (`initializeNativeAppConnection`)
+- Trust level: equivalent to or higher than extension (system-level process)
+
+### Zone E — Remote Services (1Password cloud)
+- Extensive `connect-src` allowlist (see below)
+- WebSocket connections for real-time sync (`wss://b5n.*`)
+- REST APIs for account management, vault operations
+
+### Zone F — WASM Modules (crypto boundary)
+- 7 WASM modules loaded with `wasm-unsafe-eval`
+- Crypto operations are isolated inside WASM linear memory
+- JS ↔ WASM boundary is a trust boundary: JS passes data in, WASM performs crypto, returns results
+- Key material should ideally never exist in JS heap (only WASM memory)
+
+### Zone G — External Extensions
+- `chrome.runtime.onMessageExternal` (3 references)
+- Other extensions can send messages to 1Password
+- No `externally_connectable` in manifest — on Firefox, any extension can message
+
+### Zone H — Inline UI Frames
+- `inline/menu/menu.html`, `inline/modal/modal.html`, etc.
+- Web-accessible resources loaded in iframes within web pages
+- Have extension origin but are embedded in untrusted page context
+- Communicate with background via `chrome.runtime.sendMessage`
+
+## Boundary Crossings
+
+### B0: Page DOM → Content Script
+- Content script reads form fields, detects login pages, captures credential data for save
+- All DOM-derived data is untrusted input
+- Heuristics module classifies pages/forms
+
+### B1: Content Script → Background
+- `chrome.runtime.sendMessage` with named message handlers
+- ~50 registered handlers in background's `m5({...})` router
+- **Critical question: does background validate message schemas?** (needs deep reverse engineering)
+
+### B2: Page World → Content Script (via postMessage)
+- WebAuthn IPC protocol (SYN/ACK/Direct)
+- Custom message validation (type/source/name/msgId checks)
+- `stopImmediatePropagation` on matched messages
+- **Risk: any page script can craft these messages**
+
+### B3: Background → Native Host
+- `chrome.runtime.sendNativeMessage` — structured JSON messages
+- Mycelium protocol (WASM) may also use this channel for relay
+- **Needs analysis: what commands can be sent? What data flows back?**
+
+### B4: Background → Remote Services
+- HTTPS REST + WebSocket to 1Password infrastructure
+- Snowplow telemetry to analytics endpoints
+- Sentry error reporting
+- DNS-over-HTTPS for Watchtower breach checking
+- Partner APIs: Privacy.com, Fastmail, Brex, Kolide/Trelica
+
+### B5: Background → Inline UI Frames
+- `chrome.tabs.sendMessage` to push state to inline menus/modals
+- **Risk: `relay-message-to-frames` and `targeted-message-to-inline-menu`** — background acts as a message relay between frames. If a compromised frame can influence the relay target, it could potentially spoof messages to other frames.
+
+### B6: JS → WASM
+- Core crypto, trust log, trust verification, HPKE, mycelium all in WASM
+- JS serializes data, calls WASM exports, receives results
+- **Risk: incorrect serialization could leak or corrupt key material at the boundary**
+
+## Sensitive Data Classes
+
+| Class | Where Created | Where Used | Notes |
+|-------|--------------|------------|-------|
+| Master password / derived key | User input → WASM | WASM memory only (ideally) | PBKDF2/HKDF derivation |
+| SRP verifier/session | WASM | Background ↔ Remote | Authentication protocol |
+| Vault encryption keys | WASM (decrypted from server) | WASM memory | AES-GCM, AES-CBC |
+| Item secrets (passwords, OTPs) | WASM (decrypted) | Passed to content script for fill | **Transits JS heap** |
+| Passkey private keys | WASM | WASM → background → content → page world | **Full chain traversal** |
+| Credit card numbers | WASM (decrypted) | Content script fill | |
+| Session tokens | Background | Background ↔ Remote | |
+| Device keys | Background/Native | Background | |
+| Save objects (captured credentials) | Content script | Background → WASM → Remote | Encrypted with public key before transit |
+| Telemetry data | All contexts | Background → Snowplow/Sentry | URLs, form hints, error stacks |
+| Feature flags | Remote → Background | All contexts | Controls behavior |
+
+## Network Endpoints (from CSP connect-src)
+
+### 1Password Infrastructure
+- `*.1password.com`, `*.1password.ca`, `*.1password.eu` (production)
+- `wss://b5n.1password.com/ca/eu` (WebSocket notifications)
+- `wss://b5n.ent.1password.com` (enterprise WebSocket)
+- `*.b5dev.*`, `*.b5test.*`, `*.b5local.*`, `*.b5staging.*`, `*.b5rev.*` (dev/test/staging)
+- `*.agilebits.com`
+- `f.1passwordusercontent.com/ca/eu`, `a.1passwordusercontent.com/ca/eu` (file/asset CDN)
+
+### Telemetry
+- `com-1password-prod1.mini.snplow.net/com.snowplowanalytics.snowplow/tp2` (Snowplow)
+- `telemetry.1passwordservices.com/com.snowplowanalytics.snowplow/tp2` (Snowplow alt)
+- `b5x-sentry.1passwordservices.com` (Sentry)
+
+### Partner Services
+- `api.privacy.com`, `sandbox.privacy.com` (Privacy.com virtual cards)
+- `www.fastmail.com`, `jmap.fastmail.com`, `betajmap.fastmail.com`, `api.fastmail.com` (email alias)
+- `accounts.brex.com`, `platform.brexapis.com`, `*.staging.brexapps.com` (Brex cards)
+- `app.kolide.com/ca/eu`, `api.kolide.com/ca/eu`, `auth.kolide.com/ca/eu` (Kolide EPM)
+- `*.trelica.com` (Trelica app management)
+
+### DNS-over-HTTPS (for Watchtower)
+- `cloudflare-dns.com/dns-query`
+- `private.canadianshield.cira.ca/dns-query`
+- `9.9.9.10/dns-query` (Quad9)
+- `unfiltered.joindns4.eu/dns-query`
+
+### Local / Native
+- `http://127.0.0.1:12519`, `:40978`, `:52115`, `:22287`, `:60685`, `:22322` (6 localhost ports)
+- These are likely the native helper broker / desktop app bridge endpoints
+
+### Other
+- `api.pwnedpasswords.com` (Have I Been Pwned API for Watchtower)
+- `cache.agilebits.com` (icon/asset cache)
+
+## Declarative Net Request Rules
+
+`rules_1.json` defines one rule that modifies headers on DNS-over-HTTPS requests:
+- **Removes** `User-Agent` header
+- **Removes** `Accept-Language` header
+- **Sets** `Origin` to `"null"`
+- Applies to: Mullvad DNS, Cloudflare DNS, CIRA Canadian Shield, Quad9, joindns4.eu
+- Resource type: `xmlhttprequest` only
+
+This is a privacy-enhancing measure to prevent DNS providers from fingerprinting 1Password users when checking breached domains via Watchtower.
+
+## Attack Surface Summary
+
+### High Priority
+1. **WebAuthn page-world IPC** — unauthenticated postMessage protocol, any page can participate
+2. **Frame relay** (`relay-message-to-frames`) — background forwards messages between frames without clear origin validation (needs verification)
+3. **Save object pipeline** — content script captures and transmits credentials (encrypted with public key)
+4. **829 named messages** — massive handler surface in background, schema validation unknown
+
+### Medium Priority
+5. **External extension messaging** — `onMessageExternal` accepts messages from any Firefox extension
+6. **6 localhost ports** — native bridge endpoints, protocol unknown
+7. **Web-accessible resources** — inline UI HTML files can be loaded by any page (fingerprinting, UI redressing)
+8. **WASM ↔ JS boundary** — key material handling across the boundary
+
+### Lower Priority
+9. **Telemetry data classification** — what exactly is sent to Snowplow/Sentry
+10. **Partner integration data minimization** — what's shared with Privacy.com, Fastmail, Brex, Kolide, Trelica
+11. **`<all_urls>` + `webRequestBlocking`** — can observe/modify all web traffic
diff --git a/webauthn-analysis.md b/webauthn-analysis.md
new file mode 100644
index 0000000..bb256af
--- /dev/null
+++ b/webauthn-analysis.md
@@ -0,0 +1,160 @@
+# WebAuthn / Passkey Mediation Analysis
+
+The extension intercepts all WebAuthn (passkey) operations on every HTTPS page. This is the most security-sensitive component because it runs code in the **page world** (not the extension's isolated world).
+
+## Injection Chain
+
+```
+manifest.json
+ └─ content_scripts[1]: webauthn.js + webauthn-listeners.js
+ matches: https://*/* and http://localhost/*
+ run_at: document_start
+ all_frames: true
+
+webauthn.js (content script, ISOLATED world)
+ └─ Contains full inline menu / frame manager code (93KB)
+ └─ Communicates with background via chrome.runtime.sendMessage
+
+webauthn-injection-helper.js (bundled in webauthn.js)
+ └─ Creates a <script> tag pointing to webauthn-listeners.js
+ └─ Injects into document.documentElement at document_start
+ └─ Removes the <script> element after injection
+ └─ Guards: skips injection on Dropbox download pages
+
+webauthn-listeners.js (runs in PAGE WORLD — untrusted)
+ └─ Monkey-patches navigator.credentials.create/get
+ └─ Monkey-patches PublicKeyCredential static methods
+ └─ Communicates via window.postMessage IPC protocol
+```
+
+## The Page-World Monkey-Patch
+
+`webauthn-listeners.js` replaces these browser APIs globally:
+
+| Original API | Replacement | Behavior |
+|-------------|-------------|----------|
+| `navigator.credentials.create()` | `N()` | Routes through extension if intercept enabled, else falls back to browser |
+| `navigator.credentials.get()` | `H()` | Routes through extension if intercept enabled, else falls back to browser |
+| `PublicKeyCredential.isConditionalMediationAvailable()` | `W()` | Returns `true` if extension intercepts |
+| `PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()` | `K()` | Returns `true` if extension intercepts |
+| `PublicKeyCredential.getClientCapabilities()` | `U()` | Returns hardcoded capability set if extension intercepts |
+
+### Firefox-Specific Injection
+
+Uses Firefox's privileged APIs when available:
+- `exportFunction()` — safely export JS functions across compartments (Xray wrappers)
+- `cloneInto()` — clone objects across compartments with `{cloneFunctions: true, wrapReflectors: true}`
+- `window.wrappedJSObject` — access the page's actual JS global
+
+If these aren't available (shouldn't happen on Firefox), falls back to direct property assignment.
+
+### Re-Patching Loop
+
+```javascript
+setInterval(Y, 100) // every 100ms
+```
+
+Function `Y()` checks if `navigator.credentials.create.length > 0` and `navigator.credentials.get.length > 0`. If not (indicating the patch was overwritten), and the same check fails on `wrappedJSObject`, it re-applies the patches. This is defensive against other extensions or page scripts that might also try to hook WebAuthn.
+
+## Custom IPC Protocol (window.postMessage)
+
+Since `webauthn-listeners.js` runs in the page world, it cannot use `chrome.runtime.sendMessage`. Instead, it uses a custom protocol over `window.postMessage`:
+
+### Message Types
+| Type | Direction | Purpose |
+|------|-----------|---------|
+| `op-window-syn` | Page → Extension | Initial handshake request |
+| `op-window-syn-ack` | Extension → Page | Handshake acknowledgment |
+| `op-window-direct-request` | Page → Extension | Actual request payload |
+| `op-window-direct-response` | Extension → Page | Response payload |
+| `op-window-abort` | Page → Extension | Cancel in-progress request |
+
+### Protocol Flow
+1. Page-world code generates a random `source` ID and `msgId`
+2. Sends `op-window-syn` with a route name (e.g., `"get-credential"`)
+3. Extension content script (in isolated world) receives via `window.addEventListener("message")`
+4. If it has a handler for that route, responds with `op-window-syn-ack` including its own `source` ID
+5. Page sends `op-window-direct-request` targeted to the ack'd source
+6. Extension processes request (via `chrome.runtime.sendMessage` to background)
+7. Extension sends `op-window-direct-response` back
+
+### Message Validation
+Each message must have:
+- `msgId` (non-empty string)
+- `source` (non-empty string)
+- `name` (non-empty string)
+- `type` (one of the 5 valid types)
+
+Messages are ignored if `source` matches self (prevents echo). Targeted messages (`destination`) are only processed if they match the receiver's source ID.
+
+### Registered Routes
+
+Page world → Extension:
+- `get-credential` — passkey authentication
+- `create-credential` — passkey registration
+- `should-intercept-webauthn` — check if extension should handle WebAuthn
+
+Extension → Page world:
+- `update-settings` — push `authenticatePasskeys` boolean to page
+
+## Intercept Decision Flow
+
+```
+Page calls navigator.credentials.get(options)
+ → f() checks: should we intercept?
+ → If R (authenticatePasskeys) is cached, use it
+ → If not cached, send "should-intercept-webauthn" to extension
+ → If response is true: route through extension (V/L functions)
+ → If response is false/error: call original browser API
+```
+
+## Credential Serialization
+
+WebAuthn credentials are serialized across the postMessage boundary:
+- `ArrayBuffer` / typed arrays → regular `Array` (via `O()` function)
+- Binary fields Base64URL-encoded for JSON transport
+- `AuthenticatorAssertionResponse` and `AuthenticatorAttestationResponse` are reconstructed with proper prototypes on return
+
+Response reconstruction explicitly sets `Object.setPrototypeOf(result, PublicKeyCredential.prototype)` so `instanceof` checks pass on the page side.
+
+## Error Handling and Fallbacks
+
+| Scenario | Behavior |
+|----------|----------|
+| Extension doesn't respond to SYN within 1000ms | Falls back to browser WebAuthn |
+| Extension returns handler error | Falls back to browser WebAuthn |
+| Extension returns `"timeout"` | Throws `NotAllowedError` DOMException |
+| Extension returns `"user-cancelled"` (conditional mediation) | Returns never-resolving promise |
+| Extension returns `"user-cancelled"` (non-conditional) | Returns `null` |
+| Extension returns `"fallback-requested"` (conditional) | Calls browser API without `mediation` option |
+| Extension returns `"fallback-requested"` (non-conditional) | Falls back to browser WebAuthn |
+| Extension returns `"duplicate"` (create) | Throws `InvalidStateError` DOMException |
+| AbortSignal fires | Sends `op-window-abort`, throws `AbortError` DOMException |
+
+## Dropbox Exclusion
+
+The injection helper explicitly skips WebAuthn patching on Dropbox download pages:
+```javascript
+window.location.hostname.includes("dropbox") &&
+ (window.location.pathname.includes("get") ||
+ window.location.search.includes("download_id"))
+```
+This suggests a known compatibility issue with Dropbox's download flow.
+
+## Security Considerations
+
+### Attack Surface
+1. **Any page can send `op-window-syn` messages** — the protocol is unauthenticated at the transport layer. Route name validation and the SYN/ACK handshake provide some protection, but a malicious page could race legitimate requests.
+
+2. **The `source` IDs are random 32-bit integers** (base-36 encoded). With ~4 billion possible values, a brute-force collision is impractical per-request, but the entropy is modest.
+
+3. **Re-patching via `setInterval`** means a page that continuously overwrites `navigator.credentials` creates a race condition window between overwrites and re-patches.
+
+4. **`stopImmediatePropagation`** is called on matched messages, preventing other listeners from seeing them. But a page could register its listener before the extension's (it runs at `document_start`, but timing isn't guaranteed in all edge cases).
+
+5. **Conditional mediation get requests** have their `timeout` stripped (`publicKey.timeout = undefined`), which means they can hang indefinitely until the extension responds.
+
+6. **The `signal` property is stripped** from options before sending to the extension. The extension's abort handling is through its own `op-window-abort` message type.
+
+### Hardcoded Capability Advertisement
+When the extension intercepts, `getClientCapabilities()` returns a hardcoded set claiming support for `conditionalGet`, `hybridTransport`, `passkeyPlatformAuthenticator`, `prf` extension, etc. — regardless of actual platform capabilities.