1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 13:40:06 +00:00

Passing position from native to electron

This commit is contained in:
Anders Åberg
2025-02-20 00:26:13 +01:00
parent 7555b49e3d
commit a8f63cb981
13 changed files with 183 additions and 27 deletions

View File

@@ -11,6 +11,7 @@ pub struct PasskeyAssertionRequest {
client_data_hash: Vec<u8>,
user_verification: UserVerification,
allowed_credentials: Vec<Vec<u8>>,
window_xy: Vec<i32>,
//extension_input: Vec<u8>, TODO: Implement support for extensions
}
@@ -24,6 +25,7 @@ pub struct PasskeyAssertionWithoutUserInterfaceRequest {
record_identifier: Option<String>,
client_data_hash: Vec<u8>,
user_verification: UserVerification,
window_xy: Vec<i32>,
}
#[derive(uniffi::Record, Serialize, Deserialize)]

View File

@@ -13,6 +13,7 @@ pub struct PasskeyRegistrationRequest {
client_data_hash: Vec<u8>,
user_verification: UserVerification,
supported_algorithms: Vec<i32>,
window_xy: Vec<i32>,
}
#[derive(uniffi::Record, Serialize, Deserialize)]

View File

@@ -136,6 +136,7 @@ export declare namespace autofill {
clientDataHash: Array<number>
userVerification: UserVerification
supportedAlgorithms: Array<number>
windowXy: Array<number>
}
export interface PasskeyRegistrationResponse {
rpId: string
@@ -148,6 +149,7 @@ export declare namespace autofill {
clientDataHash: Array<number>
userVerification: UserVerification
allowedCredentials: Array<Array<number>>
windowXy: Array<number>
}
export interface PasskeyAssertionWithoutUserInterfaceRequest {
rpId: string
@@ -157,6 +159,7 @@ export declare namespace autofill {
recordIdentifier?: string
clientDataHash: Array<number>
userVerification: UserVerification
windowXy: Array<number>
}
export interface PasskeyAssertionResponse {
rpId: string

View File

@@ -592,6 +592,7 @@ pub mod autofill {
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub supported_algorithms: Vec<i32>,
pub window_xy: Vec<i32>,
}
#[napi(object)]
@@ -612,6 +613,7 @@ pub mod autofill {
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub allowed_credentials: Vec<Vec<u8>>,
pub window_xy: Vec<i32>,
//extension_input: Vec<u8>, TODO: Implement support for extensions
}
@@ -626,6 +628,7 @@ pub mod autofill {
pub record_identifier: Option<String>,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub window_xy: Vec<i32>,
}
#[napi(object)]

View File

@@ -44,7 +44,7 @@ Gw
<action selector="cancel:" target="-2" id="Qav-AK-DGt"/>
</connections>
</button>
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aNc-0i-CWK">
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aNc-0i-CWK">
<rect key="frame" x="112" y="63" width="154" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="autofill-extension hello" id="0xp-rC-2gr">
<font key="font" metaFont="systemBold"/>

View File

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

View File

@@ -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<void>();
@@ -45,7 +47,7 @@ export class DesktopAutofillService implements OnDestroy {
private logService: LogService,
private cipherService: CipherService,
private configService: ConfigService,
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<void>,
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<NativeWindowObject>,
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));
})

View File

@@ -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<void>
implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject>
{
constructor(
private authService: AuthService,
@@ -55,10 +58,10 @@ export class DesktopFido2UserInterfaceService
async newSession(
fallbackSupported: boolean,
_tab: void,
_tab: NativeWindowObject,
abortController?: AbortController,
): Promise<DesktopFido2UserInterfaceSession> {
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<boolean>();
@@ -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<void> {
// 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"]);
}

View File

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

View File

@@ -11,3 +11,8 @@ export class WindowState {
y?: number;
zoomFactor?: number;
}
export class ModalModeState {
modalMode: boolean;
modalPosition?: [number, number];
}

View File

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

View File

@@ -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<boolean>(
DESKTOP_SETTINGS_DISK,
@@ -75,7 +75,7 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "
clearOn: [], // User setting, no need to clear
});
const IN_MODAL_MODE = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "inModalMode", {
const IN_MODAL_MODE = new KeyDefinition<ModalModeState>(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 }));
}
}

78
enable-passkeys.patch Normal file
View File

@@ -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 @@
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
- <!--
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
- -->
</dict>
</plist>
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 @@
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
- <!--
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
- -->
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
<string>/Library/Application Support/Mozilla/NativeMessagingHosts/</string>
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() {