mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 13:40:06 +00:00
merge recent work
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"/>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ const routes: Routes = [
|
||||
component: Fido2VaultComponent,
|
||||
},
|
||||
{
|
||||
path: "passkey-create",
|
||||
path: "create-passkey",
|
||||
component: Fido2CreateComponent,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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));
|
||||
})
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3436,5 +3436,8 @@
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
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() {
|
||||
@@ -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),
|
||||
|
||||
@@ -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$;
|
||||
|
||||
Reference in New Issue
Block a user