diff --git a/contrib/xyz.iinuwa.credentials.CredentialManager.xml b/contrib/xyz.iinuwa.credentials.CredentialManager.xml
index 534c798..056fcd8 100644
--- a/contrib/xyz.iinuwa.credentials.CredentialManager.xml
+++ b/contrib/xyz.iinuwa.credentials.CredentialManager.xml
@@ -16,6 +16,9 @@
+
+
+
diff --git a/webext/add-on/background.js b/webext/add-on/background.js
index 9c22889..863ad50 100644
--- a/webext/add-on/background.js
+++ b/webext/add-on/background.js
@@ -36,12 +36,16 @@ function rcvFromContent(msg) {
// const isCrossOrigin = origin === topOrigin
// const isTopLevel = contentPort.sender.frameId === 0;
-
- 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 })
+ 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 })
+ } else {
+ console.debug("background script received message without arguments, passing onto native app")
+ nativePort.postMessage({ requestId, cmd, origin, topOrigin })
+ }
}
function rcvFromNative(msg) {
diff --git a/webext/add-on/content.js b/webext/add-on/content.js
index 2ff5111..7ddcb6a 100644
--- a/webext/add-on/content.js
+++ b/webext/add-on/content.js
@@ -14,6 +14,11 @@ exportFunction(createCredential, navigator.credentials, { defineAs: "create"})
exportFunction(getCredential, navigator.credentials, { defineAs: "get"})
+if (window.PublicKeyCredential) {
+ console.log("overriding PublicKeyCredential.getClientCapabilities() in content script");
+ exportFunction(getClientCapabilities, PublicKeyCredential, { defineAs: "getClientCapabilities"})
+}
+
function startRequest() {
const requestId = requestCounter++;
const {promise, resolve, reject } = window.Promise.withResolvers();
@@ -182,3 +187,10 @@ function getCredential(request) {
webauthnPort.postMessage({ requestId, cmd: 'get', options, })
return promise.then(cloneCredentialResponse)
};
+
+function getClientCapabilities() {
+ console.log("forwarding getClientCapabilities call from content script to background script")
+ const { requestId, promise } = startRequest();
+ webauthnPort.postMessage({ requestId, cmd: 'getClientCapabilities', })
+ return promise.then((capabilities) => cloneInto(capabilities, window))
+};
diff --git a/webext/app/credential_manager_shim.py b/webext/app/credential_manager_shim.py
index 2f903bf..109cc55 100755
--- a/webext/app/credential_manager_shim.py
+++ b/webext/app/credential_manager_shim.py
@@ -343,7 +343,7 @@ async def run(cmd, options, origin, top_origin):
interface = proxy_object.get_interface(
'xyz.iinuwa.credentials.CredentialManagerUi1')
- logging.debug(f"COnnected to interface at {interface.path}")
+ logging.debug(f"Connected to interface at {interface.path}")
if cmd == 'create':
if 'publicKey' in options:
@@ -355,6 +355,12 @@ async def run(cmd, options, origin, top_origin):
return await get_passkey(interface, options['publicKey'], origin, top_origin)
else:
raise Exception(f"Could not get unknown credential type: {options.keys()[0]}")
+ elif cmd == 'getClientCapabilities':
+ rsp = await interface.call_get_client_capabilities()
+ response = {}
+ for name, val in rsp.items():
+ response[name] = val.value
+ return response
else:
raise Exception(f"unknown cmd: {cmd}")
@@ -366,7 +372,10 @@ async def run(cmd, options, origin, top_origin):
request_id = receivedMessage['requestId']
try:
cmd = receivedMessage['cmd']
- options = receivedMessage['options']
+
+ options = None
+ if 'options' in receivedMessage:
+ options = receivedMessage['options']
origin = receivedMessage['origin']
top_origin = receivedMessage['topOrigin']
loop = asyncio.get_event_loop()
diff --git a/xyz-iinuwa-credential-manager-portal-gtk/.gitignore b/xyz-iinuwa-credential-manager-portal-gtk/.gitignore
index f0c26c5..95d99ef 100644
--- a/xyz-iinuwa-credential-manager-portal-gtk/.gitignore
+++ b/xyz-iinuwa-credential-manager-portal-gtk/.gitignore
@@ -1 +1,2 @@
src/config.rs
+tests/config/mod.rs
\ No newline at end of file
diff --git a/xyz-iinuwa-credential-manager-portal-gtk/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/meson.build
index a39e43f..50339cd 100644
--- a/xyz-iinuwa-credential-manager-portal-gtk/meson.build
+++ b/xyz-iinuwa-credential-manager-portal-gtk/meson.build
@@ -65,6 +65,7 @@ endif
subdir('data')
subdir('po')
subdir('src')
+subdir('tests')
gnome.post_install(
gtk_update_icon_cache: true,
diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs
index 2ba2572..a43b006 100644
--- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs
+++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs
@@ -241,6 +241,20 @@ impl CredentialManager {
))
}
}
+
+ async fn get_client_capabilities(&self) -> fdo::Result {
+ Ok(GetClientCapabilitiesResponse {
+ conditional_create: false,
+ conditional_get: false,
+ hybrid_transport: false,
+ passkey_platform_authenticator: false,
+ user_verifying_platform_authenticator: false,
+ related_origins: false,
+ signal_all_accepted_credentials: false,
+ signal_current_user_details: false,
+ signal_unknown_credential: false,
+ })
+ }
}
async fn create_password(
@@ -838,6 +852,20 @@ impl From for GetCredentialResponse {
}
}
+#[derive(SerializeDict, Type)]
+#[zvariant(signature = "dict", rename_all = "camelCase")]
+pub struct GetClientCapabilitiesResponse {
+ conditional_create: bool,
+ conditional_get: bool,
+ hybrid_transport: bool,
+ passkey_platform_authenticator: bool,
+ user_verifying_platform_authenticator: bool,
+ related_origins: bool,
+ signal_all_accepted_credentials: bool,
+ signal_current_user_details: bool,
+ signal_unknown_credential: bool,
+}
+
fn format_client_data_json(
op: Operation,
challenge: &str,
diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs
index 6b401a9..8f49c64 100644
--- a/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs
+++ b/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs
@@ -715,7 +715,7 @@ mod test {
PublicKeyCredentialParameters, PublicKeyCredentialType,
};
- use super::sign_attestation;
+ use super::{create_attested_credential_data, create_authenticator_data, sign_attestation};
#[test]
fn test_attestation() {
diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in b/xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in
new file mode 100644
index 0000000..b4e7d96
--- /dev/null
+++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in
@@ -0,0 +1,4 @@
+pub const SERVICE_DIR: &'static str = @SERVICE_DIR@;
+pub const SERVICE_NAME: &'static str = "xyz.iinuwa.credentials.CredentialManagerUi";
+pub const PATH: &'static str = "/xyz/iinuwa/credentials/CredentialManagerUi";
+pub const INTERFACE: &'static str = "xyz.iinuwa.credentials.CredentialManagerUi1";
diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs
new file mode 100644
index 0000000..b65918c
--- /dev/null
+++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs
@@ -0,0 +1,70 @@
+mod config;
+
+use std::collections::HashMap;
+
+use client::DbusClient;
+use zbus::zvariant::Value;
+
+#[test]
+fn test_client_capabilities() {
+ let client = DbusClient::new();
+ let msg = client.call_method("GetClientCapabilities", &()).unwrap();
+ let body = msg.body();
+ let rsp: HashMap = body.deserialize().unwrap();
+
+ let capabilities = HashMap::from([
+ ("conditionalCreate", false),
+ ("conditionalGet", false),
+ ("hybridTransport", false),
+ ("passkeyPlatformAuthenticator", false),
+ ("userVerifyingPlatformAuthenticator", false),
+ ("relatedOrigins", false),
+ ("signalAllAcceptedCredentials", false),
+ ("signalCurrentUserDetails", false),
+ ("signalUnknownCredential", false),
+ ]);
+ for (key, expected) in capabilities.iter() {
+ let value: &Value = rsp.get(*key).unwrap();
+ assert_eq!(*expected, value.try_into().unwrap());
+ }
+}
+
+mod client {
+ use crate::config::{INTERFACE, PATH, SERVICE_DIR, SERVICE_NAME};
+ use gtk::gio::{TestDBus, TestDBusFlags};
+ use serde::Serialize;
+ use zbus::{blocking::Connection, zvariant::DynamicType, Message};
+
+ fn init_test_dbus() -> TestDBus {
+ let dbus = TestDBus::new(TestDBusFlags::NONE);
+
+ // assumes this runs in root of Cargo project.
+ let current_dir = std::env::current_dir().unwrap();
+ let service_dir = current_dir.join(SERVICE_DIR);
+ println!("{:?}", service_dir);
+ dbus.add_service_dir(service_dir.to_str().unwrap());
+
+ dbus.up();
+ dbus
+ }
+
+ pub(super) struct DbusClient {
+ _bus: TestDBus,
+ }
+
+ impl DbusClient {
+ pub fn new() -> Self {
+ Self {
+ _bus: init_test_dbus(),
+ }
+ }
+
+ pub fn call_method(&self, method_name: &str, body: &B) -> zbus::Result
+ where
+ B: Serialize + DynamicType,
+ {
+ let connection = Connection::session().unwrap();
+ connection.call_method(Some(SERVICE_NAME), PATH, Some(INTERFACE), method_name, body)
+ }
+ }
+}
diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build
new file mode 100644
index 0000000..549b598
--- /dev/null
+++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build
@@ -0,0 +1,43 @@
+test_config = configuration_data()
+test_config.set_quoted(
+ 'SERVICE_DIR',
+ meson.project_build_root() / backend_executable_name / 'tests',
+)
+test_config.set(
+ 'DBUS_EXECUTABLE',
+ meson.project_build_root() / backend_executable_name / 'src' / backend_executable_name,
+)
+configure_file(
+ input: 'config' / 'mod.rs.in',
+ output: 'config.rs',
+ configuration: test_config,
+)
+
+# Copy the config output to the source directory.
+run_command(
+ 'cp',
+ meson.project_build_root() / backend_executable_name / 'tests' / 'config.rs',
+ meson.project_source_root() / backend_executable_name / 'tests' / 'config' / 'mod.rs',
+ check: true,
+)
+
+configure_file(
+ input: 'services' / 'xyz.iinuwa.CredentialManagerUi.service.in',
+ output: 'xyz.iinuwa.CredentialManagerUi.service',
+ configuration: test_config,
+)
+
+test(
+ 'cargo dbus tests',
+ cargo,
+ env: [cargo_env],
+ args: [
+ 'test',
+ '--test', 'dbus',
+ '--no-fail-fast', cargo_options,
+ '--',
+ '--nocapture',
+ ],
+ protocol: 'exitcode',
+ verbose: true,
+)
\ No newline at end of file
diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in b/xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in
new file mode 100644
index 0000000..9fc84ea
--- /dev/null
+++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=xyz.iinuwa.credentials.CredentialManagerUi
+Exec=@DBUS_EXECUTABLE@