diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.md | 21 | ||||
| -rw-r--r-- | TODO.md | 120 | ||||
| -rw-r--r-- | architecture.md | 128 | ||||
| -rw-r--r-- | message-catalog.md | 152 | ||||
| -rw-r--r-- | telemetry.md | 146 | ||||
| -rw-r--r-- | trust-boundaries.md | 169 | ||||
| -rw-r--r-- | webauthn-analysis.md | 160 |
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 @@ -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. |
