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:
@@ -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)]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
3
apps/desktop/desktop_native/napi/index.d.ts
vendored
3
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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));
|
||||
})
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -11,3 +11,8 @@ export class WindowState {
|
||||
y?: number;
|
||||
zoomFactor?: number;
|
||||
}
|
||||
|
||||
export class ModalModeState {
|
||||
modalMode: boolean;
|
||||
modalPosition?: [number, number];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
78
enable-passkeys.patch
Normal 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() {
|
||||
Reference in New Issue
Block a user