Skip to content

Commit 9db5b3d

Browse files
authored
Merge pull request #156 from linux-credentials/push-wylwzwozupox
webext: Update extension to use portal frontend API
2 parents 31a614b + bb60bce commit 9db5b3d

3 files changed

Lines changed: 144 additions & 54 deletions

File tree

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,51 @@
11
{
2-
"description": "Helper to integrate credentialsd with the browser",
3-
"manifest_version": 3,
4-
"name": "credentialsd-helper",
5-
"version": "0.1.0",
6-
"icons": {
7-
"48": "icons/logo.svg"
8-
},
9-
10-
"browser_specific_settings": {
11-
"gecko": {
12-
"id": "credentialsd-helper@iinuwa.xyz",
13-
"strict_min_version": "140.0"
14-
}
15-
},
16-
17-
"background": {
18-
"service_worker": "background.js"
19-
},
20-
"content_scripts": [
21-
{
22-
"matches": ["https://webauthn.io/*", "https://demo.yubico.com/*"],
23-
"js": ["content-bridge.js"],
24-
"run_at": "document_start",
25-
"world": "ISOLATED"
2+
"description": "Helper to integrate credentialsd with the browser",
3+
"manifest_version": 3,
4+
"name": "credentialsd-helper",
5+
"version": "0.1.0",
6+
"icons": {
7+
"48": "icons/logo.svg"
268
},
27-
{
28-
"matches": ["https://webauthn.io/*", "https://demo.yubico.com/*"],
29-
"js": ["content-main.js"],
30-
"run_at": "document_start",
31-
"world": "MAIN"
32-
}
33-
],
34-
35-
"action": {
36-
"default_icon": "icons/logo.svg"
37-
},
38-
39-
"permissions": ["nativeMessaging"]
40-
}
9+
"browser_specific_settings": {
10+
"gecko": {
11+
"id": "credentialsd-helper@iinuwa.xyz",
12+
"strict_min_version": "140.0"
13+
}
14+
},
15+
"background": {
16+
"service_worker": "background.js",
17+
"scripts": [
18+
"background.js"
19+
]
20+
},
21+
"content_scripts": [
22+
{
23+
"matches": [
24+
"https://webauthn.io/*",
25+
"https://demo.yubico.com/*"
26+
],
27+
"js": [
28+
"content-bridge.js"
29+
],
30+
"run_at": "document_start",
31+
"world": "ISOLATED"
32+
},
33+
{
34+
"matches": [
35+
"https://webauthn.io/*",
36+
"https://demo.yubico.com/*"
37+
],
38+
"js": [
39+
"content-main.js"
40+
],
41+
"run_at": "document_start",
42+
"world": "MAIN"
43+
}
44+
],
45+
"action": {
46+
"default_icon": "icons/logo.svg"
47+
},
48+
"permissions": [
49+
"nativeMessaging"
50+
]
51+
}

webext/app/credential_manager_shim.py

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
#!/usr/bin/env python3
22

3+
from asyncio import Future
34
import asyncio
45
import base64
56
import codecs
67
from dataclasses import dataclass
78
from enum import Enum
89
import json
910
import logging
11+
import secrets
1012
import signal
1113
import struct
1214
import sys
1315
from typing import Optional
1416

15-
from dbus_next.aio import MessageBus
1617
from dbus_next import Variant
18+
from dbus_next.aio import MessageBus
19+
from dbus_next.constants import MessageType
20+
from dbus_next.message import Message
1721

1822
logging.basicConfig(
1923
filename="/tmp/credential_manager_shim.log", encoding="utf-8", level=logging.DEBUG
2024
)
2125

26+
APP_ID = "@APP_ID@"
2227
DBUS_DOC_FILE = "@DBUS_DOC_FILE@"
2328

2429

@@ -70,6 +75,61 @@ def b64_decode(s) -> bytes:
7075
return base64.urlsafe_b64decode(s + padding)
7176

7277

78+
class PortalRequest[T]:
79+
def __init__(self, token: str, fut: Future):
80+
self.token: str = token
81+
self._fut: Future = fut
82+
83+
async def wait(self) -> T:
84+
return await self._fut
85+
86+
87+
def create_portal_request_message_handler(bus: MessageBus) -> PortalRequest:
88+
loop = asyncio.get_running_loop()
89+
future = loop.create_future()
90+
if not bus.connected or bus.unique_name is None:
91+
raise Exception("Bus is not connected")
92+
unique_name = bus.unique_name[1:].replace(".", "_")
93+
token = secrets.token_hex(16)
94+
object_path = f"/org/freedesktop/portal/desktop/request/{unique_name}/{token}"
95+
96+
def message_handler(msg: Message):
97+
if future.done():
98+
return False
99+
100+
message_matches = (
101+
msg.path == object_path
102+
and msg.message_type == MessageType.SIGNAL
103+
and msg.destination == bus.unique_name
104+
and msg.interface == "org.freedesktop.portal.Request"
105+
and msg.member == "Response"
106+
)
107+
if not message_matches:
108+
return False
109+
110+
[code, value] = msg.body
111+
if code == 0:
112+
future.set_result(value)
113+
elif code == 1:
114+
future.set_exception(Exception("Portal request cancelled"))
115+
raise
116+
elif code == 2 and "error" in value:
117+
future.set_exception(
118+
Exception(f"Portal returned an error: {value['error'].value}")
119+
)
120+
else:
121+
future.set_exception(Exception("Portal returned an unknown error"))
122+
return True
123+
124+
def when_done(_fut):
125+
bus.remove_message_handler(message_handler)
126+
127+
future.add_done_callback(when_done)
128+
bus.add_message_handler(message_handler)
129+
logging.debug(f"Listening for {object_path}")
130+
return PortalRequest(token, future)
131+
132+
73133
class MajorType(Enum):
74134
PositiveInteger = (0,)
75135
NegativeInteger = (1,)
@@ -111,7 +171,7 @@ def _read_value(self, buf):
111171
argument = struct.unpack(">Q", buf[1 : 1 + argument_len])[0]
112172
elif additional_info == 31:
113173
# Indefinite length for types 2-5
114-
argument = None
174+
argument: Optional[int] = None
115175
argument_len = 0
116176
match buf[0] >> 5:
117177
case 0:
@@ -291,23 +351,24 @@ def has_flag(self, flag):
291351

292352
async def create_passkey(interface, options, origin, top_origin):
293353
logging.debug("Creating passkey")
294-
is_same_origin = origin == top_origin
295354
req_json = json.dumps(options)
296355
logging.debug(req_json)
356+
request_event = create_portal_request_message_handler(interface.bus)
297357
req = {
298-
"type": Variant("s", "publicKey"),
299-
"origin": Variant("s", origin),
300-
"is_same_origin": Variant("b", is_same_origin),
301-
"publicKey": Variant("a{sv}", {"request_json": Variant("s", req_json)}),
358+
"handle_token": Variant("s", request_event.token),
359+
"public_key": Variant("s", req_json),
302360
}
361+
if top_origin != origin:
362+
req["top_origin"] = Variant("s", top_origin)
303363
logging.debug("Sending request to D-Bus API")
304-
rsp = await interface.call_create_credential(["", req])
305-
if rsp["type"].value != "public-key":
364+
_rsp = await interface.call_create_credential("", origin, "publicKey", req)
365+
result = await request_event.wait()
366+
if result["type"].value != "public-key":
306367
raise Exception(
307-
f"Invalid credential type received: expected 'public-key', received {rsp['type'.value]}"
368+
f"Invalid credential type received: expected 'public-key', received {result['type'].value}"
308369
)
309370
response_json = json.loads(
310-
rsp["public_key"].value["registration_response_json"].value
371+
result["public_key"].value["registration_response_json"].value
311372
)
312373
attestation = cbor_loads(b64_decode(response_json["response"]["attestationObject"]))
313374
auth_data_view = attestation["authData"]
@@ -339,7 +400,7 @@ async def get_passkey(interface, options, origin, top_origin):
339400
rsp = await interface.call_get_credential(["", req])
340401
if rsp["type"].value != "public-key":
341402
raise Exception(
342-
f"Invalid credential type received: expected 'public-key', received {rsp['type'.value]}"
403+
f"Invalid credential type received: expected 'public-key', received {rsp['type'].value}"
343404
)
344405

345406
response_json = json.loads(
@@ -356,16 +417,31 @@ async def run(cmd, options, origin, top_origin):
356417

357418
logging.info(os.getcwd())
358419

420+
msg = Message(
421+
"org.freedesktop.portal.Desktop",
422+
"/org/freedesktop/portal/desktop",
423+
"org.freedesktop.host.portal.Registry",
424+
"Register",
425+
signature="sa{sv}",
426+
body=[
427+
APP_ID,
428+
{},
429+
],
430+
)
431+
await bus.call(msg)
432+
359433
with open(DBUS_DOC_FILE, "r") as f:
360434
introspection = f.read()
361435

362436
proxy_object = bus.get_proxy_object(
363-
"xyz.iinuwa.credentialsd.Credentials",
364-
"/xyz/iinuwa/credentialsd/Credentials",
437+
"org.freedesktop.portal.Desktop",
438+
"/org/freedesktop/portal/desktop",
365439
introspection,
366440
)
367441

368-
interface = proxy_object.get_interface("xyz.iinuwa.credentialsd.Credentials1")
442+
interface = proxy_object.get_interface(
443+
"org.freedesktop.portal.experimental.Credential"
444+
)
369445
logging.debug(f"Connected to interface at {interface.path}")
370446

371447
if cmd == "create":
@@ -398,6 +474,7 @@ async def run(cmd, options, origin, top_origin):
398474

399475
quit = asyncio.Event()
400476

477+
401478
async def main():
402479
logging.info("starting credential_manager_shim")
403480
while not quit.is_set():
@@ -417,5 +494,6 @@ async def main():
417494
logging.debug("Sent error message")
418495
logging.info("quitting credential_manager_shim")
419496

420-
signal.signal(signal.SIGTERM, lambda _, __ : quit.set())
497+
498+
signal.signal(signal.SIGTERM, lambda _, __: quit.set())
421499
asyncio.run(main())

webext/app/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ addon_app_config.set(
66
datadir / 'credentialsd' / 'xyz.iinuwa.credentialsd.Credentials.xml',
77
)
88

9+
addon_app_config.set('APP_ID', 'org.mozilla.firefox')
910
native_messaging_manifest_dir = libdir / 'mozilla' / 'native-messaging-hosts'
1011

1112
configure_file(

0 commit comments

Comments
 (0)