Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions webext/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
This is a web extension that allows browsers to connect to the D-Bus service
provided by this project. It can be used for testing.

Currently, this is written only for Firefox; there will be some slight API
tweaks required to make this work in Chrome.
Two variants are provided:
- `add-on/` - Firefox (MV3, requires Firefox 140+)
- `add-on-edge/` - Edge/Chromium (MV3, requires Chrome 111+ or Edge 111+)

This requires some setup to make it work:

Expand Down Expand Up @@ -48,11 +49,11 @@ couple of options:
4. Navigate to [https://webauthn.io]().
5. Run through the registration and creation process.

## For Development
## For Development (Firefox)

(Note: Paths are relative to root of this repository)

1. Copy `webext/app/credential_manager_shim.json` to `~/.mozilla/native-messaging-hosts/credential_manager_shim.json`.
1. Copy `webext/app/credential_manager_shim.json` to `~/.mozilla/native-messaging-hosts/xyz.iinuwa.credentialsd_helper.json`.
2. In `webext/app/credential_manager_shim.py`, point the `DBUS_DOC_FILE`
variable to the absolute path to
`doc/xyz.iinuwa.credentialsd.Credentials.xml`.
Expand All @@ -64,3 +65,40 @@ couple of options:
- `./build/credentialsd/target/debug/credentialsd`
7. Navigate to [https://webauthn.io]().
8. Run through the registration and creation process.

## For Development (Edge/Chromium)

(Note: Paths are relative to root of this repository)

1. In `webext/app/credential_manager_shim.py`, point the `DBUS_DOC_FILE`
variable to the absolute path to
`doc/xyz.iinuwa.credentialsd.Credentials.xml`.
2. Open Edge and go to `edge://extensions` (or `chrome://extensions` for Chrome).
3. Enable "Developer mode" (toggle in top right).
4. Click "Load unpacked" and select the `webext/add-on-edge/` directory.
5. Note the extension ID shown on the extensions page (e.g., `abcdefghijklmnop...`).
6. Create the native messaging manifest:
```shell
# For Edge:
mkdir -p ~/.config/microsoft-edge/NativeMessagingHosts
# For Chrome:
# mkdir -p ~/.config/google-chrome/NativeMessagingHosts
# For Chromium:
# mkdir -p ~/.config/chromium/NativeMessagingHosts

cat > ~/.config/microsoft-edge/NativeMessagingHosts/xyz.iinuwa.credentialsd_helper.json << EOF
{
"name": "xyz.iinuwa.credentialsd_helper",
"description": "Helper for integrating browser with credentialsd project",
"path": "$(readlink -f webext/app/credential_manager_shim.py)",
"type": "stdio",
"allowed_origins": [ "chrome-extension://YOUR_EXTENSION_ID/" ]
}
EOF
```
Replace `YOUR_EXTENSION_ID` with the extension ID from step 5.
7. Build with `ninja -C ./build` and run the D-Bus services:
- `GSCHEMA_SCHEMA_DIR=build/credentialsd-ui/data ./build/credentialsd-ui/target/debug/credentialsd-ui`
- `./build/credentialsd/target/debug/credentialsd`
8. Navigate to [https://webauthn.io]().
9. Run through the registration and creation process.
138 changes: 38 additions & 100 deletions webext/add-on/background.js
Original file line number Diff line number Diff line change
@@ -1,125 +1,63 @@
/*
On startup, connect to the "credential_shim" app.
*/
/**
* Background script that bridges content script messages
* to the native messaging host.
*
* Works in both Firefox (background script) and Chromium (service worker).
* ArrayBuffer serialization is handled by the MAIN world content script,
* so this script simply forwards messages between content and native.
*/

const browserAPI = globalThis.browser || globalThis.chrome;

let contentPort;
let nativePort;

function connected(port) {
console.log("received connection from content script");

// initialize content port
console.log('[credentialsd] received connection from content script');
contentPort = port;
console.log(contentPort);

// Initialize native port
nativePort = browser.runtime.connectNative("xyz.iinuwa.credentialsd_helper");
console.debug(nativePort);
if (nativePort.error !== null) {
console.error(nativePort.error)
throw nativePort.error
// Connect to native messaging host
nativePort = browserAPI.runtime.connectNative('xyz.iinuwa.credentialsd_helper');

// Check for connection errors (browser-specific patterns)
const connectError = nativePort.error || browserAPI.runtime.lastError;
if (connectError) {
console.error('[credentialsd] native connect error:', connectError.message || connectError);
return;
}
console.log(`connected to native app`)
console.log(nativePort)

// Set up content port listener
contentPort.onMessage.addListener(rcvFromContent)
console.log('[credentialsd] connected to native app');

// Set up native port listener
console.log("setting up native port response listener")
nativePort.onMessage.addListener(rcvFromNative);
contentPort.onMessage.addListener(rcvFromContent);
nativePort.onMessage.addListener(rcvFromNative);

nativePort.onDisconnect.addListener(() => {
const error = browserAPI.runtime.lastError;
if (error) {
console.error('[credentialsd] native port disconnected:', error.message);
}
});
}

function rcvFromContent(msg) {
const { requestId, cmd, options } = msg;
const origin = contentPort.sender.origin
const topOrigin = new URL(contentPort.sender.tab.url).origin
// const isCrossOrigin = origin === topOrigin
// const isTopLevel = contentPort.sender.frameId === 0;
const origin = contentPort.sender.origin;
const topOrigin = new URL(contentPort.sender.tab.url).origin;

if (options) {
const serializedOptions = serializeRequest(options)

console.debug(options.publicKey.challenge)
console.debug("background script received options, passing onto native app")
nativePort.postMessage({ requestId, cmd, options: serializedOptions, origin, topOrigin })
console.debug('[credentialsd] forwarding', cmd, 'to native app');
nativePort.postMessage({ requestId, cmd, options, origin, topOrigin });
} else {
console.debug("background script received message without arguments, passing onto native app")
nativePort.postMessage({ requestId, cmd, origin, topOrigin })
console.debug('[credentialsd] forwarding', cmd, '(no options) to native app');
nativePort.postMessage({ requestId, cmd, origin, topOrigin });
}
}

function rcvFromNative(msg) {
console.log("Received (native -> background): " + msg);
console.log("forwarding to content script");
const { requestId, data, error } = msg;
console.log('[credentialsd] received from native, forwarding to content');
contentPort.postMessage(msg);
}

function serializeBytes(buffer) {
const options = {alphabet: "base64url", omitPadding: true};
return new Uint8Array(buffer).toBase64(options)
}

function deserializeBytes(base64str) {
const options = {alphabet: "base64url"}
return Uint8Array.fromBase64(base64str, options)
}

function serializeRequest(options) {
// Serialize ArrayBuffers
const clone = structuredClone(options)
clone.publicKey.challenge = serializeBytes(clone.publicKey.challenge)
if (clone.publicKey.user) {
clone.publicKey.user.id = serializeBytes(clone.publicKey.user.id)
}
if (clone.publicKey.excludeCredentials) {
for (const cred of clone.publicKey.excludeCredentials) {
cred.id = serializeBytes(cred.id)
}
}
if (clone.publicKey.allowCredentials) {
for (const cred of clone.publicKey.allowCredentials) {
cred.id = serializeBytes(cred.id);
}
}
if (clone.publicKey.extensions && clone.publicKey.extensions.prf) {
if (clone.publicKey.extensions.prf.eval) {
clone.publicKey.extensions.prf.eval.first = serializeBytes(clone.publicKey.extensions.prf.eval.first);
if (clone.publicKey.extensions.prf.eval.second) {
clone.publicKey.extensions.prf.eval.second = serializeBytes(clone.publicKey.extensions.prf.eval.second);
}
}
if (clone.publicKey.extensions.prf.evalByCredential) {
const evalByCredential = clone.publicKey.extensions.prf.evalByCredential;

// Iterate over all credentialIDs, serialize the first/second bytebuffer and replace the original evalByCredential map
const result = {};
for (const credId in evalByCredentialData) {
const prfValue = evalByCredentialData[credId];

if (prfValue && prfValue.first) {
const newPrfValue = {
first: serializeBytes(prfValue.first)
};

if (prfValue.second) {
newPrfValue.second = serializeBytes(prfValue.second);
}
result[credId] = newPrfValue;
};
}
clone.publicKey.extensions.prf.evalByCredential = result;
}

if (clone.publicKey.extensions && clone.publicKey.extensions.credBlob) {
clone.publicKey.extensions.credBlob = serializeBytes(clone.publicKey.extensions.credBlob);
}
}
return clone
}


// Listen for connections from content script
console.log("Starting up credential_manager_shim background script")
browser.runtime.onConnect.addListener(connected);
console.log('[credentialsd] background script starting');
browserAPI.runtime.onConnect.addListener(connected);
36 changes: 36 additions & 0 deletions webext/add-on/content-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Content script running in ISOLATED world.
* Bridges window.postMessage from the MAIN world content script
* to the background script via runtime.connect.
*
* Works in both Firefox and Chromium browsers.
*/

const browserAPI = globalThis.browser || globalThis.chrome;
const port = browserAPI.runtime.connect({ name: 'credentialsd-helper' });

// Forward responses from background back to page context
port.onMessage.addListener((msg) => {
const { requestId, data, error } = msg;
window.postMessage({
type: 'credentialsd-response',
requestId,
data,
error,
}, '*');
});

port.onDisconnect.addListener(() => {
console.warn('[credentialsd] background port disconnected');
});

// Listen for requests from the MAIN world content script
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data?.type !== 'credentialsd-request') return;

const { requestId, cmd, options } = event.data;
port.postMessage({ requestId, cmd, options });
});

console.log('[credentialsd] content bridge active');
Loading