experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Proton Pass WebAuthn/passkey support via main-world injection

- Inject webauthn.js via executeJavaScript() on dom-ready for http/https
webviews, enabling navigator.credentials override in the main world
- Only injects when Proton Pass is loaded (isChromeExtensionLoaded check)
- Falls back to native WebAuthn if orchestrator not connected (5s timeout)
- Update compatibility doc: all 4 API blockers resolved, extension enabled

+102 -130
+36
backend/electron/entry.ts
··· 84 84 getChromeExtensionUiEntries, 85 85 openChromeExtensionPage, 86 86 cleanupChromeExtensions, 87 + isChromeExtensionLoaded, 87 88 } from './chrome-extensions.js'; 88 89 import { 89 90 initProfilesDb, ··· 148 149 // Initialize backend config with runtime values 149 150 setPreloadPath(preloadPath); 150 151 152 + // Read Proton Pass webauthn.js for main-world injection into web content webviews. 153 + // Electron ignores "world": "MAIN" in Chrome extension manifest content_scripts, 154 + // so we inject the script ourselves via executeJavaScript() which runs in the main world. 155 + let webauthnScript: string | null = null; 156 + const webauthnScriptPath = path.join(ROOT_DIR, 'chrome-extensions', 'proton-pass', 'webauthn.js'); 157 + try { 158 + if (fs.existsSync(webauthnScriptPath)) { 159 + webauthnScript = fs.readFileSync(webauthnScriptPath, 'utf-8'); 160 + DEBUG && console.log('[webauthn] Loaded Proton Pass webauthn.js for injection'); 161 + } 162 + } catch (err) { 163 + console.error('[webauthn] Failed to read webauthn.js:', err); 164 + } 165 + 151 166 // Inject preload into <webview> elements that load peek:// URLs. 152 167 // This gives internal widget pages (e.g., HUD widgets) access to window.app API 153 168 // while leaving external web content webviews untouched. 169 + // 170 + // Also inject Proton Pass webauthn.js into http/https webviews for passkey support. 171 + // The script overrides navigator.credentials.create/get in the main world. 154 172 app.on('web-contents-created', (_event, contents) => { 155 173 contents.on('will-attach-webview', (_wvEvent, webPreferences, params) => { 156 174 if (params.src && params.src.startsWith('peek://')) { ··· 158 176 DEBUG && console.log(`[webview] Injecting preload for peek:// webview: ${params.src}`); 159 177 } 160 178 }); 179 + 180 + // Inject webauthn.js into web content webviews for Proton Pass passkey support. 181 + // Uses executeJavaScript() which runs in the main world, allowing the script to 182 + // override navigator.credentials.create() and navigator.credentials.get(). 183 + // Falls back to native WebAuthn if Proton Pass orchestrator isn't connected. 184 + if (contents.getType() === 'webview') { 185 + contents.on('dom-ready', () => { 186 + if (!webauthnScript || !isChromeExtensionLoaded('proton-pass')) return; 187 + 188 + const url = contents.getURL(); 189 + if (url.startsWith('http://') || url.startsWith('https://')) { 190 + contents.executeJavaScript(webauthnScript).catch(() => { 191 + // Ignore injection errors (webContents may be destroyed) 192 + }); 193 + DEBUG && console.log(`[webauthn] Injected Proton Pass webauthn.js into: ${url}`); 194 + } 195 + }); 196 + } 161 197 }); 162 198 163 199 const strings = {
+19
backend/electron/preload-webauthn.js
··· 1 + /** 2 + * WebAuthn Bridge for Proton Pass — Architecture Note 3 + * 4 + * This file is intentionally empty. The WebAuthn injection is handled directly 5 + * in entry.ts by reading the Proton Pass extension's webauthn.js file and 6 + * injecting it via webContents.executeJavaScript() into http/https webviews. 7 + * 8 + * Why executeJavaScript() instead of a preload script: 9 + * - Electron preload scripts run in an isolated context when contextIsolation 10 + * is enabled (which it is for webviews by default) 11 + * - Proton Pass's webauthn.js needs to override navigator.credentials in the 12 + * page's MAIN world, not an isolated world 13 + * - executeJavaScript() runs in the main world, so the overrides are visible 14 + * to page scripts 15 + * - This matches how Electron ignores "world": "MAIN" in Chrome extension 16 + * manifest content_scripts — we work around it at the application level 17 + * 18 + * See entry.ts, search for "webauthn" for the injection logic. 19 + */
+47 -130
notes/proton-pass-electron-compatibility.md
··· 1 - # Proton Pass Extension: Electron Compatibility Report 1 + # Proton Pass Extension: Electron Integration 2 2 3 - **Date**: 2026-02-19 3 + **Date**: 2026-03-18 (updated from 2026-02-19) 4 4 **Proton Pass version**: 1.34.2 (from Chrome Web Store) 5 5 **Electron version**: 40.0.0 (Chromium 144) 6 6 **Manifest version**: MV3 7 7 8 8 --- 9 9 10 - ## Status: Partially Working — Critical API Gaps 10 + ## Status: Working — Enabled by Default 11 11 12 - The Proton Pass extension has been downloaded, extracted, and placed in 13 - `resources/chrome-extensions/proton-pass/`. Electron will load the extension 14 - (it discovers it, registers it, and can show the popup), but **several Chrome 15 - APIs required by Proton Pass are not implemented in Electron**, causing 16 - runtime failures in core functionality. 12 + Proton Pass is installed at `chrome-extensions/proton-pass/` and enabled by default. All previously identified API blockers have been resolved via polyfills. Passkey/WebAuthn support is enabled via main-world script injection. 17 13 18 14 --- 19 15 ··· 24 20 | Size | ~20 MB (includes WASM crypto modules) | 25 21 | License | GPLv3 | 26 22 | Manifest | V3 (service worker background) | 27 - | Content scripts | orchestrator.js (all frames), webauthn.js (MAIN world) | 23 + | Content scripts | orchestrator.js (all frames, ISOLATED world), webauthn.js (MAIN world) | 28 24 | Popup | popup.html | 29 25 | Settings | settings.html (options_ui) | 30 26 | Offscreen doc | offscreen.html (clipboard operations) | ··· 32 28 33 29 --- 34 30 35 - ## API Compatibility Matrix 31 + ## How It Works 36 32 37 - ### Declared Permissions 33 + ### Extension loading 34 + - `backend/electron/chrome-extensions.ts` discovers extensions in `chrome-extensions/` 35 + - Extensions are loaded via `session.loadExtension()` into the profile session 36 + - The profile session is the same session used by page webviews, so content scripts inject correctly 37 + - Proton Pass was removed from `DISABLED_BY_DEFAULT` — it loads automatically 38 38 39 - | Permission | Electron Support | Impact | 40 - |------------|-----------------|--------| 41 - | `activeTab` | Partial | May work for basic tab access | 42 - | `alarms` | **NOT SUPPORTED** | **CRITICAL** — session management, retry logic, scheduled tasks | 43 - | `offscreen` | **NOT SUPPORTED** | **MODERATE** — clipboard read/write operations | 44 - | `scripting` | Supported | Content script injection works | 45 - | `storage` | `local` only (no `sync`, no `session`) | **HIGH** — extension uses `storage.session` for sensitive data | 46 - | `unlimitedStorage` | Unknown | Storage quota may be limited | 47 - | `webNavigation` | **NOT SUPPORTED** | **HIGH** — page navigation tracking for autofill | 48 - | `webRequest` | Supported | Network request observation works | 39 + ### Content script injection 40 + - `orchestrator.js` injects into all frames in the ISOLATED world (Electron handles this natively) 41 + - This script manages form detection, autofill icon injection, and communication with the service worker 42 + - The autofill icons in form fields work because orchestrator.js runs in the isolated world and can manipulate the page DOM 49 43 50 - ### Chrome APIs Used in Code 44 + ### Passkey/WebAuthn support 45 + - `webauthn.js` needs to run in the MAIN world to override `navigator.credentials.create()` and `.get()` 46 + - **Electron ignores `"world": "MAIN"` in manifest content scripts** — this is a known limitation 47 + - **Solution**: `backend/electron/entry.ts` reads `webauthn.js` at startup, then injects it via `webContents.executeJavaScript()` on `dom-ready` for http/https webviews 48 + - `executeJavaScript()` runs in the main world, so the `navigator.credentials` overrides are visible to page scripts 49 + - Injection is conditional: only runs when `isChromeExtensionLoaded('proton-pass')` returns true 50 + - Timing: `dom-ready` is sufficient because WebAuthn calls happen on user interaction (button clicks), not at page load 51 + - Communication chain: webauthn.js (MAIN world) → orchestrator.js (ISOLATED world) via `window.postMessage` → service worker via `chrome.runtime.sendMessage` 52 + - Falls back to native WebAuthn if Proton Pass orchestrator isn't connected (5s timeout) 51 53 52 - | API | Usage | Electron Status | 53 - |-----|-------|-----------------| 54 - | `chrome.alarms` | 21 references — session timers, retry scheduling, worker activation | Not implemented | 55 - | `chrome.offscreen` | 9 references — clipboard copy/paste via offscreen document | Not implemented | 56 - | `chrome.webNavigation` | 7 references — page load detection for autofill injection | Not implemented | 57 - | `chrome.webRequest` | 14 references — request monitoring | Supported | 58 - | `chrome.runtime` | Messaging, extension lifecycle | Supported | 59 - | `chrome.storage.session` | Sensitive data (auth tokens, session keys) | **NOT SUPPORTED** — only `storage.local` works | 60 - | `chrome.tabs` | Tab management (via polyfill wrapper) | Partially supported | 61 - | `chrome.action` | Badge text, popup, icon changes | Partially supported | 62 - | `chrome.permissions` | Runtime permission requests | Not documented as supported | 63 - 64 - --- 65 - 66 - ## Critical Blockers 67 - 68 - ### 1. `chrome.alarms` (21 usages) 69 - The extension relies heavily on alarms for: 70 - - Session resume scheduling after auth failures 71 - - Worker activation/reactivation 72 - - Periodic cleanup and sync operations 73 - - Authentication retry with backoff 54 + ### Chrome API polyfills 55 + All previously missing APIs are now polyfilled in `backend/electron/chrome-api-polyfills/`: 74 56 75 - Without alarms, the extension's service worker lifecycle management breaks 76 - entirely. The service worker will start but cannot schedule any deferred work. 57 + | API | Polyfill | How | 58 + |-----|----------|-----| 59 + | `chrome.alarms` | `alarms.js` | Node.js `setTimeout`/`setInterval` with persistent tracking | 60 + | `chrome.storage.session` | `storage-session.js` | In-memory storage, cleared on app quit | 61 + | `chrome.webNavigation` | `webnavigation.js` | Electron `webContents` navigation events forwarded to extension | 62 + | `chrome.offscreen` | `offscreen.js` | Electron `clipboard` module for copy/paste | 63 + | `chrome.permissions` | `permissions.js` | All permissions granted via `setPermissionCheckHandler` | 77 64 78 - ### 2. `chrome.storage.session` (used for auth tokens) 79 - Proton Pass stores sensitive authentication data in `storage.session` (which 80 - is encrypted and cleared when the browser closes). Electron only supports 81 - `storage.local`. This means: 82 - - Auth tokens would need to be stored in `storage.local` (less secure) 83 - - Or authentication simply fails at runtime 84 - 85 - ### 3. `chrome.webNavigation` (7 usages) 86 - Used to detect page navigations and trigger autofill form detection. Without 87 - this, the extension cannot detect when a user navigates to a login page and 88 - therefore cannot offer autofill suggestions. 89 - 90 - ### 4. `chrome.offscreen` (9 usages) 91 - Used for clipboard operations (copy/paste passwords). Without this, 92 - copy-to-clipboard functionality breaks. This is a usability issue but not a 93 - complete blocker for login/autofill. 65 + Polyfills are injected via `webContents.executeJavaScript()` on `dom-ready` (line ~344 of `chrome-extensions.ts`). 94 66 95 67 --- 96 68 97 - ## What Will Work 98 - 99 - 1. **Extension loading** — Electron will load the unpacked extension from the directory 100 - 2. **Content script injection** — `orchestrator.js` will inject into pages 101 - 3. **Popup UI** — popup.html will render (Peek already has popup window support) 102 - 4. **Settings page** — settings.html will render 103 - 5. **Service worker** — background.js will start (Electron 40 + Chromium 144 supports MV3 service workers) 104 - 6. **WASM crypto** — WebAssembly modules should work with `wasm-unsafe-eval` CSP 105 - 7. **Basic messaging** — `chrome.runtime.sendMessage` / `onMessage` works 106 - 107 - --- 108 - 109 - ## What Will NOT Work 110 - 111 - 1. **Session management** — No `chrome.alarms` means no session refresh, no retry logic 112 - 2. **Autofill detection** — No `chrome.webNavigation` means no page change detection 113 - 3. **Clipboard** — No `chrome.offscreen` means no copy/paste passwords 114 - 4. **Secure session storage** — No `chrome.storage.session` 115 - 5. **Authentication flow** — Likely broken due to combination of missing alarms + session storage 116 - 117 - --- 118 - 119 - ## Possible Remediation Approaches 120 - 121 - ### Option A: Polyfill Missing APIs (Moderate Effort) 122 - Implement shims for missing APIs in Electron's main process: 123 - 124 - 1. **`chrome.alarms`** — Implement using Node.js `setTimeout`/`setInterval` with persistent tracking 125 - 2. **`chrome.storage.session`** — Map to in-memory storage that clears on app quit 126 - 3. **`chrome.webNavigation`** — Listen to Electron's `webContents` navigation events and forward to extension 127 - 4. **`chrome.offscreen`** — Implement clipboard access via Electron's `clipboard` module 128 - 129 - This would require modifying `backend/electron/chrome-extensions.ts` to inject 130 - these polyfills into the extension's service worker context. 131 - 132 - ### Option B: Use electron-chrome-extensions Library (Lower Effort) 133 - The [Polypane/electron-chrome-extensions](https://github.com/Polypane/electron-chrome-extensions) 134 - fork provides broader API coverage including many of the missing APIs. This 135 - would replace Electron's built-in `session.loadExtension()` with a more 136 - complete implementation. 137 - 138 - ### Option C: Build a Custom Proton Pass Integration (High Effort, Best UX) 139 - Instead of loading the browser extension, build a native Electron integration 140 - that uses the Proton Pass API directly. This would provide: 141 - - Native autofill without extension content scripts 142 - - Direct clipboard integration 143 - - Proper secure storage via Electron's safeStorage API 144 - 145 - --- 69 + ## Known Limitations 146 70 147 - ## Recommendation 71 + 1. **MAIN world content scripts**: Only `webauthn.js` needs this, and it's handled by the manual injection in `entry.ts`. If future Chrome extensions need MAIN world scripts, the same pattern can be applied. 148 72 149 - **Option A (polyfills)** is the most pragmatic path. The four missing APIs 150 - have well-defined behavior that can be shimmed: 73 + 2. **Native WebAuthn**: Electron on macOS does not support the native `navigator.credentials` API. This doesn't matter for Proton Pass because it handles passkeys entirely within the extension — it doesn't need the browser's native implementation. 151 74 152 - 1. `chrome.alarms` → `setTimeout`/`setInterval` in main process 153 - 2. `chrome.storage.session` → In-memory Map, cleared on quit 154 - 3. `chrome.webNavigation` → Electron `webContents` event forwarding 155 - 4. `chrome.offscreen` → `electron.clipboard` module 75 + 3. **User must log in**: Proton Pass requires authentication via Proton account. The popup (`popup.html`) is accessible via Peek's extension UI infrastructure. 156 76 157 - This approach keeps the extension unmodified and benefits from upstream 158 - Proton Pass updates. 77 + 4. **WASM CSP**: The extension's CSP includes `'wasm-unsafe-eval'` for its crypto modules. This works in Electron. 159 78 160 79 --- 161 80 162 - ## File Locations 163 - 164 - - Extension files: `resources/chrome-extensions/proton-pass/` 165 - - Extension manifest: `resources/chrome-extensions/proton-pass/manifest.json` 166 - - Extension manager: `backend/electron/chrome-extensions.ts` 167 - - Prior research: `notes/research-bundled-web-extensions.md` 168 - 169 - ## Source 81 + ## Files 170 82 171 - - Chrome Web Store ID: `ghmbeldphafepmbegfdlkpapadhbakde` 172 - - GitHub source: https://github.com/ProtonMail/WebClients/tree/main/applications/pass-extension 83 + | File | Purpose | 84 + |------|---------| 85 + | `chrome-extensions/proton-pass/` | Unpacked extension files | 86 + | `backend/electron/chrome-extensions.ts` | Extension loading, polyfill injection, enabled/disabled config | 87 + | `backend/electron/chrome-api-polyfills/` | Chrome API polyfills (alarms, storage.session, webNavigation, offscreen, permissions) | 88 + | `backend/electron/entry.ts` | WebAuthn main-world injection via `executeJavaScript()` | 89 + | `notes/proton-pass-electron-compatibility.md` | This document |