1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

PM-11455: Trigger sync when user enables OS setting (#14127)

* 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 <dani-garcia@users.noreply.github.com>

* clean up

* Don't add empty callbacks

* Removed comment

---------

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
This commit is contained in:
Anders Åberg
2025-04-08 19:07:46 +02:00
committed by GitHub
parent 92e9dca6b4
commit d902a0d953
8 changed files with 137 additions and 22 deletions

View File

@@ -68,6 +68,13 @@ pub struct MacOSProviderClient {
connection_status: Arc<std::sync::atomic::AtomicBool>,
}
#[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<dyn PreparePasskeyRegistrationCallback>,
) {
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<dyn PreparePasskeyAssertionCallback>,
) {
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<dyn PreparePasskeyAssertionCallback>,
) {
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<dyn Callback>,
callback: Option<Box<dyn Callback>>,
) {
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
)));
}
}
}
}

View File

@@ -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<number>
@@ -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<IpcServer>
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<IpcServer>
/** Return the path to the IPC server. */
getPath(): string
/** Stop the IPC server. */

View File

@@ -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<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(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::<PasskeyMessage<NativeStatus>>(&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}");
}
}

View File

@@ -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()

View File

@@ -9,10 +9,10 @@
<key>ASCredentialProviderExtensionCapabilities</key>
<dict>
<key>ProvidesPasskeys</key>
<true/>
<true />
<key>ShowsConfigurationUI</key>
<true />
</dict>
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
<false/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.authentication-services-credential-provider-ui</string>
@@ -20,4 +20,4 @@
<string>$(PRODUCT_MODULE_NAME).CredentialProviderViewController</string>
</dict>
</dict>
</plist>
</plist>

View File

@@ -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);
},
);
},
};

View File

@@ -77,6 +77,20 @@ export class DesktopAutofillService implements OnDestroy {
this.listenIpc();
}
async adHocSync(): Promise<any> {
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(

View File

@@ -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) => {