summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2026-02-26 09:13:23 +0100
committerYuval Adam <_@yuv.al>2026-02-26 09:13:23 +0100
commit7b609fffddf3ae138cdf301c97bad805fc508616 (patch)
treee952de5263e507e52b74db48cac8ac93f36fa8be
parent54adf11e1c8905c97512fcf29d8b3d75aa9eb0cb (diff)
Add key hierarchy analysis, update all docs with WASM/auth/native messaging findingsHEADmain
New document: - key-hierarchy.md: Full key derivation model, MUK lifecycle, SRP auth, biometric unlock, Duo MFA, dSecret bypass, delegated sessions, password timebox mechanism, crypto algorithm inventory Major updates: - architecture.md: Expanded WASM section with confirmed 80+ rA.* methods, clarified WASM is portability layer not security boundary - trust-boundaries.md: Corrected Zone A (keys in JS heap not just WASM), Zone D (confirmed native messaging protocol with biometry messages), Zone F (WASM is NOT a privilege boundary), detailed sensitive data table with confirmed storage locations, new Critical attack surface category - message-catalog.md: Added native messaging protocol (biometry save/unlock/ remove, availability check), desktop connection messages, server notification events - TODO.md: Marked completed items, added key material exposure assessment section, authentication & session security section
-rw-r--r--README.md5
-rw-r--r--TODO.md46
-rw-r--r--architecture.md56
-rw-r--r--key-hierarchy.md201
-rw-r--r--message-catalog.md31
-rw-r--r--trust-boundaries.md117
6 files changed, 394 insertions, 62 deletions
diff --git a/README.md b/README.md
index 8ad8c35..d45428d 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,9 @@ Static analysis of the 1Password browser extension for Firefox (`1pass-xpi`), ve
| 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 |
+| [architecture.md](architecture.md) | Runtime topology, boot pipeline, permission model, WASM core interface |
+| [key-hierarchy.md](key-hierarchy.md) | Key derivation, MUK lifecycle, auth flows, biometry, Duo MFA, crypto algorithms |
+| [message-catalog.md](message-catalog.md) | Complete message handler map extracted from source, including native messaging protocol |
| [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 |
diff --git a/TODO.md b/TODO.md
index 751f9ad..7d4eec6 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,6 +2,18 @@
## High Priority
+### Key Material Exposure Assessment
+- [x] Map where MUK lives — **JS heap as exportable JWK, entire unlocked session. Documented in key-hierarchy.md.**
+- [x] Map where SRP-X lives — **JS heap on CTX.session.auth.srpX. Documented in key-hierarchy.md.**
+- [x] Trace biometry save flow — **MUK + SRP-X serialized as JSON, sent via native messaging. Documented in key-hierarchy.md.**
+- [x] Trace re-auth flow — **MUK + Secret Key + SRP-X used for silent re-auth on 401. No user interaction. Documented in key-hierarchy.md.**
+- [x] Trace master password lifecycle — **5-min timebox in closure, set on CTX.user.password temporarily during sign-in then cleared. Documented in key-hierarchy.md.**
+- [ ] Determine if the `Rv` (client) class ever explicitly clears the MUK reference on lock (beyond `_dangerousInnerCTX = undefined`)
+- [ ] Check if `rA.onLock` triggers WASM-side key zeroing
+- [ ] Trace what happens to content-script-side decrypted values after fill completes — are they GC'd promptly?
+- [ ] Check if the 5-minute password timebox can be extended by repeated Duo attempts
+- [ ] Determine if `lessSafeOpenNaCl` has any key exposure implications
+
### 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
@@ -10,11 +22,12 @@
- [ ] 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
+- [x] Identify the native app ID used with `chrome.runtime.sendNativeMessage` — **empty string `""`**, Firefox routes via `applications.gecko.id`
+- [x] Map the command/response schema for native messaging — **JSON envelope `{name: "core", data: JSON.stringify({type, data})}`, Biometry messages documented in message-catalog.md**
+- [x] Determine what operations are delegated to the native app vs handled in-extension — **Biometry (save/unlock/remove MUK+SRP-X), biometry availability, dSecret proxy, device trust signing, desktop connection state**
+- [ ] Analyze the 6 localhost ports (12519, 40978, 52115, 22287, 60685, 22322) — what protocol, what data. Are these the same native bridge or separate services?
- [ ] Check if localhost connections use any authentication/signing
+- [ ] Determine if the native messaging JSON carries any MAC/signature on the MUK payload
### Frame Relay Security
- [ ] Trace `relay-message-to-frames` handler (`eij`) — does it validate sender frame? Does it restrict what messages can be relayed?
@@ -36,11 +49,13 @@
## 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
+- [x] Analyze exported functions from `op_wasm_b5x_bg` — **80+ methods documented in architecture.md `rA.*` interface table**
+- [x] Determine if key material ever crosses the WASM→JS boundary in plaintext — **YES. MUK exported as JWK, SRP-X cached in JS, decrypted passwords returned to JS for fill. Documented in key-hierarchy.md.**
- [ ] 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?
+- [x] Analyze `b5_mycelium_bg` — **confirmed as P2P relay for remote autofill. Noise-protocol-like handshake with `wasmpairingsession*` and `wasmsetuptransportsession*` exports.**
+- [ ] Map HPKE usage — what is encrypted with HPKE vs AES-GCM vs RSA-OAEP? (`wasmsealed_*` exports use HPKE)
+- [ ] Analyze the `b5_trustlog_bg` trust log entries — what operations are logged? (`wasmtrustlogclient_create_add_users_entry`, `_remove_users_entry`, `_add_groups_entry`, etc.)
+- [ ] Determine how vault keys are managed inside WASM — are they ever exposed to JS or only used internally?
### External Extension Messaging
- [ ] Trace all 3 `onMessageExternal` handler registrations
@@ -55,10 +70,11 @@
- [ ] 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
+- [x] Trace the fill flow: **background calls `rA.fillItem`/`rA.startFillSession` → WASM decrypts → plaintext returned to JS → `chrome.tabs.sendMessage` → content script → DOM injection**
+- [x] Identify where decrypted password/OTP/card values exist in JS memory — **JS heap in background (from WASM return), JS heap in content script (from sendMessage), DOM (after fill). No explicit zeroing.**
- [ ] Check if clipboard operations (`clipboardWrite`) properly clear after timeout
- [ ] Analyze `fill-generated-password` flow — is the generated password ever in plaintext outside WASM?
+- [ ] Determine how long decrypted values remain reachable in JS (fill session lifecycle)
### Partner Integration Data Minimization
- [ ] Privacy.com: what data is sent to `api.privacy.com`? Card params, user identity?
@@ -67,6 +83,16 @@
- [ ] Kolide: what health/device data is shared? Analyze `kolide.js` (60KB)
- [ ] Trelica: what app catalog data is exchanged?
+### Authentication & Session Security
+- [ ] Trace the full SRP exchange — what SRP method/group is used? (SrpMethod, SrpMethodPrefix constants)
+- [ ] Determine if transport tokens provide replay protection
+- [ ] Analyze the delegated session mechanism — can a compromised context delegate to others?
+- [ ] Check if `safeSignOutClient` properly clears all key material (it sets `_srpX = undefined`, `_dangerousInnerCTX = undefined`)
+- [ ] Trace the `invalidate` flow — does invalidation zero keys or just remove references?
+- [ ] Analyze the offline MFA account tracking (`offline-mfa-accounts` in localStorage) — security of this state
+- [ ] Check if the Duo code extraction from URL (`duo_code` parameter) is vulnerable to URL injection
+- [ ] Determine if `CTX.getTransportToken()` exposes session-equivalent tokens
+
### 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
diff --git a/architecture.md b/architecture.md
index 986be62..fe3a5e9 100644
--- a/architecture.md
+++ b/architecture.md
@@ -55,20 +55,56 @@ Semantic chunk name patterns observed: `account-family`, `account-team`, `develo
## WASM Modules
-Seven WebAssembly modules in `assets/wasm/` (total ~30MB):
+Seven WebAssembly modules in `assets/wasm/` (total ~30MB), all compiled from Rust via `wasm-bindgen`:
-| 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) |
+| Module | Size | Purpose (confirmed) |
+|--------|------|---------------------|
+| `op_wasm_b5x_bg` | 15.6MB | **Core client** — vault encrypt/decrypt, SRP auth, key derivation, item management, page analysis, password generation, TOTP, Watchtower, SSO. This is the same Rust core used by desktop/mobile apps. |
+| `op_wasm_xam_bg` | 11.2MB | XAM (cross-app management) — device trust, endpoint management, Kolide/Trelica integration backend |
+| `confidential_computing_bg` | 1.8MB | Confidential computing attestation primitives |
+| `b5_trustlog_bg` | 1.3MB | Cryptographic audit log — signs/verifies trust log entries for group/user/domain changes (`wasmtrustlogclient_*` exports) |
+| `b5_trust-verifier_bg` | 1.0MB | Trust chain verification for account/device authorization |
+| `b5_mycelium_bg` | 319KB | P2P relay protocol for remote autofill (director.ai) — Noise-protocol-like handshake (`wasmpairingsession*`, `wasmsetuptransportsession*` exports) |
+| `b5_hpke_bg` | 82KB | Hybrid Public Key Encryption (RFC 9180) — used for item sharing, secure transport, sealed encryption (`wasmsealed_*` exports) |
The background.js initialization calls `rA.init(e)` ("initializeCoreInterface") which loads the main WASM module. CSP allows `wasm-unsafe-eval` for this purpose.
+### WASM Core Interface (`rA.*`)
+
+The JS-facing core interface routes nearly all sensitive operations through WASM. Confirmed methods:
+
+| Category | Methods |
+|----------|---------|
+| **Fill/Autofill** | `fill`, `fillItem`, `startFillSession`, `fillSessionEvent`, `fillSessionStatus`, `nextFill`, `clearFillSession`, `fieldValueByIdentifier` |
+| **Save** | `createSaveObject`, `createItemFromSaveRequest`, `mergeSaveObjectWithItem`, `saveManagerCreate`, `saveManagerConfigure`, `saveManagerAction`, `saveManagerStatus`, `saveManagerMatchingItems`, `autoSaveMatchingItems`, `saveUrlsFromSaveRequest` |
+| **Password/Key Generation** | `generatePassword`, `generateSuggestedPassword`, `passwordGeneratorViewModel`, `createSshKeyItem` |
+| **TOTP** | `generateOneTimePasswordFromUrl`, `refreshTotp` |
+| **WebAuthn** | `webAuthnLogin`, `webAuthnRegister`, `webAuthnRPValidate` |
+| **Page Analysis** | `analyzePage`, `autosubmitDetectElements`, `inferBestTitle` |
+| **Email Alias** | `generateEmailAlias`, `enableEmailAliasSession`, `startEmailAliasSession`, `endEmailAliasSession`, `getEmailAliasSessionStatus`, `getEmailAliasAccountName`, `getEmailAliasState`, `updateEmailAlias` |
+| **Privacy/Brex** | `createPrivacyCard`, `validatePrivacyCardParams`, `getPrivacyFundingAccounts`, `createBrexVendorCardForUser`, `getAllCardsForBrexUser`, `getCurrentBrexUser` |
+| **Watchtower** | `compareWatchTowerDiff`, `handleWatchtowerAction`, `newCompromisedWebsiteItems` |
+| **Auth/SSO** | `signInWithEvent`, `signInWithProviderConfig`, `signInWithUrlToProvider`, `enrollTrustedDevice` |
+| **Item Management** | `getItemDetails`, `editItemFilter`, `getLargeTypeWithClientFormattedString`, `getLargeTypeWithFieldIdentifier`, `itemToReference` |
+| **URL/Domain** | `nakedDomainForUrl`, `nakedDomainForUrls`, `getRichIconForUrl`, `generateRichIconBackgroundColor`, `getAppleChangePasswordUrl`, `serviceIntegrationForUrl` |
+| **Account/UI** | `getAccountIcon`, `getVaultIcon`, `getEffectivePolicies`, `parseAccountType`, `parseVaultType`, `concealCreditCardNumber`, `getCreditCardType`, `generateSharableItemLink`, `parseMarkdown`, `setLocale`, `getSignInUrl`, `getSignInUrlVersion`, `getOauthAccessToken` |
+| **Lifecycle** | `init`, `ensureWasmIsInitialized`, `onLock` |
+| **Crypto** | `buildPermissionBundle`, `lessSafeOpenNaCl`, `unOpaqueFilePosition` |
+
+### Architectural Role of WASM
+
+**WASM is a portability layer, not a security boundary.** Key findings:
+
+1. **The Rust core is shared across all 1Password clients** (desktop, mobile, browser, CLI). WASM is how it runs in the browser. This ensures crypto correctness — one Rust implementation vs. hand-rolled JS.
+
+2. **Key material crosses the WASM↔JS boundary.** The Master Unlock Key (MUK) exists in the JS heap as an exportable JWK on the account handler object. SRP-X values are cached in JS. Decrypted item secrets (passwords, OTPs) are returned from WASM to JS for fill operations.
+
+3. **JS WebCrypto is used minimally** — only 4 `crypto.subtle.*` calls in all of background.js (2 `digest`, 1 `sign`, 1 `importKey`, 1 `decrypt`). The WASM core handles virtually all crypto: SRP, PBKDF2, HKDF, AES-GCM, AES-CBC, RSA-OAEP, ECDSA, HMAC, NaCl.
+
+4. **A compromised background page has full access** to all key material and decrypted secrets. WASM linear memory is readable from JS in the same origin.
+
+See [key-hierarchy.md](key-hierarchy.md) for the full key derivation and authentication model.
+
## Permission Profile
### Always granted
diff --git a/key-hierarchy.md b/key-hierarchy.md
new file mode 100644
index 0000000..288b674
--- /dev/null
+++ b/key-hierarchy.md
@@ -0,0 +1,201 @@
+# Key Hierarchy & Authentication Model
+
+Reconstructed from static analysis of background.js sign-in, re-auth, biometry, and MFA flows.
+
+## Key Derivation
+
+```
+Master Password (user input)
+ + Secret Key (A3-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)
+ │
+ ├─ PBKDF2/HKDF ──→ Master Unlock Key (MUK)
+ │ │
+ │ ├─ Used to derive SRP-X for authentication
+ │ ├─ Exportable as JWK: {kty, kid, alg, k, ext, key_ops}
+ │ ├─ Stored in JS heap on account handler for entire unlocked session
+ │ └─ Optionally saved to OS secure enclave via native messaging (biometry)
+ │
+ └─ SRP protocol ──→ SRP-X (exchange value)
+ ├─ Cached in JS on CTX.session.auth.srpX
+ ├─ Used for re-authentication without password
+ └─ Optionally saved to OS secure enclave via native messaging (biometry)
+```
+
+## Key Storage Locations
+
+| Key | JS Heap | WASM Memory | IndexedDB | Native/Secure Enclave | Server |
+|-----|---------|-------------|-----------|----------------------|--------|
+| Master Password | Timeboxed (5 min), then `undefined` | During derivation | Never | Never | Never |
+| Secret Key | On account handler, in DB | During SRP | Encrypted in DB | Never | Never (derived server-side during SRP) |
+| MUK | **Yes** — exportable JWK on account handler | Used for crypto ops | Never directly | **Yes** — via biometry save | Never |
+| SRP-X | **Yes** — on `CTX.session.auth` | During SRP | Never | **Yes** — via biometry save | Never (used in SRP exchange) |
+| dSecret | On account object | During MFA | In account DB | **Yes** — for dSecret proxy | Server-issued |
+| Session keys | Inside CTX (JS) | Used for API calls | Never | Never | Session-scoped |
+| Vault keys | Likely WASM-only | **Yes** — decrypted from server keysets | Never in plaintext | Never | Encrypted with keyset hierarchy |
+| Item field values | **Yes** — during fill | During decrypt | Never in plaintext | Never | Encrypted in vault |
+
+## Authentication Flows
+
+### Primary Sign-In (password-based)
+
+```
+User enters: email + master password + secret key
+ → SecretKey.fromInput() validates format
+ → SA.signIn(ctx, credentials, options)
+ → WASM: PBKDF2/HKDF derives MUK from password + secret key
+ → WASM: SRP exchange with server
+ → Server validates SRP proof
+ → If domain_changed: retry with new host (max 1 redirect)
+ → If mfa_required: enter MFA flow (see below)
+ → CTX populated with session, account, user
+ → Account overview fetched (getAccountWithAttrs)
+ → Password set on CTX.user.password temporarily, then set to undefined
+ → Password timeboxed in closure (5 min) for Duo MFA re-use
+```
+
+### MUK-based Re-Authentication (silent, no user interaction)
+
+When a server request returns 401 (authentication required), the extension re-authenticates using cached credentials:
+
+```
+executeWithReauth(operationName, callback)
+ → Try operation
+ → If 401 error:
+ 1. Try delegated session (from another signed-in context)
+ 2. If that fails, try MUK + SRP-X re-auth:
+ → buildMukSrpXCredentials():
+ - Export MUK as JWK from account handler
+ - Export Secret Key as readable string
+ - Package with email, UUID, srpX, dSecret
+ → Initialize new CTX with these credentials
+ → SRP exchange with server using MUK (no password needed)
+ 3. If new context requires auth migration: sign out
+ 4. Retry original operation
+```
+
+**Security implication:** As long as the MUK is in memory (entire unlocked session), the extension can re-authenticate silently. No user interaction required.
+
+### Biometric Unlock
+
+```
+Save flow (when unlocking with password succeeds):
+ → fkA(): for each unlocked account handler:
+ → Export MUK as JWK
+ → Get SRP-X from client context
+ → Package: {accountUuid, userUuid, muk: {kty, kid, alg, k, ...}, srpX}
+ → browser.runtime.sendNativeMessage("", {
+ name: "core",
+ data: JSON.stringify({type: "Biometry", data: {type: "save", data: {secrets: [...]}}})
+ })
+ → Native app stores in OS secure enclave (Touch ID / Watch)
+
+Unlock flow (biometric prompt):
+ → browser.runtime.sendNativeMessage("", {
+ name: "core",
+ data: JSON.stringify({type: "Biometry", data: {type: "unlock", data: {accounts: [...]}}})
+ })
+ → Native app retrieves from secure enclave after biometric verification
+ → Returns: {secrets: [{accountUuid, muk: JWK, srpX}], userFallback, userCancel}
+ → For each returned secret:
+ → Import MUK JWK back into crypto key
+ → Sign in using MUK + SRP-X (same as re-auth flow)
+ → On enrollment change (e.g., new fingerprint): auto-disable and re-enroll
+```
+
+### Duo MFA
+
+```
+If server requires MFA and Duo is enabled:
+ → Password timeboxed in closure (5 min reuse window)
+ → CTX.user.password set to undefined (cleared from context)
+ → ZR class (Duo authenticator):
+ → chrome.tabs.create(authURL) — opens Duo tab
+ → chrome.webRequest.onHeadersReceived — monitors for redirect to duo-sign-in
+ → chrome.tabs.onUpdated — watches for duo_code URL parameter
+ → On code received: tabs.remove(duoTab), resolve promise
+ → 5-minute timeout on entire flow
+ → SA.completeMFASignInWithDuo(ctx, {type: "duov4", code}, options)
+ → If password still in timebox: restore to CTX.user.password for session completion
+ → Password reference cleared again after sign-in completes
+```
+
+### dSecret MFA Bypass
+
+```
+If server requires MFA and dSecret is available:
+ → SA.completeMFASignInWithDSecret(ctx, dSecret, options)
+ → If dSecret is invalid (device deauthorized): fall through to Duo
+ → On success: mark account as not needing offline MFA
+
+dSecret Proxy (via native app):
+ → _QA(accountIdentifier, sessionId)
+ → If desktop app connected: desktopConnectionManager.requestDsecretProxy(...)
+ → Native app provides dsecretHmac + deviceUUID
+ → SA.completeMFASignInWithDSecretProxy(ctx, dsecretHmac, deviceUUID, options)
+```
+
+### Delegated Sessions
+
+```
+When one client context needs to re-auth:
+ → requestDelegatedSession(accountUUID)
+ → Returns: {notifier, sessionKey}
+ → Export initialization state from old CTX
+ → Build new CTX with delegated sessionKey + old init state
+ → If transport token enabled: transfer from old CTX
+```
+
+## Password Handling Details
+
+### Timebox Mechanism
+```javascript
+var gkA = 5 * 60 * 1000; // 5 minutes
+function _7e(password) {
+ let j = { password };
+ setTimeout(() => { j.password = undefined }, gkA);
+ return (checkOnly) => {
+ let t = j.password;
+ return checkOnly && t ? true : (j.password = undefined, t);
+ };
+}
+```
+
+The password is held in a closure. After 5 minutes, it's set to `undefined`. The getter function either checks existence (without consuming) or reads and clears. This is used for Duo MFA re-use — if the user needs to complete MFA, the password must still be available.
+
+### Password on CTX
+During sign-in: `CTX.user.password = accountPassword`
+After account load: `CTX.user.password = undefined`
+During Duo flow: password temporarily removed from CTX, restored from timebox after Duo completes, then removed again.
+
+## Crypto Algorithms (from code)
+
+| Algorithm | Usage |
+|-----------|-------|
+| PBKDF2 | Master password → key derivation |
+| HKDF | Key derivation (with SHA-256) |
+| AES-GCM | Vault item encryption/decryption |
+| AES-CBC | Legacy vault item encryption |
+| RSA-OAEP | Key wrapping, keyset encryption |
+| ECDSA | Signing (trust log, device trust) |
+| HMAC (SHA-256) | dSecret, message authentication |
+| SHA-256 | General hashing |
+| SHA-1 | HIBP password prefix hashing |
+| SRP | Authentication protocol (custom `SrpMethod`, `SrpStarter`, `SrpV`, `SrpX`) |
+| NaCl | `lessSafeOpenNaCl` — likely for legacy or specific crypto operations |
+| HPKE (RFC 9180) | Item sharing, sealed encryption |
+| X25519 | Key agreement (`JwkX25519PriKey`, `JwkX25519PubKey`) |
+| Noise-like protocol | Mycelium relay handshake |
+
+## Security Observations
+
+1. **No memory zeroing in JS.** MUK, SRP-X, decrypted passwords — all rely on JS garbage collection. The `undefined` assignment removes the reference but doesn't zero the underlying memory. GC timing is non-deterministic.
+
+2. **MUK is the skeleton key.** With the MUK + SRP-X, anyone can silently authenticate as the user and access all vaults. These values persist in JS for the entire unlocked session and are optionally stored in the OS secure enclave.
+
+3. **The `_dangerousInnerCTX` naming** is honest — the developers know the CTX object in JS is sensitive. It contains everything needed to make authenticated requests.
+
+4. **Re-auth is transparent to the user.** If a session expires, the extension silently re-authenticates using cached MUK + Secret Key + SRP-X. Users may not realize their session was re-established.
+
+5. **Duo MFA password reuse window** — the master password is kept for up to 5 minutes in a closure specifically to survive the Duo MFA flow. If the Duo flow takes less than 5 minutes, the password is available for that window.
+
+6. **Biometry stores MUK + SRP-X in the clear (as JWK)** in the OS secure enclave. The native messaging channel carries this data as plaintext JSON. If the native messaging protocol is intercepted (unlikely but possible on compromised systems), the MUK is exposed.
diff --git a/message-catalog.md b/message-catalog.md
index db5be14..416dd23 100644
--- a/message-catalog.md
+++ b/message-catalog.md
@@ -92,6 +92,29 @@ The main handler registration (`m5({...})`) in `background.js` registers these c
| `report-error` | Structured error reporting from any context |
| `health-check-request` | (in health-check.js) Returns `health-check-response` with `"alive"` |
+## Native Messaging Protocol (Background → Native App)
+
+Messages sent via `browser.runtime.sendNativeMessage("")` with JSON envelope `{name: "core", data: JSON.stringify({type, data})}`.
+
+### Biometry Messages
+| Type | Subtype | Direction | Data |
+|------|---------|-----------|------|
+| `Biometry` | `save` | → Native | `{secrets: [{accountUuid, userUuid, muk: JWK, srpX}]}` |
+| `Biometry` | `unlock` | → Native, ← Response | Request: `{accounts: [...], useBiometry, useAppleWatch, fallbackPhrase, unlockPhrase}`. Response: `{secrets: [...], userFallback, userCancel}` |
+| `Biometry` | `remove` | → Native | `{accounts: [...], useBiometry, useAppleWatch, ...}` |
+| `Biometry` | `biometryAvailability` | → Native, ← Response | Response: `{current_availability, current_method, current_policy}` |
+
+All native messages have a 10-second timeout.
+
+### Desktop Connection Messages
+| Operation | Notes |
+|-----------|-------|
+| `requestUpgradeFromOfflineState` | Ask desktop app to re-establish online connection |
+| `requestDsecretProxy` | Get dSecret HMAC from desktop app for MFA bypass |
+| `saveBiometryUnlockSecrets` | Store MUK + SRP-X in OS secure enclave |
+| `getBiometryUnlockSecrets` | Retrieve after biometric verification |
+| `removeBiometryUnlockSecrets` | Remove from secure enclave |
+
## Content Script → Background Messages (from inline scripts)
### inject-content-scripts.js
@@ -143,9 +166,15 @@ Observed event names used in background's internal pub/sub system:
- `accounts-and-vaults-changed`
- `accounts-locked`
- `can-request-unlock-changed`
-- `unleash-features-changed`
+- `unleash-features-changed` — feature flag updates from Unleash
- `extension-first-survey`
- `unified-panel-update`
+- `set-lock-screen-status` — pushed to UI during biometric unlock (`"working"`, `"error"`)
+
+### Server Notification Events (via WebSocket)
+- `ServerChanged` — server data changed, triggers sync
+- `ServerConnected` — WebSocket reconnected
+- `SessionRequestIdChanged` — session request ID changed, triggers context cache update
## Message Volume
diff --git a/trust-boundaries.md b/trust-boundaries.md
index 2481460..c7d9884 100644
--- a/trust-boundaries.md
+++ b/trust-boundaries.md
@@ -4,7 +4,10 @@
### Zone A — Background (highest extension privilege)
- Full access to all `chrome.*` APIs
-- Holds vault state, account sessions, crypto keys (in WASM memory)
+- 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
@@ -21,20 +24,30 @@
### Zone D — Native Host (desktop app)
- Connected via `nativeMessaging` permission
-- `chrome.runtime.sendNativeMessage` (2 occurrences in background.js)
+- `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`)
-- Trust level: equivalent to or higher than extension (system-level process)
+- **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 (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 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)
@@ -66,9 +79,15 @@
- **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?**
+- `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
@@ -81,26 +100,37 @@
- `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
+### B6: JS ↔ WASM (NOT a privilege boundary)
- 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**
+- 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 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 |
+| 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)
@@ -132,7 +162,8 @@
### 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
+- 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)
@@ -151,19 +182,27 @@ This is a privacy-enhancing measure to prevent DNS providers from fingerprinting
## 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
-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
+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
-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
+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
-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
+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`.