From bb60bce9226a2e86220932fbf9dccfbc312efbf6 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 8 May 2026 06:40:15 -0500 Subject: [PATCH] webext: Update extension to use portal frontend API --- webext/add-on/manifest.firefox.json | 87 +++++++++++--------- webext/app/credential_manager_shim.py | 110 ++++++++++++++++++++++---- webext/app/meson.build | 1 + 3 files changed, 144 insertions(+), 54 deletions(-) diff --git a/webext/add-on/manifest.firefox.json b/webext/add-on/manifest.firefox.json index b700d9f..cbc62d0 100644 --- a/webext/add-on/manifest.firefox.json +++ b/webext/add-on/manifest.firefox.json @@ -1,40 +1,51 @@ { - "description": "Helper to integrate credentialsd with the browser", - "manifest_version": 3, - "name": "credentialsd-helper", - "version": "0.1.0", - "icons": { - "48": "icons/logo.svg" - }, - - "browser_specific_settings": { - "gecko": { - "id": "credentialsd-helper@iinuwa.xyz", - "strict_min_version": "140.0" - } - }, - - "background": { - "service_worker": "background.js" - }, - "content_scripts": [ - { - "matches": ["https://webauthn.io/*", "https://demo.yubico.com/*"], - "js": ["content-bridge.js"], - "run_at": "document_start", - "world": "ISOLATED" + "description": "Helper to integrate credentialsd with the browser", + "manifest_version": 3, + "name": "credentialsd-helper", + "version": "0.1.0", + "icons": { + "48": "icons/logo.svg" }, - { - "matches": ["https://webauthn.io/*", "https://demo.yubico.com/*"], - "js": ["content-main.js"], - "run_at": "document_start", - "world": "MAIN" - } - ], - - "action": { - "default_icon": "icons/logo.svg" - }, - - "permissions": ["nativeMessaging"] -} + "browser_specific_settings": { + "gecko": { + "id": "credentialsd-helper@iinuwa.xyz", + "strict_min_version": "140.0" + } + }, + "background": { + "service_worker": "background.js", + "scripts": [ + "background.js" + ] + }, + "content_scripts": [ + { + "matches": [ + "https://webauthn.io/*", + "https://demo.yubico.com/*" + ], + "js": [ + "content-bridge.js" + ], + "run_at": "document_start", + "world": "ISOLATED" + }, + { + "matches": [ + "https://webauthn.io/*", + "https://demo.yubico.com/*" + ], + "js": [ + "content-main.js" + ], + "run_at": "document_start", + "world": "MAIN" + } + ], + "action": { + "default_icon": "icons/logo.svg" + }, + "permissions": [ + "nativeMessaging" + ] +} \ No newline at end of file diff --git a/webext/app/credential_manager_shim.py b/webext/app/credential_manager_shim.py index 764f573..762613b 100755 --- a/webext/app/credential_manager_shim.py +++ b/webext/app/credential_manager_shim.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +from asyncio import Future import asyncio import base64 import codecs @@ -7,18 +8,22 @@ from enum import Enum import json import logging +import secrets import signal import struct import sys from typing import Optional -from dbus_next.aio import MessageBus from dbus_next import Variant +from dbus_next.aio import MessageBus +from dbus_next.constants import MessageType +from dbus_next.message import Message logging.basicConfig( filename="/tmp/credential_manager_shim.log", encoding="utf-8", level=logging.DEBUG ) +APP_ID = "@APP_ID@" DBUS_DOC_FILE = "@DBUS_DOC_FILE@" @@ -70,6 +75,61 @@ def b64_decode(s) -> bytes: return base64.urlsafe_b64decode(s + padding) +class PortalRequest[T]: + def __init__(self, token: str, fut: Future): + self.token: str = token + self._fut: Future = fut + + async def wait(self) -> T: + return await self._fut + + +def create_portal_request_message_handler(bus: MessageBus) -> PortalRequest: + loop = asyncio.get_running_loop() + future = loop.create_future() + if not bus.connected or bus.unique_name is None: + raise Exception("Bus is not connected") + unique_name = bus.unique_name[1:].replace(".", "_") + token = secrets.token_hex(16) + object_path = f"/org/freedesktop/portal/desktop/request/{unique_name}/{token}" + + def message_handler(msg: Message): + if future.done(): + return False + + message_matches = ( + msg.path == object_path + and msg.message_type == MessageType.SIGNAL + and msg.destination == bus.unique_name + and msg.interface == "org.freedesktop.portal.Request" + and msg.member == "Response" + ) + if not message_matches: + return False + + [code, value] = msg.body + if code == 0: + future.set_result(value) + elif code == 1: + future.set_exception(Exception("Portal request cancelled")) + raise + elif code == 2 and "error" in value: + future.set_exception( + Exception(f"Portal returned an error: {value['error'].value}") + ) + else: + future.set_exception(Exception("Portal returned an unknown error")) + return True + + def when_done(_fut): + bus.remove_message_handler(message_handler) + + future.add_done_callback(when_done) + bus.add_message_handler(message_handler) + logging.debug(f"Listening for {object_path}") + return PortalRequest(token, future) + + class MajorType(Enum): PositiveInteger = (0,) NegativeInteger = (1,) @@ -111,7 +171,7 @@ def _read_value(self, buf): argument = struct.unpack(">Q", buf[1 : 1 + argument_len])[0] elif additional_info == 31: # Indefinite length for types 2-5 - argument = None + argument: Optional[int] = None argument_len = 0 match buf[0] >> 5: case 0: @@ -291,23 +351,24 @@ def has_flag(self, flag): async def create_passkey(interface, options, origin, top_origin): logging.debug("Creating passkey") - is_same_origin = origin == top_origin req_json = json.dumps(options) logging.debug(req_json) + request_event = create_portal_request_message_handler(interface.bus) req = { - "type": Variant("s", "publicKey"), - "origin": Variant("s", origin), - "is_same_origin": Variant("b", is_same_origin), - "publicKey": Variant("a{sv}", {"request_json": Variant("s", req_json)}), + "handle_token": Variant("s", request_event.token), + "public_key": Variant("s", req_json), } + if top_origin != origin: + req["top_origin"] = Variant("s", top_origin) logging.debug("Sending request to D-Bus API") - rsp = await interface.call_create_credential(["", req]) - if rsp["type"].value != "public-key": + _rsp = await interface.call_create_credential("", origin, "publicKey", req) + result = await request_event.wait() + if result["type"].value != "public-key": raise Exception( - f"Invalid credential type received: expected 'public-key', received {rsp['type'.value]}" + f"Invalid credential type received: expected 'public-key', received {result['type'].value}" ) response_json = json.loads( - rsp["public_key"].value["registration_response_json"].value + result["public_key"].value["registration_response_json"].value ) attestation = cbor_loads(b64_decode(response_json["response"]["attestationObject"])) auth_data_view = attestation["authData"] @@ -339,7 +400,7 @@ async def get_passkey(interface, options, origin, top_origin): rsp = await interface.call_get_credential(["", req]) if rsp["type"].value != "public-key": raise Exception( - f"Invalid credential type received: expected 'public-key', received {rsp['type'.value]}" + f"Invalid credential type received: expected 'public-key', received {rsp['type'].value}" ) response_json = json.loads( @@ -356,16 +417,31 @@ async def run(cmd, options, origin, top_origin): logging.info(os.getcwd()) + msg = Message( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.host.portal.Registry", + "Register", + signature="sa{sv}", + body=[ + APP_ID, + {}, + ], + ) + await bus.call(msg) + with open(DBUS_DOC_FILE, "r") as f: introspection = f.read() proxy_object = bus.get_proxy_object( - "xyz.iinuwa.credentialsd.Credentials", - "/xyz/iinuwa/credentialsd/Credentials", + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", introspection, ) - interface = proxy_object.get_interface("xyz.iinuwa.credentialsd.Credentials1") + interface = proxy_object.get_interface( + "org.freedesktop.portal.experimental.Credential" + ) logging.debug(f"Connected to interface at {interface.path}") if cmd == "create": @@ -398,6 +474,7 @@ async def run(cmd, options, origin, top_origin): quit = asyncio.Event() + async def main(): logging.info("starting credential_manager_shim") while not quit.is_set(): @@ -417,5 +494,6 @@ async def main(): logging.debug("Sent error message") logging.info("quitting credential_manager_shim") -signal.signal(signal.SIGTERM, lambda _, __ : quit.set()) + +signal.signal(signal.SIGTERM, lambda _, __: quit.set()) asyncio.run(main()) diff --git a/webext/app/meson.build b/webext/app/meson.build index 532b24e..bd1e227 100644 --- a/webext/app/meson.build +++ b/webext/app/meson.build @@ -6,6 +6,7 @@ addon_app_config.set( datadir / 'credentialsd' / 'xyz.iinuwa.credentialsd.Credentials.xml', ) +addon_app_config.set('APP_ID', 'org.mozilla.firefox') native_messaging_manifest_dir = libdir / 'mozilla' / 'native-messaging-hosts' configure_file(