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

merge recent work

This commit is contained in:
Evan Bassler
2025-02-20 16:46:15 -06:00
24 changed files with 329 additions and 120 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

@@ -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() {
@@ -50,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
@@ -134,10 +199,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
userHandle: passkeyIdentity.userHandle,
recordIdentifier: passkeyIdentity.recordIdentifier,
clientDataHash: request.clientDataHash,
userVerification: userVerification
userVerification: userVerification,
windowXy: self.getWindowPosition()
)
CredentialProviderViewController.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger))
self.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger))
return
}
}
@@ -169,22 +235,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() + 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
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 {
@@ -228,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
@@ -236,7 +304,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
}
}
@@ -305,11 +373,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
rpId: requestParameters.relyingPartyIdentifier,
clientDataHash: requestParameters.clientDataHash,
userVerification: userVerification,
allowedCredentials: requestParameters.allowedCredentials
allowedCredentials: requestParameters.allowedCredentials,
windowXy: self.getWindowPosition()
//extensionInput: requestParameters.extensionInput,
)
CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext))
self.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext))
return
}

View File

@@ -338,7 +338,7 @@ const routes: Routes = [
component: Fido2VaultComponent,
},
{
path: "passkey-create",
path: "create-passkey",
component: Fido2CreateComponent,
},
{

View File

@@ -56,7 +56,7 @@ import { DesktopSettingsService } from "../../platform/services/desktop-settings
export class Fido2PlaceholderComponent implements OnInit, OnDestroy {
session?: DesktopFido2UserInterfaceSession = null;
private cipherIdsSubject = new BehaviorSubject<string[]>([]);
cipherIds$: Observable<string[]> = this.cipherIdsSubject.asObservable();
cipherIds$: Observable<string[]>;
constructor(
private readonly desktopSettingsService: DesktopSettingsService,
@@ -64,21 +64,16 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy {
private readonly router: Router,
) {}
async ngOnInit(): Promise<void> {
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) {
this.session?.confirmChosenCipher(cipherId);
await this.router.navigate(["/"]);
await this.desktopSettingsService.setInModalMode(false);
await this.desktopSettingsService.setModalMode(false);
}
ngOnDestroy() {
@@ -108,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
}
@@ -116,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.notifyConfirmCredential(false);
// little bit hacky:

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

@@ -6,8 +6,9 @@ import {
Subject,
filter,
take,
timeout,
BehaviorSubject,
Observable,
timeout,
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -33,10 +34,18 @@ 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 = {
/**
* The position of the window, first entry is the x position, second is the y position
*/
windowXy?: [number, number];
};
export class DesktopFido2UserInterfaceService
implements Fido2UserInterfaceServiceAbstraction<void>
implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject>
{
constructor(
private authService: AuthService,
@@ -55,18 +64,18 @@ export class DesktopFido2UserInterfaceService
async newSession(
fallbackSupported: boolean,
_tab: void,
nativeWindowObject: NativeWindowObject,
abortController?: AbortController,
): Promise<DesktopFido2UserInterfaceSession> {
this.logService.warning("newSession", fallbackSupported, abortController);
this.logService.warning("newSession", fallbackSupported, abortController, nativeWindowObject);
const session = new DesktopFido2UserInterfaceSession(
this.authService,
this.cipherService,
this.accountService,
this.logService,
this.messagingService,
this.router,
this.desktopSettingsService,
nativeWindowObject,
);
this.currentSession = session;
@@ -80,9 +89,9 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
private cipherService: CipherService,
private accountService: AccountService,
private logService: LogService,
private messagingService: MessagingService,
private router: Router,
private desktopSettingsService: DesktopSettingsService,
private windowObject: NativeWindowObject,
) {}
private confirmCredentialSubject = new Subject<boolean>();
@@ -90,8 +99,17 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
private createdCipher: Cipher;
private updatedCipher: CipherView;
private availableCipherIds = new BehaviorSubject<string[]>(null);
private rpId = new BehaviorSubject<string>(null);
private availableCipherIdsSubject = new BehaviorSubject<string[]>(null);
/**
* Observable that emits available cipher IDs once they're confirmed by the UI
*/
get availableCipherIds$(): Observable<string[]> {
return this.availableCipherIdsSubject.pipe(
filter((ids) => ids != null),
take(1),
);
}
private chosenCipherSubject = new Subject<string>();
@@ -124,9 +142,9 @@ 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");
await this.showUi("/passkeys", this.windowObject.windowXy);
const chosenCipherId = await this.waitForUiChosenCipher();
@@ -141,31 +159,17 @@ 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);
}
}
/**
* Returns once the UI has confirmed and completed the operation
* @returns
*/
async getAvailableCipherIds(): Promise<string[]> {
return lastValueFrom(
this.availableCipherIds.pipe(
filter((ids) => ids != null),
take(1),
timeout(50000),
),
);
}
async getRpId(): Promise<string> {
console.log("getRpId");
return lastValueFrom(
this.rpId.pipe(
filter((id) => id != null),
take(1),
timeout(50000),
timeout(5000), // 5 seconds timeout
),
);
}
@@ -219,7 +223,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
this.rpId.next(rpId);
try {
await this.showUi("/passkey-create");
await this.showUi("/create-passkey", this.windowObject.windowXy);
// Wait for the UI to wrap up
const confirmation = await this.waitForUiNewCredentialConfirmation();
@@ -244,17 +248,14 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
}
} 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) {
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);
//pass the rpid to the fido2placeholder component through routing parameter
// await this.router.navigate(["/passkeys"]);
await this.desktopSettingsService.setModalMode(true, position);
await this.router.navigate([route]);
}

View File

@@ -3436,5 +3436,8 @@
},
"savePasskeyQuestion": {
"message": "Save passkey?"
},
"saveNewPasskey": {
"message": "Save as new login"
}
}

View File

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

View File

@@ -77,18 +77,19 @@ export class WindowMain {
}
});
this.desktopSettingsService.inModalMode$
this.desktopSettingsService.modalMode$
.pipe(
pairwise(),
concatMap(async ([lastValue, newValue]) => {
if (lastValue && !newValue) {
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 && newValue) {
} else if (!lastValue.isModalModeActive && newValue.isModalModeActive) {
// 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();
}
}),
@@ -202,13 +203,15 @@ 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");
return;
}
await this.desktopSettingsService.setInModalMode(modal);
await this.desktopSettingsService.setModalMode(modal);
await this.win.loadURL(
url.format({
protocol: "file:",
@@ -404,9 +407,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;
}

View File

@@ -1,4 +1,4 @@
<div class="tw-flex tw-flex-col">
<div class="tw-flex tw-flex-col tw-h-full">
<bit-section
disableMargin
class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
@@ -7,7 +7,7 @@
<div class="tw-flex tw-items-center">
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
<h2 bitTypography="h6">{{ "savePasskeyQuestion" | i18n }}</h2>
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">{{ "savePasskeyQuestion" | i18n }}</h2>
</div>
<button
@@ -22,7 +22,7 @@
</bit-section-header>
</bit-section>
<bit-section class="tw-bg-background-alt tw-p-4">
<bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col tw-grow">
<bit-item *ngFor="let c of ciphers$ | async" class="">
<button type="button" bit-item-content (click)="addPasskeyToCipher(c)">
<app-vault-icon [cipher]="c" slot="start"></app-vault-icon>
@@ -33,14 +33,17 @@
<span bitBadge slot="end">Save</span>
</button>
</bit-item>
<button
style="color: black; padding: 10px 20px; border: 1px solid black; margin: 10px"
bitButton
type="button"
buttonType="secondary"
(click)="confirmPasskey()"
>
Save as new login
</button>
</bit-section>
<bit-section class="tw-bg-background-alt tw-p-4 ">
<bit-item class="">
<button bitLink linkType="primary"type="button" bit-item-content (click)="confirmPasskey()">
<a bitLink linkType="primary" class="tw-font-medium tw-text-base">
{{ "saveNewPasskey" | i18n }}
</a>
</button>
</bit-item>
</bit-section>
</div>

View File

@@ -115,7 +115,7 @@ export class Fido2CreateComponent implements OnInit {
// 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
}
@@ -123,7 +123,8 @@ export class Fido2CreateComponent implements OnInit {
async closeModal() {
await this.router.navigate(["/"]);
await this.desktopSettingsService.setInModalMode(false);
await this.desktopSettingsService.setModalMode(false);
this.session.notifyConfirmCredential(false);
this.session.confirmChosenCipher(null);
}
}

View File

@@ -1,4 +1,4 @@
<div class="tw-flex tw-flex-col">
<div class="tw-flex tw-flex-col tw-h-full">
<bit-section
disableMargin
class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
@@ -7,7 +7,7 @@
<div class="tw-flex tw-items-center">
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
<h2 bitTypography="h6">{{ "passkeyLogin" | i18n }}</h2>
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">{{ "passkeyLogin" | i18n }}</h2>
</div>
<button
type="button"
@@ -21,7 +21,7 @@
</bit-section-header>
</bit-section>
<bit-section class="tw-bg-background-alt tw-p-4">
<bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col tw-grow">
<bit-item *ngFor="let c of ciphers$ | async" class="">
<button type="button" bit-item-content (click)="chooseCipher(c.id)">
<app-vault-icon [cipher]="c" slot="start"></app-vault-icon>

View File

@@ -47,6 +47,8 @@ export class Fido2VaultComponent implements OnInit {
session?: DesktopFido2UserInterfaceSession = null;
private ciphersSubject = new BehaviorSubject<CipherView[]>([]);
ciphers$: Observable<CipherView[]> = this.ciphersSubject.asObservable();
private cipherIdsSubject = new BehaviorSubject<string[]>([]);
cipherIds$: Observable<string[]>;
readonly Icons = { BitwardenShield };
constructor(
@@ -58,29 +60,36 @@ export class Fido2VaultComponent implements OnInit {
async ngOnInit() {
this.session = this.fido2UserInterfaceService.getCurrentSession();
this.cipherIds$ = this.session?.availableCipherIds$;
const cipherIds = await this.session?.getAvailableCipherIds();
this.cipherIds$.subscribe((cipherIds) => {
this.cipherService
.getAllDecryptedForIds(cipherIds || [])
.then((ciphers) => {
this.ciphersSubject.next(ciphers);
})
.catch(() => {
// console.error(err);
});
});
}
this.cipherService
.getAllDecryptedForIds(cipherIds || [])
.then((ciphers) => {
this.ciphersSubject.next(ciphers);
})
.catch(() => {
// console.error(err);
});
ngOnDestroy() {
this.cipherIdsSubject.complete(); // Clean up the BehaviorSubject
}
async chooseCipher(cipherId: string) {
this.session?.confirmChosenCipher(cipherId);
await this.router.navigate(["/"]);
await this.desktopSettingsService.setInModalMode(false);
await this.desktopSettingsService.setModalMode(false);
}
async closeModal() {
await this.router.navigate(["/"]);
await this.desktopSettingsService.setInModalMode(false);
await this.desktopSettingsService.setModalMode(false);
this.session.notifyConfirmCredential(false);
this.session.confirmChosenCipher(null);
}
}

View File

@@ -11,3 +11,8 @@ export class WindowState {
y?: number;
zoomFactor?: number;
}
export class ModalModeState {
isModalModeActive: boolean;
modalPosition?: [number, number]; // Modal position is often passed from the native UI
}

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 MODAL_MODE = new KeyDefinition<ModalModeState>(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$.pipe(map(Boolean));
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(() => 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) {
await this.inModalModeState.update(() => value);
async setModalMode(value: boolean, modalPosition?: [number, number]) {
await this.modalModeState.update(() => ({
isModalModeActive: value,
modalPosition: modalPosition,
}));
}
}

View File

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

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

View File

@@ -132,7 +132,6 @@ export class Fido2AuthenticatorService<ParentWindowReference>
userVerification: params.requireUserVerification,
rpId: params.rpEntity.id,
});
const cipherId = response.cipherId;
userVerified = response.userVerified;
@@ -147,7 +146,6 @@ export class Fido2AuthenticatorService<ParentWindowReference>
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<ParentWindowReference>
);
throw new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.Unknown);
}
const authData = await generateAuthData({
rpId: params.rpEntity.id,
credentialId: parseCredentialId(credentialId),

View File

@@ -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$;