Skip to content

Comments

feat: add Edge/Chromium web extension port#138

Open
phelix001 wants to merge 2 commits intolinux-credentials:mainfrom
phelix001:feat/edge-chromium-webext
Open

feat: add Edge/Chromium web extension port#138
phelix001 wants to merge 2 commits intolinux-credentials:mainfrom
phelix001:feat/edge-chromium-webext

Conversation

@phelix001
Copy link

Summary

  • Ports the Firefox web extension to Edge/Chromium (MV3, Chrome 111+/Edge 111+)
  • Adds webext/add-on-edge/ with a dual content-script architecture to work around Chromium's security model (no exportFunction/cloneInto)
  • Adds native messaging manifest template for Chromium-based browsers

Architecture

The Firefox extension uses exportFunction() to directly override navigator.credentials from a content script. Chromium doesn't support this API, so the Edge port uses two content scripts:

Script World Role
content-main.js MAIN Overrides navigator.credentials.create/get, communicates via window.postMessage
content-bridge.js ISOLATED Bridges postMessage to chrome.runtime.connect() for native messaging
background.js Service Worker Connects to the same native messaging host (credential_manager_shim.py)

Other differences from Firefox version:

  • Base64url encoding via btoa/atob helpers (Chromium lacks Uint8Array.toBase64/fromBase64)
  • chrome.* namespace instead of browser.*
  • Service worker background instead of persistent background page

The Python native messaging host (credential_manager_shim.py) is completely reused — no changes needed.

New files

  • webext/add-on-edge/manifest.json — MV3 manifest with dual content scripts
  • webext/add-on-edge/content-main.js — MAIN world credential override
  • webext/add-on-edge/content-bridge.js — ISOLATED world IPC bridge
  • webext/add-on-edge/background.js — Service worker for native messaging
  • webext/app/credential_manager_shim_edge.json.in — Native messaging manifest template
  • Updated webext/README.md with Edge/Chromium setup instructions

Test plan

  • Load extension in Edge via edge://extensions → "Load unpacked"
  • Configure native messaging manifest with extension ID
  • Start credentialsd + credentialsd-ui D-Bus services
  • Test credential registration on https://webauthn.io
  • Test credential assertion on https://webauthn.io
  • Test on Chrome (chrome://extensions) with equivalent setup
  • Verify Firefox extension still works unchanged

🤖 Generated with Claude Code

Port the Firefox web extension to Edge/Chromium (MV3, Chrome 111+).

Key architectural differences from Firefox version:
- Two content scripts: MAIN world (overrides navigator.credentials)
  and ISOLATED world (bridges to background via chrome.runtime)
- window.postMessage bridge between MAIN and ISOLATED worlds
  (Firefox uses exportFunction/cloneInto which don't exist in Chromium)
- Base64url encoding via btoa/atob helpers instead of
  Uint8Array.toBase64/fromBase64 (not available in Chromium)
- Service worker background script instead of persistent background page
- chrome.* namespace instead of browser.*

New files:
- webext/add-on-edge/ - Complete Edge/Chromium extension
- webext/app/credential_manager_shim_edge.json.in - Native messaging
  manifest template for Chromium-based browsers

Updated README with Edge/Chromium setup instructions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Member

@iinuwa iinuwa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! Thanks for looking into this!

I haven't gotten the chance to test this out yet, but I did an initial look through, and there's quite a bit of duplicated code. We hope not to have to keep this around long term, but I still think it'd be helpful not to duplicate the code.

I think this means that I'd like to see if we can keep all the JavaScript files in the one add-on folder, with different manifests and "utils" files that contain the differences between Firefox and Chromium, and a check at runtime to import the correct one. If that means creating the extra "bridge" port in Firefox and/or a shim of cloneInto() for Chromium even if it's technically unnecessary, then that's fine with me.

Then we'd use Meson to bundle the add-ons for each browser platform.

I can help with the Meson parts; would you be willing to look into merging these two folders together?

Comment on lines 9 to 27
function arrayBufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

function base64urlToBytes(str) {
if (!str) return null;
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is built into Chromium as Uint8Array.from/toBase64; can we use those?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — switched to native Uint8Array.toBase64() / fromBase64() with {alphabet: "base64url", omitPadding: true} throughout. The manual btoa/atob helpers are removed entirely.

Comment on lines 10 to 29
// Base64url helpers (Chromium doesn't have Uint8Array.toBase64/fromBase64)
function arrayBufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

function base64urlToArrayBuffer(str) {
if (!str) return null;
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here; the comment is out of date: Chrome has had these for about 6 months.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — removed the outdated comment and the manual helpers. Using native Uint8Array.toBase64() / fromBase64() here as well.

Address PR review feedback to eliminate code duplication between
webext/add-on/ (Firefox) and webext/add-on-edge/ (Chromium).

Key changes:
- Unified architecture: both browsers now use MAIN + ISOLATED world
  content scripts with window.postMessage bridge, eliminating the
  need for Firefox-specific cloneInto()/exportFunction() APIs
- Use native Uint8Array.toBase64()/fromBase64() for base64url
  encoding/decoding (supported in both Firefox 140+ and Chrome 111+)
- Simplified background.js: ArrayBuffer serialization now happens in
  content-main.js, so background just forwards messages
- Browser-specific manifests: manifest.firefox.json (background
  scripts) and manifest.chromium.json (service worker)
- Browser API detection via globalThis.browser || globalThis.chrome
  in content-bridge.js and background.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phelix001
Copy link
Author

Thanks for the review! I've pushed a commit that addresses all the feedback:

Merged into a single add-on/ folder — deleted add-on-edge/ entirely. Both browsers now share the same JS files:

  • content-main.js (MAIN world) — overrides navigator.credentials, handles all ArrayBuffer serialization
  • content-bridge.js (ISOLATED world) — relays messages between MAIN world and background via window.postMessageruntime.connect
  • background.js — simplified to just forward messages (no more serializeRequest needed since content-main.js handles serialization)

Browser-specific manifests: manifest.firefox.json (background scripts) and manifest.chromium.json (service worker)

Native Uint8Array.toBase64()/fromBase64() used everywhere — all manual btoa/atob helpers removed.

Browser API detection via globalThis.browser || globalThis.chrome in background.js and content-bridge.js.

Firefox now uses the same MAIN + ISOLATED world architecture as Chromium (with the bridge port as you suggested), which eliminates the need for cloneInto()/exportFunction() entirely.

I left a TODO in meson.build for the Chromium build target — happy to take your help on that part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants