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@