summaryrefslogtreecommitdiff
path: root/trust-boundaries.md
blob: c7d9884a6c342edb8d7b6e83252f68dec50f7cfc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# 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 both JS heap and WASM memory**
- Master Unlock Key (MUK) stored as exportable JWK on account handler objects in JS
- SRP-X cached in JS on client context objects
- Decrypted item secrets (passwords, OTPs, card numbers) transit through JS heap during fill operations
- 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
- `browser.runtime.sendNativeMessage("")` — empty string as native app ID (Firefox uses the manifest `applications.gecko.id` for routing)
- Native app connection initialized during background startup (`initializeNativeAppConnection`)
- **Confirmed operations via native messaging:**
  - Biometric unlock: save/retrieve/remove MUK + SRP-X from OS secure enclave
  - Biometry availability check
  - dSecret proxy for MFA bypass on trusted devices
  - Device trust public key signing
  - Delegated session management (upgrade from offline state)
  - Desktop connection manager state
- Messages use JSON envelope: `{name: "core", data: JSON.stringify({type: "Biometry", data: {...}})}`
- Trust level: higher than extension — has access to OS keychain/secure enclave

### 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 (portability layer, NOT a security boundary)
- 7 WASM modules loaded with `wasm-unsafe-eval`, compiled from Rust via `wasm-bindgen`
- Provides the same Rust core used by desktop/mobile/CLI clients — portability, not isolation
- **Key material DOES cross the WASM→JS boundary**: MUK exported as JWK, SRP-X cached in JS, decrypted passwords returned to JS for fill
- JS can read WASM linear memory — no privilege separation exists
- WASM's value is **correctness** (battle-tested Rust crypto) not **isolation**
- Only 4 `crypto.subtle.*` calls in background.js; WASM handles virtually all crypto

### 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
- `browser.runtime.sendNativeMessage("")` — JSON envelope with `{name: "core", data: ...}`
- **Confirmed message types:**
  - `{type: "Biometry", data: {type: "save", data: {secrets: [{accountUuid, userUuid, muk: {kty, kid, alg, k, ...}, srpX}]}}}` — **sends MUK (the master unlock key) and SRP-X to native app for biometric storage**
  - `{type: "Biometry", data: {type: "unlock", data: {accounts, useBiometry, useAppleWatch, ...}}}` — retrieves MUK + SRP-X from secure enclave
  - `{type: "Biometry", data: {type: "remove", data: {accounts, ...}}}` — removes stored secrets
  - `{type: "Biometry", data: {type: "biometryAvailability"}}` — checks if Touch ID / biometry is available
- 10-second timeout on all native messages
- dSecret proxy also flows through native messaging for MFA bypass
- **Risk: the MUK (symmetric key that unlocks everything) is serialized as JWK and sent over the native messaging channel**

### 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 (NOT a privilege boundary)
- Core crypto, trust log, trust verification, HPKE, mycelium all in WASM
- JS calls `rA.*` methods (80+ confirmed) which route into `op_wasm_b5x_bg`
- **Key material flows freely across this boundary:**
  - MUK exported from account handler via `.exportJwk()` → full JWK in JS heap
  - SRP-X cached on `CTX.session.auth.srpX` in JS
  - Decrypted passwords/OTPs returned from `rA.fillItem` / `rA.fieldValueByIdentifier` to JS
  - Save objects constructed via `rA.createSaveObject` — encrypted in WASM, ciphertext returned to JS
- WASM linear memory is directly accessible from same-origin JS
- **This is NOT a security boundary** — it's a code-sharing mechanism. A compromised background page can extract all key material.

## Sensitive Data Classes

| Class | Where Created | Where Stored | Where Transits | Notes |
|-------|--------------|-------------|----------------|-------|
| Master password | User input | **Timeboxed JS reference (5 min)**, then cleared | JS → WASM for key derivation, also set on `CTX.user.password` temporarily during sign-in then set to `undefined` | `setTimeout` clears after 5 min (`gkA = 5 * 60 * 1000`) |
| Master Unlock Key (MUK) | WASM (PBKDF2/HKDF from password + secret key) | **JS heap** as exportable JWK on account handler (`accountHandler.masterKey`) | JS → native messaging (biometry save), JS → WASM (re-auth) | **The crown jewel.** Can be exported via `.exportJwk()`. Sent to native app for biometric storage. |
| Secret Key (A3-XXXXX-...) | User input / stored in DB | Account handler, database | JS → WASM for SRP, exportable via `.exportSensitiveReadableString()` | Combined with password for key derivation |
| SRP-X | WASM (derived from MUK + Secret Key) | **JS heap** cached on `CTX.session.auth.srpX` | JS → native messaging (biometry save), JS → WASM (re-auth) | Used for re-authentication without master password |
| dSecret | Server / device | Account handler, native app | JS ↔ native messaging, JS → server for MFA | Device secret for MFA bypass on trusted devices |
| Session context (CTX) | WASM (`SA.initialize`) | **JS heap** on client object (`_dangerousInnerCTX`) | JS ↔ WASM, exportable via `SA.getInitializationExport()` | Contains session keys, account state. Note the `_dangerous` prefix — they know. |
| Vault/item encryption keys | WASM (decrypted from server keysets) | WASM memory (likely) | WASM internal | AES-GCM, AES-CBC. Exposed via `rA.fillItem` etc. |
| Item secrets (passwords, OTPs) | WASM (decrypted) | **JS heap** during fill | Background JS → `chrome.tabs.sendMessage` → content script → DOM | Plaintext in JS for the duration of fill |
| Passkey assertions | WASM | **JS heap** | Background → content script → page world postMessage | **Full chain traversal** through all trust zones |
| Credit card numbers | WASM (decrypted) | **JS heap** during fill | Same as item secrets | |
| Save objects | Content script (DOM capture) | Background | Content script → background → `rA.createSaveObject` (encrypted in WASM) → server | Encrypted with public key before storage |
| Biometry secrets bundle | JS (assembled from MUK + SRP-X) | Native app secure enclave | JS → `browser.runtime.sendNativeMessage` → OS keychain | `{muk: {kty, kid, alg, k, ext, key_ops}, srpX}` per account |
| Telemetry data | All contexts | Background (batched) | Background → Snowplow/Sentry | URLs, form hints, error stacks, account metadata |
| Feature flags | Remote server (Unleash) | Background (cached) | Background → all contexts | Controls security-relevant behavior |

See [key-hierarchy.md](key-hierarchy.md) for the full key derivation model.

## 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)
- Likely redundant ports for the native helper broker / desktop app bridge (multiple ports for reliability across OS configurations)
- Native messaging also used via `browser.runtime.sendNativeMessage("")` for biometry, dSecret proxy, device trust

### 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

### Critical
1. **MUK in JS heap** — the Master Unlock Key is stored as an exportable JWK in JS memory and sent over native messaging. A background page compromise (malicious update, browser bug, XSS in extension pages) exposes the key that decrypts everything.
2. **MUK + SRP-X sent to native app** — biometry save transmits `{muk: {k: "base64url_symmetric_key"}, srpX}` over native messaging JSON. If the native messaging channel is compromised, full account takeover is possible.
3. **Master password timeboxing only** — the password reference is cleared after 5 minutes via `setTimeout`, but the MUK derived from it persists in JS for the entire unlocked session. No explicit memory zeroing (JS GC handles it, which is non-deterministic).

### High Priority
4. **WebAuthn page-world IPC** — unauthenticated postMessage protocol, any page can participate
5. **Frame relay** (`relay-message-to-frames`) — background forwards messages between frames without clear origin validation (needs verification)
6. **Save object pipeline** — content script captures and transmits credentials (encrypted with public key)
7. **829 named messages** — massive handler surface in background, schema validation unknown
8. **`_dangerousInnerCTX`** — the session context object (containing session keys and auth state) is explicitly named "dangerous" by the developers, suggesting they recognize the risk of it being in JS

### Medium Priority
9. **External extension messaging** — `onMessageExternal` accepts messages from any Firefox extension
10. **6 localhost ports** — native bridge endpoints, protocol details unclear beyond biometry
11. **Web-accessible resources** — inline UI HTML files can be loaded by any page (fingerprinting, UI redressing)
12. **Re-authentication with cached MUK** — when a session expires (401), the extension re-authenticates using the cached MUK and SRP-X without user interaction (`executeWithReauth`). This means a stolen MUK enables silent re-auth.
13. **Delegated sessions** — background can request delegated sessions for re-auth, transferring session state between contexts

### Lower Priority
14. **Telemetry data classification** — what exactly is sent to Snowplow/Sentry
15. **Partner integration data minimization** — what's shared with Privacy.com, Fastmail, Brex, Kolide, Trelica
16. **`<all_urls>` + `webRequestBlocking`** — can observe/modify all web traffic
17. **Duo MFA tab injection** — extension opens a Duo MFA tab via `chrome.tabs.create`, monitors its URL for `duo_code` parameter, then closes it. The URL monitoring is done via `chrome.webRequest.onHeadersReceived`.