File webauthn_xdg_portal.patch of Package MozillaFirefox
Index: firefox-141.0.2/Cargo.lock
===================================================================
--- firefox-141.0.2.orig/Cargo.lock
+++ firefox-141.0.2/Cargo.lock
@@ -2654,6 +2654,7 @@ dependencies = [
"wgpu_bindings",
"widget_windows",
"wpf-gpu-raster",
+ "xdg_portal_auth_service",
"xpcom",
]
@@ -7947,6 +7948,26 @@ dependencies = [
]
[[package]]
+name = "xdg_portal_auth_service"
+version = "0.1.0"
+dependencies = [
+ "base64 0.22.1",
+ "cstr",
+ "dbus",
+ "log",
+ "moz_task",
+ "nserror",
+ "nsstring",
+ "rand",
+ "serde",
+ "serde_cbor",
+ "serde_json",
+ "static_prefs",
+ "thin-vec",
+ "xpcom",
+]
+
+[[package]]
name = "zeitstempel"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
Index: firefox-141.0.2/dom/webauthn/AuthrsBridge_ffi.h
===================================================================
--- firefox-141.0.2.orig/dom/webauthn/AuthrsBridge_ffi.h
+++ firefox-141.0.2/dom/webauthn/AuthrsBridge_ffi.h
@@ -20,6 +20,7 @@ nsresult authrs_webauthn_att_obj_constru
const nsTArray<uint8_t>& attestation, bool anonymize,
nsIWebAuthnAttObj** result);
+nsresult xdg_portal_auth_service_if_available(nsIWebAuthnService** result);
} // extern "C"
#endif // mozilla_dom_AuthrsBridge_ffi_h
Index: firefox-141.0.2/dom/webauthn/WebAuthnService.h
===================================================================
--- firefox-141.0.2.orig/dom/webauthn/WebAuthnService.h
+++ firefox-141.0.2/dom/webauthn/WebAuthnService.h
@@ -48,8 +48,17 @@ class WebAuthnService final : public nsI
if (!mPlatformService) {
mPlatformService = mAuthrsService;
}
+#elif defined(MOZ_WIDGET_GTK)
+
+ if (StaticPrefs::security_webauth_webauthn_enable_xdg_portal()) {
+ xdg_portal_auth_service_if_available(getter_AddRefs(mPlatformService));
+ }
+
+ if (!mPlatformService) {
+ mPlatformService = mAuthrsService;
+ }
#else
- mPlatformService = mAuthrsService;
+ mPlatformService = mAuthrsService;
#endif
}
@@ -72,7 +81,7 @@ class WebAuthnService final : public nsI
void ResetLocked(const TransactionStateMutex::AutoLock& aGuard);
nsIWebAuthnService* DefaultService() {
- if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) {
+ if (StaticPrefs::security_webauth_webauthn_enable_softtoken() && !StaticPrefs::security_webauth_webauthn_enable_xdg_portal()) {
return mAuthrsService;
}
return mPlatformService;
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/Cargo.toml
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "xdg_portal_auth_service"
+version = "0.1.0"
+edition = "2021"
+authors = ["Martin Sirringhaus", "John Schanck"]
+
+[dependencies]
+base64 = "^0.22"
+cstr = "0.2"
+log = "0.4"
+moz_task = { path = "../../../xpcom/rust/moz_task" }
+nserror = { path = "../../../xpcom/rust/nserror" }
+nsstring = { path = "../../../xpcom/rust/nsstring" }
+rand = "0.8"
+serde = { version = "1", features = ["derive"] }
+serde_cbor = "0.11"
+serde_json = "1.0"
+static_prefs = { path = "../../../modules/libpref/init/static_prefs" }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+xpcom = { path = "../../../xpcom/rust/xpcom" }
+dbus = "0.6.5"
+
+[features]
+fuzzing = []
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/dbus_controller.rs
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/dbus_controller.rs
@@ -0,0 +1,119 @@
+use super::NS_ERROR_DOM_NOT_ALLOWED_ERR;
+use dbus::{
+ arg::{IterAppend, RefArg, Variant},
+ BusType, Connection, Error, Message,
+};
+use nserror::nsresult;
+use std::collections::HashMap;
+
+use crate::{register::RegisterResult, sign::SignResult};
+
+pub(crate) struct DbusController {
+ conn: Connection,
+}
+
+impl DbusController {
+ const SERVICE_NAME: &'static str = "xyz.iinuwa.credentialsd.Credentials";
+ const PATH: &'static str = "/xyz/iinuwa/credentialsd/Credentials";
+ const INTERFACE: &'static str = "xyz.iinuwa.credentialsd.Credentials1";
+ const CREATE_CREDENTIAL_FUNCTION: &'static str = "CreateCredential";
+ const GET_CREDENTIAL_FUNCTION: &'static str = "GetCredential";
+
+ pub(crate) fn new() -> Result<Self, Error> {
+ let conn = Connection::get_private(BusType::Session)?;
+ Ok(Self { conn })
+ }
+
+ pub(crate) fn is_service_active(&self) -> bool {
+ // Create a proxy for the D-Bus daemon itself.
+ let proxy = self
+ .conn
+ .with_path("org.freedesktop.DBus", "/org/freedesktop/DBus", 5000);
+
+ // First, check if the portal is already running, by calling the "NameHasOwner" method.
+ let m = match proxy.method_call_with_args(
+ &"org.freedesktop.DBus".into(),
+ &"NameHasOwner".into(),
+ |msg| {
+ let mut i = IterAppend::new(msg);
+ i.append(Self::SERVICE_NAME);
+ },
+ ) {
+ Ok(m) => m,
+ Err(e) => {
+ log::info!("Failed to send NameHasOwner via D-Bus: {e:?}");
+ return false;
+ }
+ };
+
+ let has_owner: Option<bool> = m.get1();
+ let is_running = has_owner.unwrap_or_default();
+
+ if is_running {
+ return true;
+ }
+
+ // If it's not running, check if it is activatable
+ let m = match proxy.method_call_with_args(
+ &"org.freedesktop.DBus".into(),
+ &"ListActivatableNames".into(),
+ |_| { /* Nothing to do */ },
+ ) {
+ Ok(m) => m,
+ Err(e) => {
+ log::info!("Failed to send ListActivatableNames via D-Bus: {e:?}");
+ return false;
+ }
+ };
+
+ let activatable_services: Option<Vec<String>> = m.get1();
+ let services = activatable_services.unwrap_or_default();
+ services.iter().any(|name| name == Self::SERVICE_NAME)
+ }
+
+ pub(crate) fn send_message(
+ &self,
+ function: &str,
+ msg: HashMap<String, Variant<Box<dyn RefArg>>>,
+ ) -> Result<Message, Error> {
+ let m = Message::new_method_call(Self::SERVICE_NAME, Self::PATH, Self::INTERFACE, function)
+ // I don't know why this method returns a string instead of a DBUS-Error...
+ // Let's wrap it
+ .map_err(|e| Error::new_custom("new_method_call", &e))?
+ .append1(msg);
+
+ self.conn.send_with_reply_and_block(m, 300000)
+ }
+
+ pub(crate) fn send_create_credential(
+ &self,
+ msg: HashMap<String, Variant<Box<dyn RefArg>>>,
+ ) -> Result<RegisterResult, nsresult> {
+ let resp = self
+ .send_message(Self::CREATE_CREDENTIAL_FUNCTION, msg)
+ .map_err(|e| {
+ log::error!("Failed to send webauthn request via DBUS: {e:?}");
+ NS_ERROR_DOM_NOT_ALLOWED_ERR
+ })?;
+ RegisterResult::parse_from_dbus(resp).map_err(|e| {
+ log::error!("Failed parse webauthn reply from XDG portal: {e:?}");
+ NS_ERROR_DOM_NOT_ALLOWED_ERR
+ })
+ }
+
+ pub(crate) fn send_get_credential(
+ &self,
+ msg: HashMap<String, Variant<Box<dyn RefArg>>>,
+ ) -> Result<SignResult, nsresult> {
+ let resp = self
+ .send_message(Self::GET_CREDENTIAL_FUNCTION, msg)
+ .map_err(|e| {
+ log::error!("Failed to send webauthn request via DBUS: {e:?}");
+ NS_ERROR_DOM_NOT_ALLOWED_ERR
+ })?;
+ SignResult::parse_from_dbus(resp).map_err(|e| {
+ log::error!("Failed parse webauthn reply from XDG portal: {e:?}");
+ NS_ERROR_DOM_NOT_ALLOWED_ERR
+ })
+ }
+}
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/lib.rs
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/lib.rs
@@ -0,0 +1,887 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#[macro_use]
+extern crate xpcom;
+
+use base64::Engine;
+use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD};
+use dbus::arg::{RefArg, Variant};
+use moz_task::RunnableBuilder;
+use nserror::{
+ nsresult, NS_ERROR_DOM_ABORT_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, NS_ERROR_FAILURE,
+ NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_OK,
+};
+use nsstring::{nsACString, nsAString, nsCString, nsString};
+use serde_json::json;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex, MutexGuard};
+use thin_vec::{thin_vec, ThinVec};
+use xpcom::interfaces::{
+ nsICredentialParameters, nsIWebAuthnAutoFillEntry, nsIWebAuthnRegisterArgs,
+ nsIWebAuthnRegisterPromise, nsIWebAuthnService, nsIWebAuthnSignArgs, nsIWebAuthnSignPromise,
+};
+use xpcom::{xpcom_method, RefPtr};
+mod register;
+use register::RegisterPromise;
+pub use register::WebAuthnRegisterResult;
+
+mod sign;
+use sign::{PendingSignArgs, SignPromise};
+
+mod dbus_controller;
+use dbus_controller::DbusController;
+
+// fn authrs_to_nserror(e: AuthenticatorError) -> nsresult {
+// match e {
+// AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_INVALID_STATE_ERR,
+// _ => NS_ERROR_DOM_NOT_ALLOWED_ERR,
+// }
+// }
+
+#[derive(Clone)]
+enum TransactionPromise {
+ Register(RegisterPromise),
+ Sign(SignPromise),
+}
+
+impl TransactionPromise {
+ fn reject(&self, err: nsresult) -> Result<(), nsresult> {
+ match self {
+ TransactionPromise::Register(promise) => promise.resolve_or_reject(Err(err)),
+ TransactionPromise::Sign(promise) => promise.resolve_or_reject(Err(err)),
+ }
+ }
+}
+
+// enum TransactionArgs {
+// Sign(/* timeout */ u64),
+// }
+
+struct TransactionState {
+ tid: u64,
+ browsing_context_id: u64,
+ pending_args: Option<PendingSignArgs>,
+ promise: TransactionPromise,
+}
+
+// XdgPortalAuthService provides an nsIWebAuthnService built on top of authenticator-rs.
+#[xpcom(implement(nsIWebAuthnService), atomic)]
+pub struct XdgPortalAuthService {
+ transaction: Arc<Mutex<Option<TransactionState>>>,
+}
+
+impl XdgPortalAuthService {
+ xpcom_method!(get_is_uvpaa => GetIsUVPAA() -> bool);
+ fn get_is_uvpaa(&self) -> Result<bool, nsresult> {
+ // For now, xdg-portal doesn't offer a platform authenticator
+ Ok(false)
+ }
+
+ xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnRegisterArgs, aPromise: *const nsIWebAuthnRegisterPromise));
+ fn make_credential(
+ &self,
+ tid: u64,
+ browsing_context_id: u64,
+ args: &nsIWebAuthnRegisterArgs,
+ promise: &nsIWebAuthnRegisterPromise,
+ ) -> Result<(), nsresult> {
+ self.reset()?;
+
+ let promise = RegisterPromise(RefPtr::new(promise));
+
+ let mut challenge = ThinVec::new();
+ unsafe { args.GetChallenge(&mut challenge) }.to_result()?;
+ let challenge_str = URL_SAFE_NO_PAD.encode(challenge);
+
+ let mut origin = nsString::new();
+ unsafe { args.GetOrigin(&mut *origin) }.to_result()?;
+
+ let mut relying_party_id = nsString::new();
+ unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?;
+
+ let mut client_data = nsCString::new();
+ unsafe { args.GetClientDataJSON(&mut *client_data) }.to_result()?;
+ let client_data_json: Option<serde_json::Map<_, _>> =
+ serde_json::from_str(&client_data.to_string()).ok();
+ let is_cross_origin = client_data_json
+ .and_then(|o| o.get("crossOrigin").and_then(|c| c.as_bool()))
+ .unwrap_or_default();
+
+ let mut timeout_ms = 0u32;
+ unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?;
+
+ let mut exclude_list_ids = ThinVec::new();
+ unsafe { args.GetExcludeList(&mut exclude_list_ids) }.to_result()?;
+ let exclude_list: Vec<_> = exclude_list_ids
+ .iter()
+ .map(|id| {
+ json!({
+ "id": URL_SAFE_NO_PAD.encode(&id),
+ "type": "public-key",
+ })
+ })
+ .collect();
+
+ let mut relying_party_name = nsString::new();
+ unsafe { args.GetRpName(&mut *relying_party_name) }.to_result()?;
+
+ let mut user_id = ThinVec::new();
+ unsafe { args.GetUserId(&mut user_id) }.to_result()?;
+ let user_id_str = URL_SAFE_NO_PAD.encode(user_id);
+
+ let mut user_name = nsString::new();
+ unsafe { args.GetUserName(&mut *user_name) }.to_result()?;
+
+ let mut user_display_name = nsString::new();
+ unsafe { args.GetUserDisplayName(&mut *user_display_name) }.to_result()?;
+
+ let mut cose_algs = ThinVec::new();
+ unsafe { args.GetCoseAlgs(&mut cose_algs) }.to_result()?;
+ let pub_key_cred_params: Vec<_> = cose_algs
+ .iter()
+ .map(|alg| {
+ json!({
+ "alg": alg,
+ "type": "public-key",
+ })
+ })
+ .collect();
+
+ let mut resident_key = nsString::new();
+ unsafe { args.GetResidentKey(&mut *resident_key) }.to_result()?;
+
+ let mut user_verification = nsString::new();
+ unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?;
+
+ let mut authenticator_attachment = nsString::new();
+ if unsafe { args.GetAuthenticatorAttachment(&mut *authenticator_attachment) }
+ .to_result()
+ .is_ok()
+ {
+ // For now, xdg-portal doesn't offer a platform authenticator
+ if authenticator_attachment.eq("platform") {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+ }
+
+ let mut extensions = serde_json::Map::new();
+ let mut cred_protect_policy_value = nsCString::new();
+ if unsafe { args.GetCredentialProtectionPolicy(&mut *cred_protect_policy_value) }
+ .to_result()
+ .is_ok()
+ {
+ extensions.insert(
+ "credentialProtectionPolicy".to_string(),
+ json!(cred_protect_policy_value.to_string()),
+ );
+ let mut enforce_cred_protect_value = false;
+ unsafe { args.GetEnforceCredentialProtectionPolicy(&mut enforce_cred_protect_value) }
+ .to_result()?;
+ extensions.insert(
+ "enforceCredentialProtectionPolicy".to_string(),
+ json!(enforce_cred_protect_value),
+ );
+ }
+
+ // Not yet supported by Firefox. See bmo#1844448
+ // let mut cred_blob = false;
+ // unsafe { args.GetCredBlob(&mut cred_blob) }.to_result()?;
+
+ let mut cred_props = false;
+ unsafe { args.GetCredProps(&mut cred_props) }.to_result()?;
+ if cred_props {
+ extensions.insert("credProps".to_string(), json!(cred_props));
+ }
+
+ let mut min_pin_length = false;
+ unsafe { args.GetMinPinLength(&mut min_pin_length) }.to_result()?;
+ if min_pin_length {
+ extensions.insert("minPinLength".to_string(), json!(min_pin_length));
+ }
+
+ // Firefox currently doesn't support largeBlob.support == "preferred", only "required"
+ let mut large_blob_support_required = false;
+ if unsafe { args.GetLargeBlobSupportRequired(&mut large_blob_support_required) }
+ .to_result()
+ .is_ok()
+ {
+ if large_blob_support_required {
+ extensions.insert("largeBlob".to_string(), json!({"support": "required"}));
+ }
+ }
+
+ let mut prf = false;
+ if unsafe { args.GetPrf(&mut prf) }.to_result().is_ok() && prf {
+ let mut prf_map = serde_json::Map::new();
+ let mut prf_eval_first: ThinVec<u8> = ThinVec::new();
+ if unsafe { args.GetPrfEvalFirst(&mut prf_eval_first) }
+ .to_result()
+ .is_ok()
+ {
+ prf_map.insert(
+ "first".to_string(),
+ json!(URL_SAFE_NO_PAD.encode(prf_eval_first)),
+ );
+ }
+
+ let mut prf_eval_second: ThinVec<u8> = ThinVec::new();
+ if unsafe { args.GetPrfEvalSecond(&mut prf_eval_second) }
+ .to_result()
+ .is_ok()
+ {
+ prf_map.insert(
+ "second".to_string(),
+ json!(URL_SAFE_NO_PAD.encode(prf_eval_second)),
+ );
+ }
+ extensions.insert("prf".to_string(), json!(prf_map));
+ }
+
+ let mut maybe_hmac_create_secret = false;
+ if unsafe { args.GetHmacCreateSecret(&mut maybe_hmac_create_secret) }
+ .to_result()
+ .is_ok()
+ {
+ extensions.insert(
+ "hmacCreateSecret".to_string(),
+ json!(maybe_hmac_create_secret),
+ );
+ }
+
+ let json_str = json!({
+ "challenge": challenge_str,
+ "rp": {
+ "id": relying_party_id.to_string(),
+ "name": relying_party_name.to_string(),
+ },
+ "user": {
+ "id": user_id_str,
+ "name": user_name.to_string(),
+ "display_name": user_display_name.to_string(),
+ },
+ "timeout": timeout_ms,
+ "excludeCredentials": exclude_list,
+ "pubKeyCredParams": pub_key_cred_params,
+ "extensions": extensions,
+ "authenticatorSelection": {
+ "residentKey": resident_key.to_string(),
+ "userVerification": user_verification.to_string(),
+ },
+ })
+ .to_string();
+
+ let mut guard = self.transaction.lock().unwrap();
+ *guard = Some(TransactionState {
+ tid,
+ browsing_context_id,
+ pending_args: None,
+ promise: TransactionPromise::Register(promise),
+ });
+ // drop the guard here to ensure we don't deadlock if the call to `register()` below
+ // hairpins the state callback.
+ drop(guard);
+
+ let callback_transaction = self.transaction.clone();
+ RunnableBuilder::new("XdgPortalService::MakeCredential::DbusSend", move || {
+ // We need to craft a message like this:
+ // 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)
+ // })
+ // }
+ //
+ // --- Build the inner dictionary for "publicKey" ---
+ // This corresponds to the a{sv} value of the "publicKey" key.
+ let mut public_key_dict = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
+ public_key_dict.insert("request_json".to_string(), Variant(Box::new(json_str)));
+
+ // --- Build the main dictionary payload ---
+ // This is the top-level a{sv} structure.
+ let mut req = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
+ req.insert(
+ "type".to_string(),
+ Variant(Box::new("publicKey".to_string())),
+ );
+ req.insert("origin".to_string(), Variant(Box::new(origin.to_string())));
+ req.insert(
+ "is_same_origin".to_string(),
+ Variant(Box::new(!is_cross_origin)),
+ );
+ req.insert(
+ "publicKey".to_string(),
+ // The inner dictionary must also be wrapped in a Variant
+ Variant(Box::new(public_key_dict)),
+ );
+
+ // Create a new dbus connection and send the message
+ let dbus_controller = match DbusController::new() {
+ Ok(c) => c,
+ Err(e) => {
+ log::warn!("Failed to create DBUS connection {e:?}");
+ return;
+ }
+ };
+ let result = dbus_controller.send_create_credential(req);
+
+ let mut guard = callback_transaction.lock().unwrap();
+ let Some(state) = guard.as_mut() else {
+ return;
+ };
+ if state.tid != tid {
+ return;
+ }
+ let TransactionPromise::Register(ref promise) = state.promise else {
+ return;
+ };
+
+ let _ = promise.resolve_or_reject(result); // TODO: Do correct error mapping here
+ *guard = None;
+ })
+ .may_block(true)
+ .dispatch_background_task()?;
+
+ Ok(())
+ }
+
+ xpcom_method!(set_has_attestation_consent => SetHasAttestationConsent(aTid: u64, aHasConsent: bool));
+ fn set_has_attestation_consent(&self, _tid: u64, _has_consent: bool) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnSignArgs, aPromise: *const nsIWebAuthnSignPromise));
+ fn get_assertion(
+ &self,
+ tid: u64,
+ browsing_context_id: u64,
+ args: &nsIWebAuthnSignArgs,
+ promise: &nsIWebAuthnSignPromise,
+ ) -> Result<(), nsresult> {
+ self.reset()?;
+
+ let promise = SignPromise(RefPtr::new(promise));
+
+ let mut challenge = ThinVec::new();
+ unsafe { args.GetChallenge(&mut challenge) }.to_result()?;
+ let challenge_str = URL_SAFE_NO_PAD.encode(challenge);
+
+ let mut origin = nsString::new();
+ unsafe { args.GetOrigin(&mut *origin) }.to_result()?;
+
+ let mut relying_party_id = nsString::new();
+ unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?;
+
+ let mut client_data = nsCString::new();
+ unsafe { args.GetClientDataJSON(&mut *client_data) }.to_result()?;
+ let client_data_json: Option<serde_json::Map<_, _>> =
+ serde_json::from_str(&client_data.to_string()).ok();
+ let is_cross_origin = client_data_json
+ .and_then(|o| o.get("crossOrigin").and_then(|c| c.as_bool()))
+ .unwrap_or_default();
+
+ let mut timeout_ms = 0u32;
+ unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?;
+
+ let mut allow_list_ids = ThinVec::new();
+ unsafe { args.GetAllowList(&mut allow_list_ids) }.to_result()?;
+ let allow_list: Vec<_> = allow_list_ids.iter().map(|id| id.to_vec()).collect();
+
+ let mut user_verification = nsString::new();
+ unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?;
+
+ // let mut app_id = None;
+ // let mut maybe_app_id = nsString::new();
+ // match unsafe { args.GetAppId(&mut *maybe_app_id) }.to_result() {
+ // Ok(_) => app_id = Some(maybe_app_id.to_string()),
+ // _ => (),
+ // }
+
+ let mut extensions = serde_json::Map::new();
+
+ let mut prf = false;
+ if unsafe { args.GetPrf(&mut prf) }.to_result().is_ok() && prf {
+ let mut prf_map = serde_json::Map::new();
+ let mut prf_eval_map = serde_json::Map::new();
+ let mut prf_eval_first: ThinVec<u8> = ThinVec::new();
+ unsafe { args.GetPrfEvalFirst(&mut prf_eval_first) }.to_result()?;
+ prf_eval_map.insert(
+ "first".to_string(),
+ json!(URL_SAFE_NO_PAD.encode(prf_eval_first)),
+ );
+
+ let mut prf_eval_second: ThinVec<u8> = ThinVec::new();
+ if unsafe { args.GetPrfEvalSecond(&mut prf_eval_second) }
+ .to_result()
+ .is_ok()
+ {
+ prf_eval_map.insert(
+ "second".to_string(),
+ json!(URL_SAFE_NO_PAD.encode(prf_eval_second)),
+ );
+ }
+ prf_map.insert("eval".to_string(), json!(prf_eval_map));
+
+ let mut prf_eval_by_creds = serde_json::Map::new();
+ let mut credential_ids: ThinVec<ThinVec<u8>> = ThinVec::new();
+ let mut eval_by_cred_firsts: ThinVec<ThinVec<u8>> = ThinVec::new();
+ let mut eval_by_cred_second_maybes: ThinVec<bool> = ThinVec::new();
+ let mut eval_by_cred_seconds: ThinVec<ThinVec<u8>> = ThinVec::new();
+ if unsafe { args.GetPrfEvalByCredentialCredentialId(&mut credential_ids) }
+ .to_result()
+ .is_ok()
+ && !credential_ids.is_empty()
+ {
+ // All three functions are guaranteed to return arrays of the same length.
+ // If seconds are missing (because they are optional), then
+ // eval_by_cred_second_maybes[i] will have `false`, and eval_by_cred_seconds[i]
+ // an empty array
+ unsafe { args.GetPrfEvalByCredentialEvalFirst(&mut eval_by_cred_firsts) }
+ .to_result()?;
+ unsafe {
+ args.GetPrfEvalByCredentialEvalSecondMaybe(&mut eval_by_cred_second_maybes)
+ }
+ .to_result()?;
+ unsafe { args.GetPrfEvalByCredentialEvalSecond(&mut eval_by_cred_seconds) }
+ .to_result()?;
+
+ for i in 0..credential_ids.len() {
+ let mut prf_eval_by_cred_map = serde_json::Map::new();
+ prf_eval_by_cred_map.insert(
+ "first".to_string(),
+ json!(URL_SAFE_NO_PAD.encode(&eval_by_cred_firsts[i])),
+ );
+
+ if eval_by_cred_second_maybes[i] {
+ prf_eval_by_cred_map.insert(
+ "second".to_string(),
+ json!(URL_SAFE_NO_PAD.encode(&eval_by_cred_seconds[i])),
+ );
+ }
+ prf_eval_by_creds.insert(
+ URL_SAFE_NO_PAD.encode(&credential_ids[i]),
+ json!(prf_eval_by_cred_map),
+ );
+ }
+ prf_map.insert("evalByCredential".to_string(), json!(prf_eval_by_creds));
+ }
+ extensions.insert("prf".to_string(), json!(prf_map));
+ }
+
+ let mut large_blob_map = serde_json::Map::new();
+ let mut large_blob_read = false;
+ if unsafe { args.GetLargeBlobRead(&mut large_blob_read) }
+ .to_result()
+ .is_ok()
+ {
+ large_blob_map.insert("support".to_string(), json!("required"));
+ }
+ let mut large_blob_write: ThinVec<u8> = ThinVec::new();
+ if unsafe { args.GetLargeBlobWrite(&mut large_blob_write) }
+ .to_result()
+ .is_ok()
+ {
+ large_blob_map.insert(
+ "write".to_string(),
+ json!(URL_SAFE_NO_PAD.encode(&large_blob_write)),
+ );
+ }
+ if !large_blob_map.is_empty() {
+ extensions.insert("largeBlob".to_string(), json!(large_blob_map));
+ }
+
+ // https://w3c.github.io/webauthn/#prf-extension
+ // "The hmac-secret extension provides two PRFs per credential: one which is used for
+ // requests where user verification is performed and another for all other requests.
+ // This extension [PRF] only exposes a single PRF per credential and, when implementing
+ // on top of hmac-secret, that PRF MUST be the one used for when user verification is
+ // performed. This overrides the UserVerificationRequirement if neccessary."
+ if prf && user_verification == "discouraged" {
+ user_verification = "preferred".into();
+ }
+
+ let mut conditionally_mediated = false;
+ unsafe { args.GetConditionallyMediated(&mut conditionally_mediated) }.to_result()?;
+
+ let mut guard = self.transaction.lock().unwrap();
+ *guard = Some(TransactionState {
+ tid,
+ browsing_context_id,
+ pending_args: Some(PendingSignArgs {
+ origin: origin.to_string(),
+ challenge_str,
+ timeout_ms,
+ rp_id: relying_party_id.to_string(),
+ allow_credential_ids: allow_list,
+ user_verification: user_verification.to_string(),
+ extensions,
+ is_same_origin: !is_cross_origin,
+ }),
+ promise: TransactionPromise::Sign(promise),
+ });
+
+ if !conditionally_mediated {
+ // Immediately proceed to the modal UI flow.
+ self.do_get_assertion(None, guard)
+ } else {
+ // Cache the request and wait for the conditional UI to request autofill entries, etc.
+ Ok(())
+ }
+ }
+
+ fn do_get_assertion(
+ &self,
+ mut selected_credential_id: Option<Vec<u8>>,
+ mut guard: MutexGuard<Option<TransactionState>>,
+ ) -> Result<(), nsresult> {
+ let Some(state) = guard.as_mut() else {
+ return Err(NS_ERROR_FAILURE);
+ };
+ let tid = state.tid;
+ let mut pending_args = match state.pending_args.take() {
+ Some(args) => args,
+ None => return Err(NS_ERROR_FAILURE),
+ };
+
+ if let Some(id) = selected_credential_id.take() {
+ if pending_args.allow_credential_ids.is_empty() {
+ pending_args.allow_credential_ids.push(id);
+ } else {
+ // We need to ensure that the selected credential id
+ // was in the original allow_list.
+ pending_args.allow_credential_ids.retain(|i| i == &id);
+ if pending_args.allow_credential_ids.is_empty() {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ }
+
+ let allow_list: Vec<_> = pending_args
+ .allow_credential_ids
+ .iter()
+ .map(|id| {
+ json!({
+ "id": URL_SAFE_NO_PAD.encode(&id),
+ "type": "public-key",
+ })
+ })
+ .collect();
+
+ let json_str = json!({
+ "challenge": pending_args.challenge_str,
+ "timeout": pending_args.timeout_ms,
+ "rpId": pending_args.rp_id,
+ "allowCredentials": allow_list,
+ "userVerification": pending_args.user_verification,
+ // "hints": [],
+ "extensions": pending_args.extensions,
+ })
+ .to_string();
+
+ let callback_transaction = self.transaction.clone();
+ RunnableBuilder::new("XdgPortalService::GetCredential::DbusSend", move || {
+ // We need to craft a message like this:
+ // 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)
+ // })
+ // }
+ // --- Build the inner dictionary for "publicKey" ---
+ // This corresponds to the a{sv} value of the "publicKey" key.
+ let mut public_key_dict = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
+ public_key_dict.insert("request_json".to_string(), Variant(Box::new(json_str)));
+
+ // --- Build the main dictionary payload ---
+ // This is the top-level a{sv} structure.
+ let mut req = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
+ req.insert(
+ "type".to_string(),
+ Variant(Box::new("publicKey".to_string())),
+ );
+ req.insert(
+ "origin".to_string(),
+ Variant(Box::new(pending_args.origin.clone())),
+ );
+ req.insert(
+ "is_same_origin".to_string(),
+ Variant(Box::new(pending_args.is_same_origin)),
+ );
+ req.insert(
+ "publicKey".to_string(),
+ // The inner dictionary must also be wrapped in a Variant
+ Variant(Box::new(public_key_dict)),
+ );
+
+ // Create a new dbus connection and send the message
+ let dbus_controller = match DbusController::new() {
+ Ok(c) => c,
+ Err(e) => {
+ log::warn!("Failed to create DBUS connection {e:?}");
+ return;
+ }
+ };
+ let result = dbus_controller.send_get_credential(req);
+
+ let mut guard = callback_transaction.lock().unwrap();
+ let Some(state) = guard.as_mut() else {
+ return;
+ };
+ if state.tid != tid {
+ return;
+ }
+ let TransactionPromise::Sign(ref promise) = state.promise else {
+ return;
+ };
+ let _ = promise.resolve_or_reject(result); // TODO: Do correct error mapping here
+ *guard = None;
+ })
+ .may_block(true)
+ .dispatch_background_task()?;
+ Ok(())
+ }
+
+ xpcom_method!(has_pending_conditional_get => HasPendingConditionalGet(aBrowsingContextId: u64, aOrigin: *const nsAString) -> u64);
+ fn has_pending_conditional_get(
+ &self,
+ browsing_context_id: u64,
+ origin: &nsAString,
+ ) -> Result<u64, nsresult> {
+ let mut guard = self.transaction.lock().unwrap();
+ let Some(state) = guard.as_mut() else {
+ return Ok(0);
+ };
+ let Some(pending_args) = state.pending_args.as_ref() else {
+ return Ok(0);
+ };
+ if state.browsing_context_id != browsing_context_id {
+ return Ok(0);
+ }
+ if !pending_args.origin.eq(&origin.to_string()) {
+ return Ok(0);
+ }
+ Ok(state.tid)
+ }
+
+ xpcom_method!(get_autofill_entries => GetAutoFillEntries(aTransactionId: u64) -> ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>);
+ fn get_autofill_entries(
+ &self,
+ tid: u64,
+ ) -> Result<ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>, nsresult> {
+ let mut guard = self.transaction.lock().unwrap();
+ let Some(state) = guard.as_mut() else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ if state.tid != tid {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+ let Some(_pending_args) = state.pending_args.as_ref() else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ // We don't currently support silent discovery for credentials via xdg portal, YET.
+ // But we would have everything we need here.
+ return Ok(thin_vec![]);
+ }
+
+ xpcom_method!(select_autofill_entry => SelectAutoFillEntry(aTid: u64, aCredentialId: *const ThinVec<u8>));
+ fn select_autofill_entry(&self, tid: u64, credential_id: &ThinVec<u8>) -> Result<(), nsresult> {
+ // As we don't yet support silent discovery, this would never be called, but
+ // it doesn't hurt to have it implemented already for the future
+ let mut guard = self.transaction.lock().unwrap();
+ let Some(state) = guard.as_mut() else {
+ return Err(NS_ERROR_FAILURE);
+ };
+ if tid != state.tid {
+ return Err(NS_ERROR_FAILURE);
+ }
+ self.do_get_assertion(Some(credential_id.to_vec()), guard)
+ }
+
+ xpcom_method!(resume_conditional_get => ResumeConditionalGet(aTid: u64));
+ fn resume_conditional_get(&self, tid: u64) -> Result<(), nsresult> {
+ let mut guard = self.transaction.lock().unwrap();
+ let Some(state) = guard.as_mut() else {
+ return Err(NS_ERROR_FAILURE);
+ };
+ if tid != state.tid {
+ return Err(NS_ERROR_FAILURE);
+ }
+ self.do_get_assertion(None, guard)
+ }
+
+ // Clears the transaction state if tid matches the ongoing transaction ID.
+ // Returns whether the tid was a match.
+ fn clear_transaction(&self, tid: u64) -> bool {
+ let mut guard = self.transaction.lock().unwrap();
+ let Some(state) = guard.as_ref() else {
+ return true; // workaround for Bug 1864526.
+ };
+ if state.tid != tid {
+ // Ignore the cancellation request if the transaction
+ // ID does not match.
+ return false;
+ }
+ // It's possible that we haven't dispatched the request to the usb_token_manager yet,
+ // e.g. if we're waiting for resume_make_credential. So reject the promise and drop the
+ // state here rather than from the StateCallback
+ let _ = state.promise.reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ *guard = None;
+ true
+ }
+
+ xpcom_method!(cancel => Cancel(aTransactionId: u64));
+ fn cancel(&self, tid: u64) -> Result<(), nsresult> {
+ self.clear_transaction(tid);
+ // TODO: Cancel dbus controller?
+ Ok(())
+ }
+
+ xpcom_method!(reset => Reset());
+ fn reset(&self) -> Result<(), nsresult> {
+ {
+ if let Some(state) = self.transaction.lock().unwrap().take() {
+ // cancel_prompts(state.tid)?;
+ state.promise.reject(NS_ERROR_DOM_ABORT_ERR)?;
+ }
+ } // release the transaction lock so a StateCallback can take it
+ // TODO!;
+ // self.usb_token_manager.lock().unwrap().cancel();
+ Ok(())
+ }
+
+ xpcom_method!(
+ add_virtual_authenticator => AddVirtualAuthenticator(
+ protocol: *const nsACString,
+ transport: *const nsACString,
+ has_resident_key: bool,
+ has_user_verification: bool,
+ is_user_consenting: bool,
+ is_user_verified: bool) -> nsACString
+ );
+ fn add_virtual_authenticator(
+ &self,
+ _protocol: &nsACString,
+ _transport: &nsACString,
+ _has_resident_key: bool,
+ _has_user_verification: bool,
+ _is_user_consenting: bool,
+ _is_user_verified: bool,
+ ) -> Result<nsCString, nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(remove_virtual_authenticator => RemoveVirtualAuthenticator(authenticatorId: *const nsACString));
+ fn remove_virtual_authenticator(&self, _authenticator_id: &nsACString) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ add_credential => AddCredential(
+ authenticatorId: *const nsACString,
+ credential_id: *const nsACString,
+ is_resident_credential: bool,
+ rp_id: *const nsACString,
+ private_key: *const nsACString,
+ user_handle: *const nsACString,
+ sign_count: u32)
+ );
+ fn add_credential(
+ &self,
+ _authenticator_id: &nsACString,
+ _credential_id: &nsACString,
+ _is_resident_credential: bool,
+ _rp_id: &nsACString,
+ _private_key: &nsACString,
+ _user_handle: &nsACString,
+ _sign_count: u32,
+ ) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(get_credentials => GetCredentials(authenticatorId: *const nsACString) -> ThinVec<Option<RefPtr<nsICredentialParameters>>>);
+ fn get_credentials(
+ &self,
+ _authenticator_id: &nsACString,
+ ) -> Result<ThinVec<Option<RefPtr<nsICredentialParameters>>>, nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(remove_credential => RemoveCredential(authenticatorId: *const nsACString, credentialId: *const nsACString));
+ fn remove_credential(
+ &self,
+ _authenticator_id: &nsACString,
+ _credential_id: &nsACString,
+ ) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(remove_all_credentials => RemoveAllCredentials(authenticatorId: *const nsACString));
+ fn remove_all_credentials(&self, _authenticator_id: &nsACString) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(set_user_verified => SetUserVerified(authenticatorId: *const nsACString, isUserVerified: bool));
+ fn set_user_verified(
+ &self,
+ _authenticator_id: &nsACString,
+ _is_user_verified: bool,
+ ) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(listen => Listen());
+ pub(crate) fn listen(&self) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(run_command => RunCommand(c_cmd: *const nsACString));
+ pub fn run_command(&self, _c_cmd: &nsACString) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString));
+ fn pin_callback(&self, _transaction_id: u64, _pin: &nsACString) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(selection_callback => SelectionCallback(aTransactionId: u64, aSelection: u64));
+ fn selection_callback(&self, _transaction_id: u64, _selection: u64) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn xdg_portal_auth_service_if_available(
+ result: *mut *const nsIWebAuthnService,
+) -> nsresult {
+ // This is not send-able to other threads because it contains a raw pointer,
+ // but we don't need to keep it. We can simply create a new one on the fly
+ let dbus_controller = match DbusController::new() {
+ Ok(c) => c,
+ Err(e) => {
+ log::warn!(
+ "Failed to create DBUS connection. Assuming XDG portal is not available. {e:?}"
+ );
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ };
+
+ if !dbus_controller.is_service_active() {
+ log::warn!("Failed to find XDG portal.");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ let wrapper = XdgPortalAuthService::allocate(InitXdgPortalAuthService {
+ transaction: Arc::new(Mutex::new(None)),
+ });
+
+ unsafe {
+ RefPtr::new(wrapper.coerce::<nsIWebAuthnService>()).forget(&mut *result);
+ }
+ NS_OK
+}
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/register.rs
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/register.rs
@@ -0,0 +1,339 @@
+use base64::Engine;
+use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD};
+use dbus::arg::{ArgType, RefArg, Variant};
+use dbus::Message;
+use nserror::{
+ nsresult, NS_ERROR_FAILURE, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_OK,
+};
+use nsstring::{nsACString, nsAString, nsCString, nsString};
+use std::cell::RefCell;
+use std::collections::HashMap;
+use thin_vec::ThinVec;
+use xpcom::interfaces::{nsIWebAuthnRegisterPromise, nsIWebAuthnRegisterResult};
+use xpcom::{xpcom_method, RefPtr};
+
+#[derive(Debug, Clone)]
+pub(crate) struct RegisterExtensionsResult {
+ pub(crate) hmac_create_secret: Option<bool>,
+ pub(crate) large_blob_supported: Option<bool>,
+ pub(crate) prf_enabled: Option<bool>,
+ pub(crate) prf_results_first: Option<Vec<u8>>,
+ pub(crate) prf_results_second: Option<Vec<u8>>,
+ pub(crate) cred_props_rk: Option<bool>,
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct RegisterResult {
+ pub(crate) client_data_json: Option<String>,
+ pub(crate) transports: Option<Vec<String>>,
+ pub(crate) attestation_object: Option<Vec<u8>>,
+ pub(crate) credential_id: Option<Vec<u8>>,
+ pub(crate) extensions: Option<RegisterExtensionsResult>,
+ pub(crate) authenticator_attachment: Option<String>,
+}
+
+impl RegisterResult {
+ pub(crate) fn parse_from_dbus(reply: Message) -> Result<Self, Box<dyn std::error::Error>> {
+ // Extract the main dictionary `a{sv}` from the message.
+ let mut iter = reply.iter_init();
+
+ // dbus-rs is a bit of a mess with respect to parsing nested messages.
+ // We have to iterate over the Dicts (and Dicts are Arrays with DictEntry-values)
+ // and then parse first the key, and second the value, to be able to
+ // re-parse the nested public_key-dict.
+ let mut public_key_dict = None;
+ let mut iter = iter.recurse(ArgType::Array).ok_or("Empty reply")?;
+ // Loop through the key-value pairs of the main dictionary.
+ while iter.arg_type() == ArgType::DictEntry {
+ // To read a key-value pair, recurse into the DictEntry itself.
+ let mut dict_entry_iter = iter
+ .recurse(ArgType::DictEntry)
+ .ok_or("Reply is not a dict")?;
+ let key: String = dict_entry_iter.read()?;
+
+ // Check the "type" field, if it is correct.
+ if key == "type" {
+ let mut dict_entry_iter = dict_entry_iter
+ .recurse(ArgType::Variant)
+ .ok_or("Dict value missing")?;
+ let cred_type: String = dict_entry_iter.read()?;
+ if cred_type != "public-key" {
+ return Err(format!("Invalid credential type: {}", cred_type).into());
+ }
+ } else if key == "public_key" {
+ // The value is a Variant that wraps the target dictionary.
+ // First, recurse into the Variant wrapper.
+ let mut dict_entry_iter = dict_entry_iter
+ .recurse(ArgType::Variant)
+ .ok_or("Dict value missing")?;
+
+ // Now the iterator is positioned at the start of the inner value.
+ // We can now read the entire inner dictionary directly.
+ let public_key: HashMap<String, Variant<Box<dyn RefArg>>> =
+ dict_entry_iter.read()?;
+
+ public_key_dict = Some(public_key);
+ }
+
+ iter.next();
+ }
+ let public_key_dict = public_key_dict.ok_or("Missing public_key dict from response")?;
+
+ // Extract and parse the response JSON string.
+ let reg_response_str = public_key_dict
+ .get("registration_response_json")
+ .and_then(|v| v.as_str())
+ .ok_or("Missing 'registration_response_json'")?;
+
+ let response_json: serde_json::Value = serde_json::from_str(reg_response_str)?;
+
+ let credential_id = response_json["id"]
+ .as_str()
+ .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+ .flatten();
+
+ let authenticator_attachment = response_json["authenticatorAttachment"]
+ .as_str()
+ .map(String::from);
+
+ let extensions = if let Some(extensions) =
+ response_json["clientExtensionResults"].as_object()
+ {
+ let hmac_create_secret = extensions.get("hmacCreateSecret").and_then(|x| x.as_bool());
+ let large_blob_supported = extensions.get("largeBlob").and_then(|b| {
+ b.as_object()
+ .and_then(|blob| blob.get("supported").and_then(|s| s.as_bool()))
+ });
+ let prf_enabled = extensions.get("prf").and_then(|p| {
+ p.as_object()
+ .and_then(|blob| blob.get("enabled").and_then(|s| s.as_bool()))
+ });
+ let prf_results_first = None; // Currently not supported by the spec, but may come in the future
+ let prf_results_second = None; // Currently not supported by the spec, but may come in the future
+ let cred_props_rk = extensions.get("credProps").and_then(|p| {
+ p.as_object()
+ .and_then(|blob| blob.get("rk").and_then(|s| s.as_bool()))
+ });
+ Some(RegisterExtensionsResult {
+ hmac_create_secret,
+ large_blob_supported,
+ prf_enabled,
+ prf_results_first,
+ prf_results_second,
+ cred_props_rk,
+ })
+ } else {
+ None
+ };
+
+ let attestation_object = response_json["response"]["attestationObject"]
+ .as_str()
+ .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+ .flatten();
+
+ let client_data_json = response_json["response"]["clientDataJSON"]
+ .as_str()
+ .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+ .flatten()
+ .map(|b| String::from_utf8(b).ok())
+ .unwrap_or_default();
+
+ let transports = response_json["response"]["transports"]
+ .as_array()
+ .map(|ts| {
+ ts.iter()
+ .filter_map(|t| t.as_str())
+ .map(String::from)
+ .collect()
+ });
+ Ok(Self {
+ client_data_json,
+ transports,
+ attestation_object,
+ credential_id,
+ extensions,
+ authenticator_attachment,
+ })
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct RegisterPromise(pub(crate) RefPtr<nsIWebAuthnRegisterPromise>);
+
+impl RegisterPromise {
+ pub(crate) fn resolve_or_reject(
+ &self,
+ result: Result<RegisterResult, nsresult>,
+ ) -> Result<(), nsresult> {
+ match result {
+ Ok(result) => {
+ let wrapped_result = WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult {
+ result: RefCell::new(result),
+ })
+ .query_interface::<nsIWebAuthnRegisterResult>()
+ .ok_or(NS_ERROR_FAILURE)?;
+ unsafe { self.0.Resolve(wrapped_result.coerce()) };
+ }
+ Err(result) => {
+ unsafe { self.0.Reject(result) };
+ }
+ }
+ Ok(())
+ }
+}
+
+#[xpcom(implement(nsIWebAuthnRegisterResult), atomic)]
+pub struct WebAuthnRegisterResult {
+ // result is only borrowed mutably in `Anonymize`.
+ result: RefCell<RegisterResult>,
+}
+
+impl WebAuthnRegisterResult {
+ xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString);
+ fn get_client_data_json(&self) -> Result<nsCString, nsresult> {
+ let Some(client_data_json) = &self.result.borrow().client_data_json else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(client_data_json.into())
+ }
+
+ xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>);
+ fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(attestation_object) = &self.result.borrow().attestation_object else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(ThinVec::from(attestation_object.as_slice()))
+ }
+
+ xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
+ fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(credential_id) = &self.result.borrow().credential_id else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(credential_id.as_slice().into())
+ }
+
+ xpcom_method!(get_transports => GetTransports() -> ThinVec<nsString>);
+ fn get_transports(&self) -> Result<ThinVec<nsString>, nsresult> {
+ Ok(self
+ .result
+ .borrow()
+ .transports
+ .as_ref()
+ .map(|ts| ts.iter().map(|t| t.into()).collect())
+ .unwrap_or_default())
+ }
+
+ xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool);
+ fn get_hmac_create_secret(&self) -> Result<bool, nsresult> {
+ let Some(hmac_create_secret) = self
+ .result
+ .borrow()
+ .extensions
+ .as_ref()
+ .and_then(|e| e.hmac_create_secret)
+ else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(hmac_create_secret)
+ }
+
+ xpcom_method!(get_large_blob_supported => GetLargeBlobSupported() -> bool);
+ fn get_large_blob_supported(&self) -> Result<bool, nsresult> {
+ let Some(large_blob_supported) = self
+ .result
+ .borrow()
+ .extensions
+ .as_ref()
+ .and_then(|e| e.large_blob_supported)
+ else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(large_blob_supported)
+ }
+
+ xpcom_method!(get_prf_enabled => GetPrfEnabled() -> bool);
+ fn get_prf_enabled(&self) -> Result<bool, nsresult> {
+ let Some(prf_enabled) = self
+ .result
+ .borrow()
+ .extensions
+ .as_ref()
+ .and_then(|e| e.prf_enabled)
+ else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(prf_enabled)
+ }
+
+ xpcom_method!(get_prf_results_first => GetPrfResultsFirst() -> ThinVec<u8>);
+ fn get_prf_results_first(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(extensions) = &self.result.borrow().extensions else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ let Some(prf_results_first) = &extensions.prf_results_first else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ Ok(prf_results_first.as_slice().into())
+ }
+
+ xpcom_method!(get_prf_results_second => GetPrfResultsSecond() -> ThinVec<u8>);
+ fn get_prf_results_second(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(extensions) = &self.result.borrow().extensions else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ let Some(prf_results_second) = &extensions.prf_results_second else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ Ok(prf_results_second.as_slice().into())
+ }
+
+ xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool);
+ fn get_cred_props_rk(&self) -> Result<bool, nsresult> {
+ let Some(cred_props_rk) = self
+ .result
+ .borrow()
+ .extensions
+ .as_ref()
+ .and_then(|e| e.cred_props_rk)
+ else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(cred_props_rk)
+ }
+
+ xpcom_method!(set_cred_props_rk => SetCredPropsRk(aCredPropsRk: bool));
+ fn set_cred_props_rk(&self, _cred_props_rk: bool) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString);
+ fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> {
+ let Some(authenticator_attachment) = &self.result.borrow().authenticator_attachment else {
+ return Err(NS_ERROR_FAILURE);
+ };
+ Ok(authenticator_attachment.into())
+ }
+
+ xpcom_method!(has_identifying_attestation => HasIdentifyingAttestation() -> bool);
+ fn has_identifying_attestation(&self) -> Result<bool, nsresult> {
+ // if self.result.borrow().att_obj.att_stmt != AttestationStatement::None {
+ // return Ok(true);
+ // }
+ // if let Some(data) = &self.result.borrow().att_obj.auth_data.credential_data {
+ // return Ok(data.aaguid != AAGuid::default());
+ // }
+ Ok(false)
+ }
+
+ xpcom_method!(anonymize => Anonymize());
+ fn anonymize(&self) -> Result<nsresult, nsresult> {
+ // self.result.borrow_mut().att_obj.anonymize();
+ // Ok(NS_OK)
+ Err(NS_ERROR_NOT_AVAILABLE)
+ }
+}
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/sign.rs
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/sign.rs
@@ -0,0 +1,364 @@
+use base64::Engine;
+use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD};
+use dbus::arg::{ArgType, RefArg, Variant};
+use dbus::Message;
+use nserror::{
+ nsresult, NS_ERROR_FAILURE, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_OK,
+};
+use nsstring::{nsACString, nsAString, nsCString, nsString};
+use serde_json::{Map, Value};
+use std::collections::HashMap;
+use thin_vec::ThinVec;
+use xpcom::interfaces::{nsIWebAuthnSignPromise, nsIWebAuthnSignResult};
+use xpcom::{xpcom_method, RefPtr};
+
+// Flattened struct of this:
+// let response = json!({
+// "clientDataJSON": URL_SAFE_NO_PAD.encode(self.client_data_json.as_bytes()),
+// "authenticatorData": URL_SAFE_NO_PAD.encode(&self.authenticator_data),
+// "signature": URL_SAFE_NO_PAD.encode(&self.signature),
+// "userHandle": self.user_handle.as_ref().map(|h| URL_SAFE_NO_PAD.encode(h))
+// });
+// let output = json!({
+// "id": id,
+// "rawId": id,
+// "authenticatorAttachment": self.attachment_modality,
+// "response": response,
+// "clientExtensionResults": self.extensions,
+// });
+//
+#[derive(Debug, Clone)]
+pub(crate) struct SignResult {
+ pub(crate) credential_id: Option<Vec<u8>>,
+ pub(crate) authenticator_attachment: Option<String>,
+ pub(crate) client_data_json: Option<String>,
+ pub(crate) authenticator_data: Option<Vec<u8>>,
+ pub(crate) signature: Option<Vec<u8>>,
+ pub(crate) user_handle: Option<Vec<u8>>,
+ pub(crate) extensions: Option<SignExtensionsResult>,
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct SignExtensionsResult {
+ pub(crate) large_blob_value: Option<Vec<u8>>,
+ pub(crate) large_blob_written: Option<bool>,
+ pub(crate) prf_maybe: bool,
+ pub(crate) prf_results_first: Option<Vec<u8>>,
+ pub(crate) prf_results_second: Option<Vec<u8>>,
+}
+
+impl SignResult {
+ pub(crate) fn parse_from_dbus(reply: Message) -> Result<Self, Box<dyn std::error::Error>> {
+ // Extract the main dictionary `a{sv}` from the message.
+ let mut iter = reply.iter_init();
+
+ // dbus-rs is a bit of a mess with respect to parsing nested messages.
+ // We have to iterate over the Dicts (and Dicts are Arrays with DictEntry-values)
+ // and then parse first the key, and second the value, to be able to
+ // re-parse the nested public_key-dict.
+ let mut public_key_dict = None;
+ let mut iter = iter.recurse(ArgType::Array).ok_or("Empty reply")?;
+ // Loop through the key-value pairs of the main dictionary.
+ while iter.arg_type() == ArgType::DictEntry {
+ // To read a key-value pair, recurse into the DictEntry itself.
+ let mut dict_entry_iter = iter
+ .recurse(ArgType::DictEntry)
+ .ok_or("Reply is not a dict")?;
+ let key: String = dict_entry_iter.read()?;
+
+ // Check the "type" field, if it is correct.
+ if key == "type" {
+ let mut dict_entry_iter = dict_entry_iter
+ .recurse(ArgType::Variant)
+ .ok_or("Dict value missing")?;
+ let cred_type: String = dict_entry_iter.read()?;
+ if cred_type != "public-key" {
+ return Err(format!("Invalid credential type: {}", cred_type).into());
+ }
+ } else if key == "public_key" {
+ // The value is a Variant that wraps the target dictionary.
+ // First, recurse into the Variant wrapper.
+ let mut dict_entry_iter = dict_entry_iter
+ .recurse(ArgType::Variant)
+ .ok_or("Dict value missing")?;
+
+ // Now the iterator is positioned at the start of the inner value.
+ // We can now read the entire inner dictionary directly.
+ let public_key: HashMap<String, Variant<Box<dyn RefArg>>> =
+ dict_entry_iter.read()?;
+
+ public_key_dict = Some(public_key);
+ }
+
+ iter.next();
+ }
+ let public_key_dict = public_key_dict.ok_or("Missing public_key dict from response")?;
+
+ // Extract and parse the response JSON string.
+ let auth_response_str = public_key_dict
+ .get("authentication_response_json")
+ .and_then(|v| v.as_str())
+ .ok_or("Missing 'authentication_response_json'")?;
+
+ let response_json: serde_json::Value = serde_json::from_str(auth_response_str)?;
+
+ let credential_id = response_json["id"]
+ .as_str()
+ .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+ .flatten();
+
+ let authenticator_attachment = response_json["authenticatorAttachment"]
+ .as_str()
+ .map(String::from);
+
+ let extensions =
+ if let Some(extensions) = response_json["clientExtensionResults"].as_object() {
+ let mut large_blob_written = None;
+ let mut large_blob_value = None;
+ if let Some(large_blob) = extensions.get("largeBlob") {
+ large_blob_written = large_blob.get("written").and_then(|w| w.as_bool());
+ large_blob_value = large_blob
+ .get("blob")
+ .and_then(|b| b.as_str().and_then(|v| URL_SAFE_NO_PAD.decode(v).ok()));
+ }
+ let mut prf_maybe = false;
+ let mut prf_results_first = None;
+ let mut prf_results_second = None;
+
+ if let Some(prf) = extensions.get("prf") {
+ prf_maybe = true;
+ if let Some(results) = prf.get("results").and_then(|r| r.as_object()) {
+ prf_results_first = results
+ .get("first")
+ .and_then(|f| f.as_str().and_then(|f| URL_SAFE_NO_PAD.decode(f).ok()));
+ prf_results_second = results
+ .get("second")
+ .and_then(|s| s.as_str().and_then(|s| URL_SAFE_NO_PAD.decode(s).ok()));
+ }
+ }
+ Some(SignExtensionsResult {
+ large_blob_value,
+ large_blob_written,
+ prf_maybe,
+ prf_results_first,
+ prf_results_second,
+ })
+ } else {
+ None
+ };
+
+ let client_data_json = response_json["response"]["clientDataJSON"]
+ .as_str()
+ .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+ .flatten()
+ .map(|b| String::from_utf8(b).ok())
+ .unwrap_or_default();
+
+ let authenticator_data = response_json["response"]["authenticatorData"]
+ .as_str()
+ .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+ .flatten();
+
+ let signature = response_json["response"]["signature"]
+ .as_str()
+ .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+ .flatten();
+
+ let user_handle = response_json["response"]["userHandle"]
+ .as_str()
+ .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+ .flatten();
+
+ Ok(Self {
+ client_data_json,
+ credential_id,
+ extensions,
+ authenticator_attachment,
+ authenticator_data,
+ signature,
+ user_handle,
+ })
+ }
+}
+
+#[xpcom(implement(nsIWebAuthnSignResult), atomic)]
+pub struct WebAuthnSignResult {
+ pub result: SignResult,
+}
+
+impl WebAuthnSignResult {
+ xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString);
+ fn get_client_data_json(&self) -> Result<nsCString, nsresult> {
+ let Some(client_data_json) = &self.result.client_data_json else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(client_data_json.into())
+ }
+
+ xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
+ fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(credential_id) = &self.result.credential_id else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(credential_id.as_slice().into())
+ }
+
+ xpcom_method!(get_signature => GetSignature() -> ThinVec<u8>);
+ fn get_signature(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(signature) = &self.result.signature else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(signature.as_slice().into())
+ }
+
+ xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>);
+ fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(authenticator_data) = &self.result.authenticator_data else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(authenticator_data.as_slice().into())
+ }
+
+ xpcom_method!(get_user_handle => GetUserHandle() -> ThinVec<u8>);
+ fn get_user_handle(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(user_handle) = &self.result.user_handle else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(user_handle.as_slice().into())
+ }
+
+ xpcom_method!(get_user_name => GetUserName() -> nsACString);
+ fn get_user_name(&self) -> Result<nsCString, nsresult> {
+ // xdg-portal does not send back the username, only user_handle
+ // let Some(user) = &self.result.assertion.user else {
+ // return Err(NS_ERROR_NOT_AVAILABLE);
+ // };
+ // let Some(name) = &user.name else {
+ // return Err(NS_ERROR_NOT_AVAILABLE);
+ // };
+ // Ok(nsCString::from(name))
+ Err(NS_ERROR_NOT_AVAILABLE)
+ }
+
+ xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString);
+ fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> {
+ let Some(authenticator_attachment) = &self.result.authenticator_attachment else {
+ return Err(NS_ERROR_FAILURE);
+ };
+ Ok(authenticator_attachment.into())
+ }
+
+ xpcom_method!(get_used_app_id => GetUsedAppId() -> bool);
+ fn get_used_app_id(&self) -> Result<bool, nsresult> {
+ // self.result.extensions.app_id.ok_or(NS_ERROR_NOT_AVAILABLE)
+ Err(NS_ERROR_NOT_AVAILABLE)
+ }
+
+ xpcom_method!(set_used_app_id => SetUsedAppId(aUsedAppId: bool));
+ fn set_used_app_id(&self, _used_app_id: bool) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(get_large_blob_value => GetLargeBlobValue() -> ThinVec<u8>);
+ fn get_large_blob_value(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(extensions) = &self.result.extensions else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ let Some(large_blob_value) = &extensions.large_blob_value else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ Ok(large_blob_value.as_slice().into())
+ }
+
+ xpcom_method!(get_large_blob_written => GetLargeBlobWritten() -> bool);
+ fn get_large_blob_written(&self) -> Result<bool, nsresult> {
+ let Some(large_blob_written) = self
+ .result
+ .extensions
+ .as_ref()
+ .map(|e| e.large_blob_written)
+ .flatten()
+ else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(large_blob_written)
+ }
+
+ xpcom_method!(get_prf_maybe => GetPrfMaybe() -> bool);
+ /// Return true if a PRF output is present, even if all attributes are absent.
+ fn get_prf_maybe(&self) -> Result<bool, nsresult> {
+ let Some(prf_maybe) = self.result.extensions.as_ref().map(|e| e.prf_maybe) else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+ Ok(prf_maybe)
+ }
+
+ xpcom_method!(get_prf_results_first => GetPrfResultsFirst() -> ThinVec<u8>);
+ fn get_prf_results_first(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(extensions) = &self.result.extensions else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ let Some(prf_results_first) = &extensions.prf_results_first else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ Ok(prf_results_first.as_slice().into())
+ }
+
+ xpcom_method!(get_prf_results_second => GetPrfResultsSecond() -> ThinVec<u8>);
+ fn get_prf_results_second(&self) -> Result<ThinVec<u8>, nsresult> {
+ let Some(extensions) = &self.result.extensions else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ let Some(prf_results_second) = &extensions.prf_results_second else {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ };
+
+ Ok(prf_results_second.as_slice().into())
+ }
+}
+
+// Used for conditional mediation:
+// The webpage runs a get_assertion()-request, but adds mediation: 'conditional' to it.
+// If this is the case, we cache the incoming request and execute it at a later point
+// in time, when the user selects it, after clicking into the input-field.
+#[derive(Debug, Clone)]
+pub(crate) struct PendingSignArgs {
+ pub(crate) origin: String,
+ /// base64 encoded challenge
+ pub(crate) challenge_str: String,
+ pub(crate) timeout_ms: u32,
+ pub(crate) rp_id: String,
+ pub(crate) allow_credential_ids: Vec<Vec<u8>>,
+ pub(crate) user_verification: String,
+ pub(crate) extensions: Map<String, Value>,
+ pub(crate) is_same_origin: bool,
+}
+
+#[derive(Clone)]
+pub(crate) struct SignPromise(pub(crate) RefPtr<nsIWebAuthnSignPromise>);
+
+impl SignPromise {
+ pub(crate) fn resolve_or_reject(
+ &self,
+ result: Result<SignResult, nsresult>,
+ ) -> Result<(), nsresult> {
+ match result {
+ Ok(result) => {
+ let wrapped_result =
+ WebAuthnSignResult::allocate(InitWebAuthnSignResult { result })
+ .query_interface::<nsIWebAuthnSignResult>()
+ .ok_or(NS_ERROR_FAILURE)?;
+ unsafe { self.0.Resolve(wrapped_result.coerce()) };
+ }
+ Err(result) => {
+ unsafe { self.0.Reject(result) };
+ }
+ }
+ Ok(())
+ }
+}
Index: firefox-141.0.2/modules/libpref/init/StaticPrefList.yaml
===================================================================
--- firefox-141.0.2.orig/modules/libpref/init/StaticPrefList.yaml
+++ firefox-141.0.2/modules/libpref/init/StaticPrefList.yaml
@@ -16693,6 +16693,15 @@
mirror: always
rust: true
+#if defined(MOZ_WIDGET_GTK)
+# Dispatch WebAuthn requests to linux xdg portal
+- name: security.webauth.webauthn_enable_xdg_portal
+ type: RelaxedAtomicBool
+ value: true
+ mirror: always
+ rust: true
+#endif
+
# residentKey support when using Android platform API
- name: security.webauthn.webauthn_enable_android_fido2.residentkey
type: RelaxedAtomicBool
Index: firefox-141.0.2/toolkit/library/rust/shared/Cargo.toml
===================================================================
--- firefox-141.0.2.orig/toolkit/library/rust/shared/Cargo.toml
+++ firefox-141.0.2/toolkit/library/rust/shared/Cargo.toml
@@ -124,6 +124,9 @@ webext-storage = "0.1"
detect_win32k_conflicts = { path = "../../../xre/detect_win32k_conflicts" }
widget_windows = { path = "../../../../widget/windows/rust" }
+[target.'cfg(target_os = "linux")'.dependencies]
+xdg_portal_auth_service = { path = "../../../../dom/webauthn/linux_xdg_portal_service" }
+
[features]
default = []
moz_memory = ["mozglue-static/moz_memory"]
Index: firefox-141.0.2/toolkit/library/rust/shared/lib.rs
===================================================================
--- firefox-141.0.2.orig/toolkit/library/rust/shared/lib.rs
+++ firefox-141.0.2/toolkit/library/rust/shared/lib.rs
@@ -52,6 +52,7 @@ extern crate signature_cache;
extern crate static_prefs;
extern crate storage;
extern crate webrender_bindings;
+extern crate xdg_portal_auth_service;
extern crate xpcom;
extern crate audio_thread_priority;