From 5f5f6a75b86614aa7f2a59ca8ff099a5717f66e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 17 Feb 2025 15:48:48 +0100 Subject: [PATCH 1/9] Cancel after 90s --- .../autofill-extension/CredentialProviderViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 8c2aa15550f..5c8756ec9d5 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -177,10 +177,10 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } // Schedule the timeout - DispatchQueue.main.asyncAfter(deadline: .now() + 20, execute: timeoutTimer) + DispatchQueue.main.asyncAfter(deadline: .now() + 90, execute: timeoutTimer) // Create a timer to show UI after 10 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 90) { [weak self] in guard let self = self else { return } // Configure and show UI elements for manual cancellation self.configureTimeoutUI() From 578802e465134970e2b663766682106d5a809781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 17 Feb 2025 16:10:16 +0100 Subject: [PATCH 2/9] Introduced observable --- .../CredentialProviderViewController.swift | 16 +++++----- .../components/fido2placeholder.component.ts | 11 ++----- .../desktop-fido2-user-interface.service.ts | 31 +++++++------------ 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 5c8756ec9d5..54102da2043 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -169,22 +169,22 @@ class CredentialProviderViewController: ASCredentialProviderViewController { override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) { logger.log("[autofill-extension] prepareInterface") - // Create a timer for 20 second timeout + // Create a timer for 90 second timeout let timeoutTimer = DispatchWorkItem { [weak self] in guard let self = self else { return } - logger.log("[autofill-extension] Registration timed out after 20 seconds") + logger.log("[autofill-extension] Registration timed out after 90 seconds") self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Registration timed out")) } // Schedule the timeout DispatchQueue.main.asyncAfter(deadline: .now() + 90, execute: timeoutTimer) - // Create a timer to show UI after 10 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 90) { [weak self] in - guard let self = self else { return } - // Configure and show UI elements for manual cancellation - self.configureTimeoutUI() - } + // // Create a timer to show UI after 10 seconds + // DispatchQueue.main.asyncAfter(deadline: .now() + 90) { [weak self] in + // guard let self = self else { return } + // // Configure and show UI elements for manual cancellation + // self.configureTimeoutUI() + // } if let request = registrationRequest as? ASPasskeyCredentialRequest { if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity { diff --git a/apps/desktop/src/app/components/fido2placeholder.component.ts b/apps/desktop/src/app/components/fido2placeholder.component.ts index c2876cb6b59..84558ac27dc 100644 --- a/apps/desktop/src/app/components/fido2placeholder.component.ts +++ b/apps/desktop/src/app/components/fido2placeholder.component.ts @@ -56,7 +56,7 @@ import { DesktopSettingsService } from "../../platform/services/desktop-settings export class Fido2PlaceholderComponent implements OnInit, OnDestroy { session?: DesktopFido2UserInterfaceSession = null; private cipherIdsSubject = new BehaviorSubject([]); - cipherIds$: Observable = this.cipherIdsSubject.asObservable(); + cipherIds$: Observable; constructor( private readonly desktopSettingsService: DesktopSettingsService, @@ -64,14 +64,9 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy { private readonly router: Router, ) {} - async ngOnInit(): Promise { + ngOnInit() { this.session = this.fido2UserInterfaceService.getCurrentSession(); - - const cipherIds = await this.session?.getAvailableCipherIds(); - this.cipherIdsSubject.next(cipherIds || []); - - // eslint-disable-next-line no-console - console.log("Available cipher IDs", cipherIds); + this.cipherIds$ = this.session?.availableCipherIds$; } async chooseCipher(cipherId: string) { diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index b331ef03d57..018f3fe5cb5 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -6,8 +6,8 @@ import { Subject, filter, take, - timeout, BehaviorSubject, + Observable, } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -64,7 +64,6 @@ export class DesktopFido2UserInterfaceService this.cipherService, this.accountService, this.logService, - this.messagingService, this.router, this.desktopSettingsService, ); @@ -80,7 +79,6 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private cipherService: CipherService, private accountService: AccountService, private logService: LogService, - private messagingService: MessagingService, private router: Router, private desktopSettingsService: DesktopSettingsService, ) {} @@ -88,7 +86,16 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private confirmCredentialSubject = new Subject(); private createdCipher: Cipher; - private availableCipherIds = new BehaviorSubject(null); + private availableCipherIdsSubject = new BehaviorSubject(null); + /** + * Observable that emits available cipher IDs once they're confirmed by the UI + */ + get availableCipherIds$(): Observable { + return this.availableCipherIdsSubject.pipe( + filter((ids) => ids != null), + take(1), + ); + } private chosenCipherSubject = new Subject(); @@ -121,7 +128,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi // make the cipherIds available to the UI. // Not sure if the UI also need to know about masterPasswordRepromptRequired -- probably not, otherwise we can send all of the params. - this.availableCipherIds.next(cipherIds); + this.availableCipherIdsSubject.next(cipherIds); await this.showUi("/passkeys"); @@ -142,20 +149,6 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } } - /** - * Returns once the UI has confirmed and completed the operation - * @returns - */ - async getAvailableCipherIds(): Promise { - return lastValueFrom( - this.availableCipherIds.pipe( - filter((ids) => ids != null), - take(1), - timeout(50000), - ), - ); - } - confirmChosenCipher(cipherId: string): void { this.chosenCipherSubject.next(cipherId); this.chosenCipherSubject.complete(); From 7555b49e3ddc1c402a353468192c63655185bd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 18 Feb 2025 22:58:06 +0100 Subject: [PATCH 3/9] Launching bitwarden if its not running --- .../CredentialProviderViewController.swift | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 54102da2043..8815d7e0f4c 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -17,10 +17,50 @@ class CredentialProviderViewController: ASCredentialProviderViewController { // // If instead I make this a static, the deinit gets called correctly after each request. // I think we still might want a static regardless, to be able to reuse the connection if possible. - static let client: MacOsProviderClient = { - let instance = MacOsProviderClient.connect() - // setup code - return instance + let client: MacOsProviderClient = { + let logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") + + // Check if the Electron app is running + let workspace = NSWorkspace.shared + let isRunning = workspace.runningApplications.contains { app in + app.bundleIdentifier == "com.bitwarden.desktop" + } + + + if !isRunning { + logger.log("[autofill-extension] Bitwarden Desktop not running, attempting to launch") + + // Try to launch the app + if let appURL = workspace.urlForApplication(withBundleIdentifier: "com.bitwarden.desktop") { + let semaphore = DispatchSemaphore(value: 0) + + workspace.openApplication(at: appURL, + configuration: NSWorkspace.OpenConfiguration()) { app, error in + if let error = error { + logger.error("[autofill-extension] Failed to launch Bitwarden Desktop: \(error.localizedDescription)") + } else if let app = app { + logger.log("[autofill-extension] Successfully launched Bitwarden Desktop") + } else { + logger.error("[autofill-extension] Failed to launch Bitwarden Desktop: unknown error") + } + semaphore.signal() + } + + // Wait for launch completion with timeout + _ = semaphore.wait(timeout: .now() + 5.0) + + // Add a small delay to allow for initialization + Thread.sleep(forTimeInterval: 1.0) + } else { + logger.error("[autofill-extension] Could not find Bitwarden Desktop app") + } + } else { + logger.log("[autofill-extension] Bitwarden Desktop is running") + } + + logger.log("[autofill-extension] Connecting to Bitwarden over IPC") + + return MacOsProviderClient.connect() }() init() { @@ -137,7 +177,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { userVerification: userVerification ) - CredentialProviderViewController.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger)) + self.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger)) return } } @@ -236,7 +276,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { logger.log("[autofill-extension] rpId: \(req.userName)") - CredentialProviderViewController.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext)) + self.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext)) return } } @@ -309,7 +349,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { //extensionInput: requestParameters.extensionInput, ) - CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext)) + self.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext)) return } From a8f63cb9814061ced476a14f6d266d66afa581b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 20 Feb 2025 00:26:13 +0100 Subject: [PATCH 4/9] Passing position from native to electron --- .../macos_provider/src/assertion.rs | 2 + .../macos_provider/src/registration.rs | 1 + apps/desktop/desktop_native/napi/index.d.ts | 3 + apps/desktop/desktop_native/napi/src/lib.rs | 3 + .../CredentialProviderViewController.xib | 2 +- .../CredentialProviderViewController.swift | 36 ++++++++- .../services/desktop-autofill.service.ts | 22 +++++- .../desktop-fido2-user-interface.service.ts | 21 +++-- apps/desktop/src/main/window.main.ts | 7 +- .../platform/models/domain/window-state.ts | 5 ++ .../src/platform/popup-modal-styles.ts | 18 ++++- .../services/desktop-settings.service.ts | 12 +-- enable-passkeys.patch | 78 +++++++++++++++++++ 13 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 enable-passkeys.patch diff --git a/apps/desktop/desktop_native/macos_provider/src/assertion.rs b/apps/desktop/desktop_native/macos_provider/src/assertion.rs index c31601b1f6d..a530bb26a22 100644 --- a/apps/desktop/desktop_native/macos_provider/src/assertion.rs +++ b/apps/desktop/desktop_native/macos_provider/src/assertion.rs @@ -11,6 +11,7 @@ pub struct PasskeyAssertionRequest { client_data_hash: Vec, user_verification: UserVerification, allowed_credentials: Vec>, + window_xy: Vec, //extension_input: Vec, TODO: Implement support for extensions } @@ -24,6 +25,7 @@ pub struct PasskeyAssertionWithoutUserInterfaceRequest { record_identifier: Option, client_data_hash: Vec, user_verification: UserVerification, + window_xy: Vec, } #[derive(uniffi::Record, Serialize, Deserialize)] diff --git a/apps/desktop/desktop_native/macos_provider/src/registration.rs b/apps/desktop/desktop_native/macos_provider/src/registration.rs index d484af58b6c..89260c2defc 100644 --- a/apps/desktop/desktop_native/macos_provider/src/registration.rs +++ b/apps/desktop/desktop_native/macos_provider/src/registration.rs @@ -13,6 +13,7 @@ pub struct PasskeyRegistrationRequest { client_data_hash: Vec, user_verification: UserVerification, supported_algorithms: Vec, + window_xy: Vec, } #[derive(uniffi::Record, Serialize, Deserialize)] diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 79dccab6afe..1bf64596700 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -136,6 +136,7 @@ export declare namespace autofill { clientDataHash: Array userVerification: UserVerification supportedAlgorithms: Array + windowXy: Array } export interface PasskeyRegistrationResponse { rpId: string @@ -148,6 +149,7 @@ export declare namespace autofill { clientDataHash: Array userVerification: UserVerification allowedCredentials: Array> + windowXy: Array } export interface PasskeyAssertionWithoutUserInterfaceRequest { rpId: string @@ -157,6 +159,7 @@ export declare namespace autofill { recordIdentifier?: string clientDataHash: Array userVerification: UserVerification + windowXy: Array } export interface PasskeyAssertionResponse { rpId: string diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index a59833669a7..7edea7308af 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -592,6 +592,7 @@ pub mod autofill { pub client_data_hash: Vec, pub user_verification: UserVerification, pub supported_algorithms: Vec, + pub window_xy: Vec, } #[napi(object)] @@ -612,6 +613,7 @@ pub mod autofill { pub client_data_hash: Vec, pub user_verification: UserVerification, pub allowed_credentials: Vec>, + pub window_xy: Vec, //extension_input: Vec, TODO: Implement support for extensions } @@ -626,6 +628,7 @@ pub mod autofill { pub record_identifier: Option, pub client_data_hash: Vec, pub user_verification: UserVerification, + pub window_xy: Vec, } #[napi(object)] diff --git a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib index 6bcbd589ce3..1e47cc54de2 100644 --- a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib +++ b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib @@ -44,7 +44,7 @@ Gw - + diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 8815d7e0f4c..1b2786baca7 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -90,6 +90,31 @@ class CredentialProviderViewController: ASCredentialProviderViewController { self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) } + private func getWindowPosition() -> [Int32] { + let frame = self.view.window?.frame ?? .zero + let screenHeight = NSScreen.main?.frame.height ?? 0 + + logger.log("[autofill-extension] Detailed window debug:") + logger.log(" Popup frame:") + logger.log(" origin.x: \(frame.origin.x)") + logger.log(" origin.y: \(frame.origin.y)") + logger.log(" width: \(frame.width)") + logger.log(" height: \(frame.height)") + + + // frame.width and frame.height is always 0. Estimating works OK for now. + let estimatedWidth:CGFloat = 400; + let estimatedHeight:CGFloat = 200; + let centerX = Int32(round(frame.origin.x + estimatedWidth/2)) + let centerY = Int32(round(screenHeight - (frame.origin.y + estimatedHeight/2))) + + logger.log(" Calculated center:") + logger.log(" x: \(centerX)") + logger.log(" y: \(centerY)") + + return [centerX, centerY] + } + override func loadView() { let view = NSView() view.isHidden = true @@ -174,7 +199,8 @@ class CredentialProviderViewController: ASCredentialProviderViewController { userHandle: passkeyIdentity.userHandle, recordIdentifier: passkeyIdentity.recordIdentifier, clientDataHash: request.clientDataHash, - userVerification: userVerification + userVerification: userVerification, + windowXy: self.getWindowPosition() ) self.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger)) @@ -268,7 +294,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController { userHandle: passkeyIdentity.userHandle, clientDataHash: request.clientDataHash, userVerification: userVerification, - supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) } + supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) }, + windowXy: self.getWindowPosition() + ) logger.log("[autofill-extension] prepareInterface(passkey) calling preparePasskeyRegistration") // Log details of the request @@ -345,7 +373,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController { rpId: requestParameters.relyingPartyIdentifier, clientDataHash: requestParameters.clientDataHash, userVerification: userVerification, - allowedCredentials: requestParameters.allowedCredentials + allowedCredentials: requestParameters.allowedCredentials, + windowXy: self.getWindowPosition() + //extensionInput: requestParameters.extensionInput, ) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 3d19699c2eb..1f84a55a428 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -37,6 +37,8 @@ import { NativeAutofillSyncCommand, } from "../../platform/main/autofill/sync.command"; +import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"; + @Injectable() export class DesktopAutofillService implements OnDestroy { private destroy$ = new Subject(); @@ -45,7 +47,7 @@ export class DesktopAutofillService implements OnDestroy { private logService: LogService, private cipherService: CipherService, private configService: ConfigService, - private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction, + private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction, private accountService: AccountService, ) {} @@ -147,7 +149,11 @@ export class DesktopAutofillService implements OnDestroy { const controller = new AbortController(); void this.fido2AuthenticatorService - .makeCredential(this.convertRegistrationRequest(request), null, controller) + .makeCredential( + this.convertRegistrationRequest(request), + { windowXy: request.windowXy as [number, number] }, // TODO: Not sure if we want to change the type of windowXy to just number[] or if rust can generate [number,number]? + controller, + ) .then((response) => { callback(null, this.convertRegistrationResponse(request, response)); }) @@ -198,7 +204,11 @@ export class DesktopAutofillService implements OnDestroy { const controller = new AbortController(); void this.fido2AuthenticatorService - .getAssertion(this.convertAssertionRequest(request, true), null, controller) + .getAssertion( + this.convertAssertionRequest(request, true), + { windowXy: request.windowXy as [number, number] }, + controller, + ) .then((response) => { callback(null, this.convertAssertionResponse(request, response)); }) @@ -214,7 +224,11 @@ export class DesktopAutofillService implements OnDestroy { const controller = new AbortController(); void this.fido2AuthenticatorService - .getAssertion(this.convertAssertionRequest(request), null, controller) + .getAssertion( + this.convertAssertionRequest(request), + { windowXy: request.windowXy as [number, number] }, + controller, + ) .then((response) => { callback(null, this.convertAssertionResponse(request, response)); }) diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 018f3fe5cb5..5fbd667b3e9 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -33,10 +33,13 @@ import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note. import { DesktopSettingsService } from "src/platform/services/desktop-settings.service"; -// import the angular router +// This type is used to pass the window position from the native UI +export type NativeWindowObject = { + windowXy?: [number, number]; +}; export class DesktopFido2UserInterfaceService - implements Fido2UserInterfaceServiceAbstraction + implements Fido2UserInterfaceServiceAbstraction { constructor( private authService: AuthService, @@ -55,10 +58,10 @@ export class DesktopFido2UserInterfaceService async newSession( fallbackSupported: boolean, - _tab: void, + _tab: NativeWindowObject, abortController?: AbortController, ): Promise { - this.logService.warning("newSession", fallbackSupported, abortController); + this.logService.warning("newSession", fallbackSupported, abortController, _tab); const session = new DesktopFido2UserInterfaceSession( this.authService, this.cipherService, @@ -66,6 +69,7 @@ export class DesktopFido2UserInterfaceService this.logService, this.router, this.desktopSettingsService, + _tab, ); this.currentSession = session; @@ -81,6 +85,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private logService: LogService, private router: Router, private desktopSettingsService: DesktopSettingsService, + private windowObject: NativeWindowObject, ) {} private confirmCredentialSubject = new Subject(); @@ -130,7 +135,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi // Not sure if the UI also need to know about masterPasswordRepromptRequired -- probably not, otherwise we can send all of the params. this.availableCipherIdsSubject.next(cipherIds); - await this.showUi("/passkeys"); + await this.showUi("/passkeys", this.windowObject.windowXy); const chosenCipherId = await this.waitForUiChosenCipher(); @@ -194,7 +199,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi ); try { - await this.showUi("/passkeys"); + await this.showUi("/passkeys", this.windowObject.windowXy); // Wait for the UI to wrap up const confirmation = await this.waitForUiNewCredentialConfirmation(); @@ -224,10 +229,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } } - private async showUi(route: string) { + private async showUi(route: string, position?: [number, number]): Promise { // Load the UI: // maybe toggling to modal mode shouldn't be done here? - await this.desktopSettingsService.setInModalMode(true); + await this.desktopSettingsService.setInModalMode(true, position); await this.router.navigate(["/passkeys"]); } diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 5420959e287..b5e02f64557 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -81,14 +81,15 @@ export class WindowMain { .pipe( pairwise(), concatMap(async ([lastValue, newValue]) => { - if (lastValue && !newValue) { + if (lastValue.modalMode && !newValue.modalMode) { // Reset the window state to the main window state applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]); // Because modal is used in front of another app, UX wise it makes sense to hide the main window when leaving modal mode. this.win.hide(); - } else if (!lastValue && newValue) { + } else if (!lastValue.modalMode && newValue.modalMode) { // Apply the popup modal styles - applyPopupModalStyles(this.win); + this.logService.info("Applying popup modal styles", newValue.modalPosition); + applyPopupModalStyles(this.win, newValue.modalPosition); this.win.show(); } }), diff --git a/apps/desktop/src/platform/models/domain/window-state.ts b/apps/desktop/src/platform/models/domain/window-state.ts index 00230319972..f0292abfff4 100644 --- a/apps/desktop/src/platform/models/domain/window-state.ts +++ b/apps/desktop/src/platform/models/domain/window-state.ts @@ -11,3 +11,8 @@ export class WindowState { y?: number; zoomFactor?: number; } + +export class ModalModeState { + modalMode: boolean; + modalPosition?: [number, number]; +} diff --git a/apps/desktop/src/platform/popup-modal-styles.ts b/apps/desktop/src/platform/popup-modal-styles.ts index 9c3f06b34bf..e33c41f09b4 100644 --- a/apps/desktop/src/platform/popup-modal-styles.ts +++ b/apps/desktop/src/platform/popup-modal-styles.ts @@ -6,10 +6,17 @@ import { WindowState } from "./models/domain/window-state"; const popupWidth = 680; const popupHeight = 500; -export function applyPopupModalStyles(window: BrowserWindow) { +export function applyPopupModalStyles(window: BrowserWindow, position?: [number, number]) { window.unmaximize(); window.setSize(popupWidth, popupHeight); - window.center(); + + if (position) { + const centeredX = position[0] - popupWidth / 2; + const centeredY = position[1] - popupHeight / 2; + window.setPosition(centeredX, centeredY); + } else { + window.center(); + } window.setWindowButtonVisibility?.(false); window.setMenuBarVisibility?.(false); window.setResizable(false); @@ -21,6 +28,13 @@ export function applyPopupModalStyles(window: BrowserWindow) { window.once("leave-full-screen", () => { window.setSize(popupWidth, popupHeight); window.center(); + if (position) { + const centeredX = position[0] - popupWidth / 2; + const centeredY = position[1] - popupHeight / 2; + window.setPosition(centeredX, centeredY); + } else { + window.center(); + } }); } } diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index 77bd57cddb1..422a6f0913e 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -8,7 +8,7 @@ import { } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -import { WindowState } from "../models/domain/window-state"; +import { ModalModeState, WindowState } from "../models/domain/window-state"; export const HARDWARE_ACCELERATION = new KeyDefinition( DESKTOP_SETTINGS_DISK, @@ -75,7 +75,7 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition(DESKTOP_SETTINGS_DISK, " clearOn: [], // User setting, no need to clear }); -const IN_MODAL_MODE = new KeyDefinition(DESKTOP_SETTINGS_DISK, "inModalMode", { +const IN_MODAL_MODE = new KeyDefinition(DESKTOP_SETTINGS_DISK, "inModalMode", { deserializer: (b) => b, }); @@ -161,7 +161,7 @@ export class DesktopSettingsService { private readonly inModalModeState = this.stateProvider.getGlobal(IN_MODAL_MODE); - inModalMode$ = this.inModalModeState.state$.pipe(map(Boolean)); + inModalMode$ = this.inModalModeState.state$; constructor(private stateProvider: StateProvider) { this.window$ = this.windowState.state$.pipe( @@ -176,7 +176,7 @@ export class DesktopSettingsService { * stuck in modal mode if the application is force-closed in modal mode. */ async resetInModalMode() { - await this.inModalModeState.update(() => false); + await this.inModalModeState.update(() => ({ modalMode: false })); } async setHardwareAcceleration(enabled: boolean) { @@ -291,7 +291,7 @@ export class DesktopSettingsService { * Sets the modal mode of the application. Setting this changes the windows-size and other properties. * @param value `true` if the application is in modal mode, `false` if it is not. */ - async setInModalMode(value: boolean) { - await this.inModalModeState.update(() => value); + async setInModalMode(value: boolean, windowXy?: [number, number]) { + await this.inModalModeState.update(() => ({ modalMode: value, modalPosition: windowXy })); } } diff --git a/enable-passkeys.patch b/enable-passkeys.patch new file mode 100644 index 00000000000..4fec564f8ee --- /dev/null +++ b/enable-passkeys.patch @@ -0,0 +1,78 @@ +diff --git a/apps/desktop/resources/entitlements.mas.inherit.plist b/apps/desktop/resources/entitlements.mas.inherit.plist +index 7e957fce7c..e9a28f8f32 100644 +--- a/apps/desktop/resources/entitlements.mas.inherit.plist ++++ b/apps/desktop/resources/entitlements.mas.inherit.plist +@@ -8,9 +8,7 @@ + + com.apple.security.cs.allow-jit + +- + + +diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist +index 0450111beb..5bb95f76af 100644 +--- a/apps/desktop/resources/entitlements.mas.plist ++++ b/apps/desktop/resources/entitlements.mas.plist +@@ -16,10 +16,8 @@ + + com.apple.security.files.user-selected.read-write + +- + com.apple.security.temporary-exception.files.home-relative-path.read-write + + /Library/Application Support/Mozilla/NativeMessagingHosts/ +diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts +index 1ce58596b3..86f7ef0a43 100644 +--- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts ++++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts +@@ -1,14 +1,13 @@ + import { Injectable, OnDestroy } from "@angular/core"; + import { autofill } from "desktop_native/napi"; + import { +- EMPTY, + Subject, + distinctUntilChanged, + firstValueFrom, + map, + mergeMap, + switchMap, +- takeUntil, ++ takeUntil + } from "rxjs"; + + import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +@@ -56,9 +55,9 @@ export class DesktopAutofillService implements OnDestroy { + .pipe( + distinctUntilChanged(), + switchMap((enabled) => { +- if (!enabled) { ++ /*if (!enabled) { + return EMPTY; +- } ++ }*/ + + return this.cipherService.cipherViews$; + }), +diff --git a/apps/desktop/src/utils.ts b/apps/desktop/src/utils.ts +index c798faac36..d203998ed4 100644 +--- a/apps/desktop/src/utils.ts ++++ b/apps/desktop/src/utils.ts +@@ -20,11 +20,7 @@ export function invokeMenu(menu: RendererMenuItem[]) { + } + + export function isDev() { +- // ref: https://github.com/sindresorhus/electron-is-dev +- if ("ELECTRON_IS_DEV" in process.env) { +- return parseInt(process.env.ELECTRON_IS_DEV, 10) === 1; +- } +- return process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath); ++ return true; + } + + export function isLinux() { From 48d1743ab52b8e3906cf31faaf81359e0b4eb092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 20 Feb 2025 00:32:50 +0100 Subject: [PATCH 5/9] Rename inModalMode to modalMode --- .../components/fido2placeholder.component.ts | 6 +++--- .../desktop-fido2-user-interface.service.ts | 6 +++--- apps/desktop/src/main.ts | 2 +- apps/desktop/src/main/window.main.ts | 12 ++++++------ .../src/platform/models/domain/window-state.ts | 4 ++-- .../services/desktop-settings.service.ts | 17 ++++++++++------- 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/app/components/fido2placeholder.component.ts b/apps/desktop/src/app/components/fido2placeholder.component.ts index 84558ac27dc..96d52c011d4 100644 --- a/apps/desktop/src/app/components/fido2placeholder.component.ts +++ b/apps/desktop/src/app/components/fido2placeholder.component.ts @@ -73,7 +73,7 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy { this.session?.confirmChosenCipher(cipherId); await this.router.navigate(["/"]); - await this.desktopSettingsService.setInModalMode(false); + await this.desktopSettingsService.setModalMode(false); } ngOnDestroy() { @@ -103,7 +103,7 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy { // The session currently toggles modal on and send us here // But if this route is somehow opened outside of session we want to make sure we clean up? await this.router.navigate(["/"]); - await this.desktopSettingsService.setInModalMode(false); + await this.desktopSettingsService.setModalMode(false); } catch (error) { // TODO: Handle error appropriately } @@ -111,7 +111,7 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy { async closeModal() { await this.router.navigate(["/"]); - await this.desktopSettingsService.setInModalMode(false); + await this.desktopSettingsService.setModalMode(false); this.session.notifyConfirmNewCredential(false); // little bit hacky: diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 5fbd667b3e9..e08a288e044 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -150,7 +150,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi return { cipherId: resultCipherId, userVerified: true }; } finally { // Make sure to clean up so the app is never stuck in modal mode? - await this.desktopSettingsService.setInModalMode(false); + await this.desktopSettingsService.setModalMode(false); } } @@ -225,14 +225,14 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi return { cipherId: this.createdCipher.id, userVerified: userVerification }; } finally { // Make sure to clean up so the app is never stuck in modal mode? - await this.desktopSettingsService.setInModalMode(false); + await this.desktopSettingsService.setModalMode(false); } } private async showUi(route: string, position?: [number, number]): Promise { // Load the UI: // maybe toggling to modal mode shouldn't be done here? - await this.desktopSettingsService.setInModalMode(true, position); + await this.desktopSettingsService.setModalMode(true, position); await this.router.navigate(["/passkeys"]); } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index c1f08927073..1ff9fe1675c 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -279,7 +279,7 @@ export class Main { async () => { await this.toggleHardwareAcceleration(); // Reset modal mode to make sure main window is displayed correctly - await this.desktopSettingsService.resetInModalMode(); + await this.desktopSettingsService.resetModalMode(); await this.windowMain.init(); await this.i18nService.init(); await this.messagingMain.init(); diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index b5e02f64557..1a1151e62a3 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -77,16 +77,16 @@ export class WindowMain { } }); - this.desktopSettingsService.inModalMode$ + this.desktopSettingsService.modalMode$ .pipe( pairwise(), concatMap(async ([lastValue, newValue]) => { - if (lastValue.modalMode && !newValue.modalMode) { + if (lastValue.isModalModeActive && !newValue.isModalModeActive) { // Reset the window state to the main window state applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]); // Because modal is used in front of another app, UX wise it makes sense to hide the main window when leaving modal mode. this.win.hide(); - } else if (!lastValue.modalMode && newValue.modalMode) { + } else if (!lastValue.isModalModeActive && newValue.isModalModeActive) { // Apply the popup modal styles this.logService.info("Applying popup modal styles", newValue.modalPosition); applyPopupModalStyles(this.win, newValue.modalPosition); @@ -209,7 +209,7 @@ export class WindowMain { return; } - await this.desktopSettingsService.setInModalMode(modal); + await this.desktopSettingsService.setModalMode(modal); await this.win.loadURL( url.format({ protocol: "file:", @@ -405,9 +405,9 @@ export class WindowMain { return; } - const inModalMode = await firstValueFrom(this.desktopSettingsService.inModalMode$); + const modalMode = await firstValueFrom(this.desktopSettingsService.modalMode$); - if (inModalMode) { + if (modalMode.isModalModeActive) { return; } diff --git a/apps/desktop/src/platform/models/domain/window-state.ts b/apps/desktop/src/platform/models/domain/window-state.ts index f0292abfff4..8aead4477f6 100644 --- a/apps/desktop/src/platform/models/domain/window-state.ts +++ b/apps/desktop/src/platform/models/domain/window-state.ts @@ -13,6 +13,6 @@ export class WindowState { } export class ModalModeState { - modalMode: boolean; - modalPosition?: [number, number]; + isModalModeActive: boolean; + modalPosition?: [number, number]; // Modal position is often passed from the native UI } diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index 422a6f0913e..d52405e697c 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -75,7 +75,7 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition(DESKTOP_SETTINGS_DISK, " clearOn: [], // User setting, no need to clear }); -const IN_MODAL_MODE = new KeyDefinition(DESKTOP_SETTINGS_DISK, "inModalMode", { +const MODAL_MODE = new KeyDefinition(DESKTOP_SETTINGS_DISK, "modalMode", { deserializer: (b) => b, }); @@ -159,9 +159,9 @@ export class DesktopSettingsService { */ minimizeOnCopy$ = this.minimizeOnCopyState.state$.pipe(map(Boolean)); - private readonly inModalModeState = this.stateProvider.getGlobal(IN_MODAL_MODE); + private readonly modalModeState = this.stateProvider.getGlobal(MODAL_MODE); - inModalMode$ = this.inModalModeState.state$; + modalMode$ = this.modalModeState.state$; constructor(private stateProvider: StateProvider) { this.window$ = this.windowState.state$.pipe( @@ -175,8 +175,8 @@ export class DesktopSettingsService { * This is used to clear the setting on application start to make sure we don't end up * stuck in modal mode if the application is force-closed in modal mode. */ - async resetInModalMode() { - await this.inModalModeState.update(() => ({ modalMode: false })); + async resetModalMode() { + await this.modalModeState.update(() => ({ isModalModeActive: false })); } async setHardwareAcceleration(enabled: boolean) { @@ -291,7 +291,10 @@ export class DesktopSettingsService { * Sets the modal mode of the application. Setting this changes the windows-size and other properties. * @param value `true` if the application is in modal mode, `false` if it is not. */ - async setInModalMode(value: boolean, windowXy?: [number, number]) { - await this.inModalModeState.update(() => ({ modalMode: value, modalPosition: windowXy })); + async setModalMode(value: boolean, modalPosition?: [number, number]) { + await this.modalModeState.update(() => ({ + isModalModeActive: value, + modalPosition: modalPosition, + })); } } From bd9ffb57fce65c59f91c8987ea853b82fd27b1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 20 Feb 2025 00:35:38 +0100 Subject: [PATCH 6/9] remove tap --- libs/common/src/vault/services/cipher.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 0f1b1fe93b4..8711496b374 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -10,7 +10,6 @@ import { shareReplay, Subject, switchMap, - tap, } from "rxjs"; import { SemVer } from "semver"; @@ -142,7 +141,6 @@ export class CipherService implements CipherServiceAbstraction { this.cipherViews$ = combineLatest([this.encryptedCiphersState.state$, this.localData$]).pipe( filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())), - tap((v) => console.log("---- cipherViews$", v)), shareReplay({ bufferSize: 1, refCount: true }), ); this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; From da79e89b03f00381a2512a2a38c4843dc0edc82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 20 Feb 2025 00:37:06 +0100 Subject: [PATCH 7/9] revert spaces --- .../src/platform/services/fido2/fido2-authenticator.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index cc7f897687e..376f4dcdced 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -132,7 +132,6 @@ export class Fido2AuthenticatorService userVerification: params.requireUserVerification, rpId: params.rpEntity.id, }); - const cipherId = response.cipherId; userVerified = response.userVerified; @@ -147,7 +146,6 @@ export class Fido2AuthenticatorService keyPair = await createKeyPair(); pubKeyDer = await crypto.subtle.exportKey("spki", keyPair.publicKey); const encrypted = await this.cipherService.get(cipherId); - const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -182,6 +180,7 @@ export class Fido2AuthenticatorService ); throw new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.Unknown); } + const authData = await generateAuthData({ rpId: params.rpEntity.id, credentialId: parseCredentialId(credentialId), From 4ca132ca81ed5d406328e75c8212c92a613dfc8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 20 Feb 2025 00:39:15 +0100 Subject: [PATCH 8/9] added back isDev --- apps/desktop/src/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/utils.ts b/apps/desktop/src/utils.ts index d203998ed4d..c798faac36e 100644 --- a/apps/desktop/src/utils.ts +++ b/apps/desktop/src/utils.ts @@ -20,7 +20,11 @@ export function invokeMenu(menu: RendererMenuItem[]) { } export function isDev() { - return true; + // ref: https://github.com/sindresorhus/electron-is-dev + if ("ELECTRON_IS_DEV" in process.env) { + return parseInt(process.env.ELECTRON_IS_DEV, 10) === 1; + } + return process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath); } export function isLinux() { From 2a8fbc313261d3ef8c3a07fe69f474a603cb8c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 20 Feb 2025 00:43:44 +0100 Subject: [PATCH 9/9] cleaned up a bit --- .../desktop-fido2-user-interface.service.ts | 13 +++++++++---- apps/desktop/src/main/window.main.ts | 2 ++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index e08a288e044..0cba3459cd3 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -33,8 +33,13 @@ import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note. import { DesktopSettingsService } from "src/platform/services/desktop-settings.service"; -// This type is used to pass the window position from the native UI +/** + * This type is used to pass the window position from the native UI + */ export type NativeWindowObject = { + /** + * The position of the window, first entry is the x position, second is the y position + */ windowXy?: [number, number]; }; @@ -58,10 +63,10 @@ export class DesktopFido2UserInterfaceService async newSession( fallbackSupported: boolean, - _tab: NativeWindowObject, + nativeWindowObject: NativeWindowObject, abortController?: AbortController, ): Promise { - this.logService.warning("newSession", fallbackSupported, abortController, _tab); + this.logService.warning("newSession", fallbackSupported, abortController, nativeWindowObject); const session = new DesktopFido2UserInterfaceSession( this.authService, this.cipherService, @@ -69,7 +74,7 @@ export class DesktopFido2UserInterfaceService this.logService, this.router, this.desktopSettingsService, - _tab, + nativeWindowObject, ); this.currentSession = session; diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 1a1151e62a3..5a600070d56 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -203,6 +203,8 @@ export class WindowMain { } } + // TODO: REMOVE ONCE WE CAN STOP USING FAKE POP UP BTN FROM TRAY + // Only used during initial UI development async loadUrl(targetPath: string, modal: boolean = false) { if (this.win == null || this.win.isDestroyed()) { await this.createWindow("modal-app");