From d902a0d953fbe2c49d2c28544d07ada6fc02870c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 8 Apr 2025 19:07:46 +0200 Subject: [PATCH] PM-11455: Trigger sync when user enables OS setting (#14127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implemented a SendNativeStatus command This allows reporting status or asking the electron app to do something. * fmt * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Daniel García * clean up * Don't add empty callbacks * Removed comment --------- Co-authored-by: Daniel García --- .../desktop_native/macos_provider/src/lib.rs | 50 +++++++++++++------ apps/desktop/desktop_native/napi/index.d.ts | 6 ++- apps/desktop/desktop_native/napi/src/lib.rs | 34 ++++++++++++- .../CredentialProviderViewController.swift | 5 ++ .../macos/autofill-extension/Info.plist | 8 +-- apps/desktop/src/autofill/preload.ts | 19 +++++++ .../services/desktop-autofill.service.ts | 23 +++++++++ .../main/autofill/native-autofill.main.ts | 14 ++++++ 8 files changed, 137 insertions(+), 22 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 26409f14a96..1a70b49b69e 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -68,6 +68,13 @@ pub struct MacOSProviderClient { connection_status: Arc, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeStatus { + key: String, + value: String, +} + #[uniffi::export] impl MacOSProviderClient { #[uniffi::constructor] @@ -81,7 +88,7 @@ impl MacOSProviderClient { let client = MacOSProviderClient { to_server_send, - response_callbacks_counter: AtomicU32::new(0), + response_callbacks_counter: AtomicU32::new(1), // 0 is reserved for no callback response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)), }; @@ -149,12 +156,17 @@ impl MacOSProviderClient { client } + pub fn send_native_status(&self, key: String, value: String) { + let status = NativeStatus { key, value }; + self.send_message(status, None); + } + pub fn prepare_passkey_registration( &self, request: PasskeyRegistrationRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); } pub fn prepare_passkey_assertion( @@ -162,7 +174,7 @@ impl MacOSProviderClient { request: PasskeyAssertionRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); } pub fn prepare_passkey_assertion_without_user_interface( @@ -170,7 +182,7 @@ impl MacOSProviderClient { request: PasskeyAssertionWithoutUserInterfaceRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); } pub fn get_connection_status(&self) -> ConnectionStatus { @@ -219,9 +231,13 @@ impl MacOSProviderClient { fn send_message( &self, message: impl Serialize + DeserializeOwned, - callback: Box, + callback: Option>, ) { - let sequence_number = self.add_callback(callback); + let sequence_number = if let Some(cb) = callback { + self.add_callback(cb) + } else { + 0 // Special value indicating "no callback" + }; let message = serde_json::to_string(&SerializedMessage::Message { sequence_number, @@ -231,16 +247,18 @@ impl MacOSProviderClient { if let Err(e) = self.to_server_send.blocking_send(message) { // Make sure we remove the callback from the queue if we can't send the message - if let Some((cb, _)) = self - .response_callbacks_queue - .lock() - .unwrap() - .remove(&sequence_number) - { - cb.error(BitwardenError::Internal(format!( - "Error sending message: {}", - e - ))); + if sequence_number != 0 { + if let Some((cb, _)) = self + .response_callbacks_queue + .lock() + .unwrap() + .remove(&sequence_number) + { + cb.error(BitwardenError::Internal(format!( + "Error sending message: {}", + e + ))); + } } } } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index ca1fe29e254..0d26be18bdb 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -154,6 +154,10 @@ export declare namespace autofill { userVerification: UserVerification windowXy: Position } + export interface NativeStatus { + key: string + value: string + } export interface PasskeyAssertionResponse { rpId: string userHandle: Array @@ -169,7 +173,7 @@ export declare namespace autofill { * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. * @param callback This function will be called whenever a message is received from a client. */ - static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void): Promise + static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void, nativeStatusCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void): Promise /** Return the path to the IPC server. */ getPath(): string /** Stop the IPC server. */ diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index f02be2b27b6..28eaa61df17 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -572,6 +572,14 @@ pub mod autofill { pub window_xy: Position, } + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct NativeStatus { + pub key: String, + pub value: String, + } + #[napi(object)] #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -621,6 +629,13 @@ pub mod autofill { (u32, u32, PasskeyAssertionWithoutUserInterfaceRequest), ErrorStrategy::CalleeHandled, >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" + )] + native_status_callback: ThreadsafeFunction< + (u32, u32, NativeStatus), + ErrorStrategy::CalleeHandled, + >, ) -> napi::Result { let (send, mut recv) = tokio::sync::mpsc::channel::(32); tokio::spawn(async move { @@ -689,7 +704,24 @@ pub mod autofill { continue; } Err(e) => { - println!("[ERROR] Error deserializing message2: {e}"); + println!( + "[ERROR] Error deserializing registration request: {e}" + ); + } + } + + match serde_json::from_str::>(&message) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + native_status_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + println!("[ERROR] Error deserializing native status: {e}"); } } diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index bf541e233d9..0e6c0fc7023 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -155,6 +155,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController { view.isHidden = true self.view = view } + + override func prepareInterfaceForExtensionConfiguration() { + client.sendNativeStatus(key: "request-sync", value: "") + self.extensionContext.completeExtensionConfigurationRequest() + } override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) { let timeoutTimer = createTimer() diff --git a/apps/desktop/macos/autofill-extension/Info.plist b/apps/desktop/macos/autofill-extension/Info.plist index 539cfa35b9d..a8ae0f021ad 100644 --- a/apps/desktop/macos/autofill-extension/Info.plist +++ b/apps/desktop/macos/autofill-extension/Info.plist @@ -9,10 +9,10 @@ ASCredentialProviderExtensionCapabilities ProvidesPasskeys - + + ShowsConfigurationUI + - ASCredentialProviderExtensionShowsConfigurationUI - NSExtensionPointIdentifier com.apple.authentication-services-credential-provider-ui @@ -20,4 +20,4 @@ $(PRODUCT_MODULE_NAME).CredentialProviderViewController - + \ No newline at end of file diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 2c006b5c928..537a4fdaf4e 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -127,4 +127,23 @@ export default { }, ); }, + + listenNativeStatus: ( + fn: (clientId: number, sequenceNumber: number, status: { key: string; value: string }) => void, + ) => { + ipcRenderer.on( + "autofill.nativeStatus", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + status: { key: string; value: string }; + }, + ) => { + const { clientId, sequenceNumber, status } = data; + fn(clientId, sequenceNumber, status); + }, + ); + }, }; diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index b7d9894907e..b72c6974c97 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -77,6 +77,20 @@ export class DesktopAutofillService implements OnDestroy { this.listenIpc(); } + async adHocSync(): Promise { + this.logService.info("Performing AdHoc sync"); + const account = await firstValueFrom(this.accountService.activeAccount$); + const userId = account?.id; + + if (!userId) { + throw new Error("No active user found"); + } + + const cipherViewMap = await firstValueFrom(this.cipherService.cipherViews$(userId)); + this.logService.info("Performing AdHoc sync", Object.values(cipherViewMap ?? [])); + await this.sync(Object.values(cipherViewMap ?? [])); + } + /** Give metadata about all available credentials in the users vault */ async sync(cipherViews: CipherView[]) { const status = await this.status(); @@ -245,6 +259,15 @@ export class DesktopAutofillService implements OnDestroy { callback(error, null); } }); + + // Listen for native status messages + ipc.autofill.listenNativeStatus(async (clientId, sequenceNumber, status) => { + this.logService.info("Received native status", status.key, status.value); + if (status.key === "request-sync") { + // perform ad-hoc sync + await this.adHocSync(); + } + }); } private convertRegistrationRequest( diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index f66eea180cf..ae901d75c1d 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -75,6 +75,20 @@ export class NativeAutofillMain { request, }); }, + // NativeStatusCallback + (error, clientId, sequenceNumber, status) => { + if (error) { + this.logService.error("autofill.IpcServer.nativeStatus", error); + this.ipcServer.completeError(clientId, sequenceNumber, String(error)); + return; + } + this.logService.info("Received native status", status); + this.windowMain.win.webContents.send("autofill.nativeStatus", { + clientId, + sequenceNumber, + status, + }); + }, ); ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {