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