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:
@@ -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
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
apps/desktop/desktop_native/napi/index.d.ts
vendored
6
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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. */
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user