From 70661c014f9ca034bfd5fde62b9c16db9d53bd5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 24 Mar 2025 13:23:49 +0100 Subject: [PATCH 01/98] Turn on passkeys and dev mode --- apps/desktop/resources/entitlements.mas.inherit.plist | 2 -- apps/desktop/resources/entitlements.mas.plist | 2 -- .../src/autofill/services/desktop-autofill.service.ts | 7 +++---- apps/desktop/src/utils.ts | 11 ++++++----- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/desktop/resources/entitlements.mas.inherit.plist b/apps/desktop/resources/entitlements.mas.inherit.plist index 7e957fce7ce..e9a28f8f327 100644 --- a/apps/desktop/resources/entitlements.mas.inherit.plist +++ b/apps/desktop/resources/entitlements.mas.inherit.plist @@ -8,9 +8,7 @@ com.apple.security.cs.allow-jit - diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index bb06ae10431..8bca39937d6 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -18,10 +18,8 @@ com.apple.security.device.usb - com.apple.security.temporary-exception.files.home-relative-path.read-write /Library/Application Support/Mozilla/NativeMessagingHosts/ diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index e88e16c2ffc..70a9280d8ef 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -9,7 +9,6 @@ import { mergeMap, switchMap, takeUntil, - EMPTY, } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -61,9 +60,9 @@ export class DesktopAutofillService implements OnDestroy { .pipe( distinctUntilChanged(), switchMap((enabled) => { - if (!enabled) { - return EMPTY; - } + // if (!enabled) { + // return EMPTY; + // } return this.accountService.activeAccount$.pipe( map((account) => account?.id), diff --git a/apps/desktop/src/utils.ts b/apps/desktop/src/utils.ts index c798faac36e..4ab32488726 100644 --- a/apps/desktop/src/utils.ts +++ b/apps/desktop/src/utils.ts @@ -20,11 +20,12 @@ 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); + // // 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() { From aeb3b9f94b95faf04b7d61d1c2da4c61f383f618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 31 Mar 2025 21:53:09 +0200 Subject: [PATCH 02/98] PM-19138: Add try-catch to desktop-autofill (#13964) --- .../services/desktop-autofill.service.ts | 132 +++++++++--------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 70a9280d8ef..9912e9459a1 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -147,7 +147,7 @@ export class DesktopAutofillService implements OnDestroy { } listenIpc() { - ipc.autofill.listenPasskeyRegistration((clientId, sequenceNumber, request, callback) => { + ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => { this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request); this.logService.warning( "listenPasskeyRegistration2", @@ -155,19 +155,19 @@ export class DesktopAutofillService implements OnDestroy { ); const controller = new AbortController(); - void this.fido2AuthenticatorService - .makeCredential( + + try { + const response = await this.fido2AuthenticatorService.makeCredential( this.convertRegistrationRequest(request), { windowXy: request.windowXy }, controller, - ) - .then((response) => { - callback(null, this.convertRegistrationResponse(request, response)); - }) - .catch((error) => { - this.logService.error("listenPasskeyRegistration error", error); - callback(error, null); - }); + ); + + callback(null, this.convertRegistrationResponse(request, response)); + } catch (error) { + this.logService.error("listenPasskeyRegistration error", error); + callback(error, null); + } }); ipc.autofill.listenPasskeyAssertionWithoutUserInterface( @@ -179,55 +179,56 @@ export class DesktopAutofillService implements OnDestroy { request, ); - // For some reason the credentialId is passed as an empty array in the request, so we need to - // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. - if (request.recordIdentifier && request.credentialId.length === 0) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(getOptionalUserId), - ); - if (!activeUserId) { - this.logService.error("listenPasskeyAssertion error", "Active user not found"); - callback(new Error("Active user not found"), null); - return; - } - - const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId); - if (!cipher) { - this.logService.error("listenPasskeyAssertion error", "Cipher not found"); - callback(new Error("Cipher not found"), null); - return; - } - - const decrypted = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); - - const fido2Credential = decrypted.login.fido2Credentials?.[0]; - if (!fido2Credential) { - this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); - callback(new Error("Fido2Credential not found"), null); - return; - } - - request.credentialId = Array.from( - parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId), - ); - } - const controller = new AbortController(); - void this.fido2AuthenticatorService - .getAssertion( + + try { + // For some reason the credentialId is passed as an empty array in the request, so we need to + // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. + if (request.recordIdentifier && request.credentialId.length === 0) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (!activeUserId) { + this.logService.error("listenPasskeyAssertion error", "Active user not found"); + callback(new Error("Active user not found"), null); + return; + } + + const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId); + if (!cipher) { + this.logService.error("listenPasskeyAssertion error", "Cipher not found"); + callback(new Error("Cipher not found"), null); + return; + } + + const decrypted = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + + const fido2Credential = decrypted.login.fido2Credentials?.[0]; + if (!fido2Credential) { + this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); + callback(new Error("Fido2Credential not found"), null); + return; + } + + request.credentialId = Array.from( + parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId), + ); + } + + const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request), { windowXy: request.windowXy }, controller, - ) - .then((response) => { - callback(null, this.convertAssertionResponse(request, response)); - }) - .catch((error) => { - this.logService.error("listenPasskeyAssertion error", error); - callback(error, null); - }); + ); + + callback(null, this.convertAssertionResponse(request, response)); + } catch (error) { + this.logService.error("listenPasskeyAssertion error", error); + callback(error, null); + return; + } }, ); @@ -235,19 +236,18 @@ export class DesktopAutofillService implements OnDestroy { this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request); const controller = new AbortController(); - void this.fido2AuthenticatorService - .getAssertion( + try { + const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request), { windowXy: request.windowXy }, controller, - ) - .then((response) => { - callback(null, this.convertAssertionResponse(request, response)); - }) - .catch((error) => { - this.logService.error("listenPasskeyAssertion error", error); - callback(error, null); - }); + ); + + callback(null, this.convertAssertionResponse(request, response)); + } catch (error) { + this.logService.error("listenPasskeyAssertion error", error); + callback(error, null); + } }); } From 849aa546d463f776573a1368097d95f48ed99e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 7 Apr 2025 14:31:42 +0200 Subject: [PATCH 03/98] PM-19424: React to IPC disconnect (#14123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * React to IPC disconnects * Minor cleanup * Update apps/desktop/package.json Co-authored-by: Daniel García * Relaxed ordering --------- Co-authored-by: Daniel García --- .../desktop_native/macos_provider/src/lib.rs | 24 ++++++++++ .../CredentialProviderViewController.swift | 48 +++++++++++++++++++ apps/desktop/package.json | 1 + 3 files changed, 73 insertions(+) diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 8f2499ae68d..26409f14a96 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -49,6 +49,12 @@ trait Callback: Send + Sync { fn error(&self, error: BitwardenError); } +#[derive(uniffi::Enum, Debug)] +pub enum ConnectionStatus { + Connected, + Disconnected, +} + #[derive(uniffi::Object)] pub struct MacOSProviderClient { to_server_send: tokio::sync::mpsc::Sender, @@ -57,6 +63,9 @@ pub struct MacOSProviderClient { response_callbacks_counter: AtomicU32, #[allow(clippy::type_complexity)] response_callbacks_queue: Arc, Instant)>>>, + + // Flag to track connection status - atomic for thread safety without locks + connection_status: Arc, } #[uniffi::export] @@ -74,11 +83,13 @@ impl MacOSProviderClient { to_server_send, response_callbacks_counter: AtomicU32::new(0), response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), + connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)), }; let path = desktop_core::ipc::path("autofill"); let queue = client.response_callbacks_queue.clone(); + let connection_status = client.connection_status.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() @@ -96,9 +107,11 @@ impl MacOSProviderClient { match serde_json::from_str::(&message) { Ok(SerializedMessage::Command(CommandMessage::Connected)) => { info!("Connected to server"); + connection_status.store(true, std::sync::atomic::Ordering::Relaxed); } Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => { info!("Disconnected from server"); + connection_status.store(false, std::sync::atomic::Ordering::Relaxed); } Ok(SerializedMessage::Message { sequence_number, @@ -159,6 +172,17 @@ impl MacOSProviderClient { ) { self.send_message(request, Box::new(callback)); } + + pub fn get_connection_status(&self) -> ConnectionStatus { + let is_connected = self + .connection_status + .load(std::sync::atomic::Ordering::Relaxed); + if is_connected { + ConnectionStatus::Connected + } else { + ConnectionStatus::Disconnected + } + } } #[derive(Serialize, Deserialize)] diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 5568b2e75db..bf541e233d9 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -62,12 +62,56 @@ class CredentialProviderViewController: ASCredentialProviderViewController { return MacOsProviderClient.connect() }() + // Timer for checking connection status + private var connectionMonitorTimer: Timer? + private var lastConnectionStatus: ConnectionStatus = .disconnected + + // Setup the connection monitoring timer + private func setupConnectionMonitoring() { + // Check connection status every 1 second + connectionMonitorTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.checkConnectionStatus() + } + + // Make sure timer runs even when UI is busy + RunLoop.current.add(connectionMonitorTimer!, forMode: .common) + + // Initial check + checkConnectionStatus() + } + + // Check the connection status by calling into Rust + private func checkConnectionStatus() { + // Get the current connection status from Rust + let currentStatus = client.getConnectionStatus() + + // Only post notification if state changed + if currentStatus != lastConnectionStatus { + if(currentStatus == .connected) { + logger.log("[autofill-extension] Connection status changed: Connected") + } else { + logger.log("[autofill-extension] Connection status changed: Disconnected") + } + + // Save the new status + lastConnectionStatus = currentStatus + + // If we just disconnected, try to cancel the request + if currentStatus == .disconnected { + self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Bitwarden desktop app disconnected")) + } + } + } + init() { logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") logger.log("[autofill-extension] initializing extension") super.init(nibName: nil, bundle: nil) + + // Setup connection monitoring now that self is available + setupConnectionMonitoring() } required init?(coder: NSCoder) { @@ -76,6 +120,10 @@ class CredentialProviderViewController: ASCredentialProviderViewController { deinit { logger.log("[autofill-extension] deinitializing extension") + + // Stop the connection monitor timer + connectionMonitorTimer?.invalidate() + connectionMonitorTimer = nil } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d4fe93d05b9..fd524269838 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,6 +18,7 @@ "scripts": { "postinstall": "electron-rebuild", "start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build", + "build-native-macos": "cd desktop_native && ./macos_provider/build.sh && node build.js cross-platform", "build-native": "cd desktop_native && node build.js", "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", From 92e9dca6b47508ea1599cfe74f17a6b36d494231 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Tue, 8 Apr 2025 16:59:23 +0200 Subject: [PATCH 04/98] Autofill/pm 9034 implement passkey for unlocked accounts (#13826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Passkey stuff Co-authored-by: Anders Åberg * Ugly hacks * Work On Modal State Management * Applying modalStyles * modal * Improved hide/show * fixed promise * File name * fix prettier * Protecting against null API's and undefined data * Only show fake popup to devs * cleanup mock code * rename minmimal-app to modal-app * Added comment * Added comment * removed old comment * Avoided changing minimum size * Add small comment * Rename component * adress feedback * Fixed uppercase file * Fixed build * Added codeowners * added void * commentary * feat: reset setting on app start * Moved reset to be in main / process launch * Add comment to create window * Added a little bit of styling * Use Messaging service to loadUrl * Enable passkeysautofill * Add logging * halfbaked * Integration working * And now it works without extra delay * Clean up * add note about messaging * lb * removed console.logs * Cleanup and adress review feedback * This hides the swift UI * add modal components * update modal with correct ciphers and functionality * add create screen * pick credential, draft * Remove logger * a whole lot of wiring * not working * Improved wiring * Cancel after 90s * Introduced observable * update cipher handling * update to use matchesUri * Launching bitwarden if its not running * Passing position from native to electron * Rename inModalMode to modalMode * remove tap * revert spaces * added back isDev * cleaned up a bit * Cleanup swift file * tweaked logging * clean up * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu * Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts Co-authored-by: Andreas Coroiu * Update apps/desktop/src/platform/services/desktop-settings.service.ts Co-authored-by: Andreas Coroiu * adress position feedback * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu * Removed extra logging * Adjusted error logging * Use .error to log errors * remove dead code * Update desktop-autofill.service.ts * use parseCredentialId instead of guidToRawFormat * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Andreas Coroiu * Change windowXy to a Record instead of [number,number] * Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts Co-authored-by: Andreas Coroiu * Remove unsued dep and comment * changed timeout to be spec recommended maxium, 10 minutes, for now. * Correctly assume UP * Removed extra cancelRequest in deinint * Add timeout and UV to confirmChoseCipher UV is performed by UI, not the service * Improved docs regarding undefined cipherId * cleanup: UP is no longer undefined * Run completeError if ipc messages conversion failed * don't throw, instead return undefined * Disabled passkey provider * Throw error if no activeUserId was found * removed comment * Fixed lint * removed unsued service * reset entitlement formatting * Update entitlements.mas.plist * Fix build issues * Fix import issues * Update route names to use `fido2` * Fix being unable to select a passkey * Fix linting issues * Followup to fix merge issues and other comments * Update `userHandle` value * Add error handling for missing session or other errors * Remove unused route * Fix linting issues * Simplify updateCredential method * Followup to remove comments and timeouts and handle errors * Address lint issue by using `takeUntilDestroyed` * PR Followup for typescript and vault concerns * Add try block for cipher creation * Make userId manditory for cipher service --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Anders Åberg Co-authored-by: Anders Åberg Co-authored-by: Colton Hurst Co-authored-by: Andreas Coroiu Co-authored-by: Evan Bassler Co-authored-by: Andreas Coroiu --- apps/desktop/src/app/app-routing.module.ts | 11 +- .../components/fido2placeholder.component.ts | 4 +- .../services/desktop-autofill.service.ts | 4 - .../desktop-fido2-user-interface.service.ts | 99 ++++++++---- apps/desktop/src/locales/en/messages.json | 21 +++ apps/desktop/src/main/tray.main.ts | 11 +- .../create/fido2-create.component.html | 48 ++++++ .../passkeys/create/fido2-create.component.ts | 143 ++++++++++++++++++ .../modal/passkeys/fido2-vault.component.html | 36 +++++ .../modal/passkeys/fido2-vault.component.ts | 102 +++++++++++++ ...ido2-user-interface.service.abstraction.ts | 2 +- libs/components/src/icon-button/index.ts | 1 + 12 files changed, 439 insertions(+), 43 deletions(-) create mode 100644 apps/desktop/src/modal/passkeys/create/fido2-create.component.html create mode 100644 apps/desktop/src/modal/passkeys/create/fido2-create.component.ts create mode 100644 apps/desktop/src/modal/passkeys/fido2-vault.component.html create mode 100644 apps/desktop/src/modal/passkeys/fido2-vault.component.ts diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 3bb130d321d..3b4d5994e8b 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -55,9 +55,10 @@ import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; +import { Fido2CreateComponent } from "../modal/passkeys/create/fido2-create.component"; +import { Fido2VaultComponent } from "../modal/passkeys/fido2-vault.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; -import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; import { SendComponent } from "./tools/send/send.component"; /** @@ -179,12 +180,12 @@ const routes: Routes = [ canActivate: [authGuard], }, { - path: "passkeys", - component: Fido2PlaceholderComponent, + path: "fido2-assertion", + component: Fido2VaultComponent, }, { - path: "passkeys", - component: Fido2PlaceholderComponent, + path: "fido2-creation", + component: Fido2CreateComponent, }, { path: "", diff --git a/apps/desktop/src/app/components/fido2placeholder.component.ts b/apps/desktop/src/app/components/fido2placeholder.component.ts index b95dcc6d890..2982b380939 100644 --- a/apps/desktop/src/app/components/fido2placeholder.component.ts +++ b/apps/desktop/src/app/components/fido2placeholder.component.ts @@ -97,7 +97,7 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy { // userVerification: true, // }); - this.session.notifyConfirmNewCredential(true); + this.session.notifyConfirmCreateCredential(true); // Not sure this clean up should happen here or in session. // The session currently toggles modal on and send us here @@ -113,7 +113,7 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy { await this.router.navigate(["/"]); await this.desktopSettingsService.setModalMode(false); - this.session.notifyConfirmNewCredential(false); + this.session.notifyConfirmCreateCredential(false); // little bit hacky: this.session.confirmChosenCipher(null); } diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 9912e9459a1..b7d9894907e 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -60,10 +60,6 @@ export class DesktopAutofillService implements OnDestroy { .pipe( distinctUntilChanged(), switchMap((enabled) => { - // if (!enabled) { - // return EMPTY; - // } - return this.accountService.activeAccount$.pipe( map((account) => account?.id), filter((userId): userId is UserId => userId != null), diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 3caf13fa5b7..2763e439c7d 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -94,9 +94,12 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi ) {} private confirmCredentialSubject = new Subject(); - private createdCipher: Cipher; - private availableCipherIdsSubject = new BehaviorSubject(null); + private createdCipher: Cipher = new Cipher(); + private updatedCipher: CipherView = new CipherView(); + + private rpId = new BehaviorSubject(""); + private availableCipherIdsSubject = new BehaviorSubject([""]); /** * Observable that emits available cipher IDs once they're confirmed by the UI */ @@ -136,15 +139,15 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi // make the cipherIds available to the UI. this.availableCipherIdsSubject.next(cipherIds); - await this.showUi("/passkeys", this.windowObject.windowXy); + await this.showUi("/fido2-assertion", this.windowObject.windowXy); const chosenCipherResponse = await this.waitForUiChosenCipher(); this.logService.debug("Received chosen cipher", chosenCipherResponse); return { - cipherId: chosenCipherResponse.cipherId, - userVerified: chosenCipherResponse.userVerified, + cipherId: chosenCipherResponse?.cipherId, + userVerified: chosenCipherResponse?.userVerified, }; } finally { // Make sure to clean up so the app is never stuck in modal mode? @@ -152,6 +155,15 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } } + async getRpId(): Promise { + return lastValueFrom( + this.rpId.pipe( + filter((id) => id != null), + take(1), + ), + ); + } + confirmChosenCipher(cipherId: string, userVerified: boolean = false): void { this.chosenCipherSubject.next({ cipherId, userVerified }); this.chosenCipherSubject.complete(); @@ -159,7 +171,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private async waitForUiChosenCipher( timeoutMs: number = 60000, - ): Promise<{ cipherId: string; userVerified: boolean } | undefined> { + ): Promise<{ cipherId?: string; userVerified: boolean } | undefined> { try { return await lastValueFrom(this.chosenCipherSubject.pipe(timeout(timeoutMs))); } catch { @@ -174,7 +186,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi /** * Notifies the Fido2UserInterfaceSession that the UI operations has completed and it can return to the OS. */ - notifyConfirmNewCredential(confirmed: boolean): void { + notifyConfirmCreateCredential(confirmed: boolean, updatedCipher?: CipherView): void { + if (updatedCipher) { + this.updatedCipher = updatedCipher; + } this.confirmCredentialSubject.next(confirmed); this.confirmCredentialSubject.complete(); } @@ -195,42 +210,43 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi async confirmNewCredential({ credentialName, userName, + userHandle, userVerification, rpId, - }: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { + }: NewCredentialParams): Promise<{ cipherId?: string; userVerified: boolean }> { this.logService.warning( "confirmNewCredential", credentialName, userName, + userHandle, userVerification, rpId, ); + this.rpId.next(rpId); try { - await this.showUi("/passkeys", this.windowObject.windowXy); + await this.showUi("/fido2-creation", this.windowObject.windowXy); // Wait for the UI to wrap up const confirmation = await this.waitForUiNewCredentialConfirmation(); if (!confirmation) { return { cipherId: undefined, userVerified: false }; } - // Create the credential - await this.createCredential({ - credentialName, - userName, - rpId, - userHandle: "", - userVerification, - }); - // wait for 10ms to help RXJS catch up(?) - // We sometimes get a race condition from this.createCredential not updating cipherService in time - //console.log("waiting 10ms.."); - //await new Promise((resolve) => setTimeout(resolve, 10)); - //console.log("Just waited 10ms"); - - // Return the new cipher (this.createdCipher) - return { cipherId: this.createdCipher.id, userVerified: userVerification }; + if (this.updatedCipher) { + await this.updateCredential(this.updatedCipher); + return { cipherId: this.updatedCipher.id, userVerified: userVerification }; + } else { + // Create the credential + await this.createCipher({ + credentialName, + userName, + rpId, + userHandle, + userVerification, + }); + return { cipherId: this.createdCipher.id, userVerified: userVerification }; + } } finally { // Make sure to clean up so the app is never stuck in modal mode? await this.desktopSettingsService.setModalMode(false); @@ -240,15 +256,16 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private async showUi(route: string, position?: { x: number; y: number }): Promise { // Load the UI: await this.desktopSettingsService.setModalMode(true, position); - await this.router.navigate(["/passkeys"]); + await this.router.navigate([route]); } /** * Can be called by the UI to create a new credential with user input etc. * @param param0 */ - async createCredential({ credentialName, userName, rpId }: NewCredentialParams): Promise { + async createCipher({ credentialName, userName, rpId }: NewCredentialParams): Promise { // Store the passkey on a new cipher to avoid replacing something important + const cipher = new CipherView(); cipher.name = credentialName; @@ -267,12 +284,34 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + if (!activeUserId) { + throw new Error("No active user ID found!"); + } + const encCipher = await this.cipherService.encrypt(cipher, activeUserId); - const createdCipher = await this.cipherService.createWithServer(encCipher); - this.createdCipher = createdCipher; + try { + const createdCipher = await this.cipherService.createWithServer(encCipher); + this.createdCipher = createdCipher; - return createdCipher; + return createdCipher; + } catch { + throw new Error("Unable to create cipher"); + } + } + + async updateCredential(cipher: CipherView): Promise { + this.logService.warning("updateCredential"); + await firstValueFrom( + this.accountService.activeAccount$.pipe( + map(async (a) => { + if (a) { + const encCipher = await this.cipherService.encrypt(cipher, a.id); + await this.cipherService.updateWithServer(encCipher); + } + }), + ), + ); } async informExcludedCredential(existingCipherIds: string[]): Promise { diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index f93db44aa69..168753a1f2c 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -797,6 +797,12 @@ "unexpectedError": { "message": "An unexpected error has occurred." }, + "unexpectedErrorShort": { + "message": "Unexpected error" + }, + "closeThisBitwardenWindow": { + "message": "Close this Bitwarden window and try again." + }, "itemInformation": { "message": "Item information" }, @@ -3559,6 +3565,21 @@ "changeAcctEmail": { "message": "Change account email" }, + "passkeyLogin": { + "message": "Log in with passkey?" + }, + "savePasskeyQuestion": { + "message": "Save passkey?" + }, + "saveNewPasskey": { + "message": "Save as new login" + }, + "unableToSavePasskey": { + "message": "Unable to save passkey" + }, + "closeBitwarden": { + "message": "Close Bitwarden" + }, "allowScreenshots": { "message": "Allow screen capture" }, diff --git a/apps/desktop/src/main/tray.main.ts b/apps/desktop/src/main/tray.main.ts index b7ddefe6e1b..81df6497ca8 100644 --- a/apps/desktop/src/main/tray.main.ts +++ b/apps/desktop/src/main/tray.main.ts @@ -53,9 +53,14 @@ export class TrayMain { }, { visible: isDev(), - label: "Fake Popup", + label: "Fake Popup Select", click: () => this.fakePopup(), }, + { + visible: isDev(), + label: "Fake Popup Create", + click: () => this.fakePopupCreate(), + }, { type: "separator" }, { label: this.i18nService.t("exit"), @@ -218,4 +223,8 @@ export class TrayMain { private async fakePopup() { await this.messagingService.send("loadurl", { url: "/passkeys", modal: true }); } + + private async fakePopupCreate() { + await this.messagingService.send("loadurl", { url: "/create-passkey", modal: true }); + } } diff --git a/apps/desktop/src/modal/passkeys/create/fido2-create.component.html b/apps/desktop/src/modal/passkeys/create/fido2-create.component.html new file mode 100644 index 00000000000..e3423d6d7f8 --- /dev/null +++ b/apps/desktop/src/modal/passkeys/create/fido2-create.component.html @@ -0,0 +1,48 @@ +
+ + +
+ + +

+ {{ "savePasskeyQuestion" | i18n }} +

+
+ + +
+
+ + + + + {{ c.subTitle }} + Save + + + + + + + + +
diff --git a/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts b/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts new file mode 100644 index 00000000000..776ceae9d85 --- /dev/null +++ b/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts @@ -0,0 +1,143 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { RouterModule, Router } from "@angular/router"; +import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield } from "@bitwarden/auth/angular"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + DialogService, + BadgeModule, + ButtonModule, + DialogModule, + IconModule, + ItemModule, + SectionComponent, + TableModule, + SectionHeaderComponent, + BitIconButtonComponent, +} from "@bitwarden/components"; + +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../../autofill/services/desktop-fido2-user-interface.service"; +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; + +@Component({ + standalone: true, + imports: [ + CommonModule, + RouterModule, + SectionHeaderComponent, + BitIconButtonComponent, + TableModule, + JslibModule, + IconModule, + ButtonModule, + DialogModule, + SectionComponent, + ItemModule, + BadgeModule, + ], + templateUrl: "fido2-create.component.html", +}) +export class Fido2CreateComponent implements OnInit { + session?: DesktopFido2UserInterfaceSession = null; + private ciphersSubject = new BehaviorSubject([]); + ciphers$: Observable = this.ciphersSubject.asObservable(); + readonly Icons = { BitwardenShield }; + + constructor( + private readonly desktopSettingsService: DesktopSettingsService, + private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, + private readonly accountService: AccountService, + private readonly cipherService: CipherService, + private readonly dialogService: DialogService, + private readonly domainSettingsService: DomainSettingsService, + private readonly logService: LogService, + private readonly router: Router, + ) {} + + async ngOnInit() { + this.session = this.fido2UserInterfaceService.getCurrentSession(); + const rpid = await this.session.getRpId(); + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(rpid), + ); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.cipherService + .getAllDecrypted(activeUserId) + .then((ciphers) => { + const relevantCiphers = ciphers.filter((cipher) => { + if (!cipher.login || !cipher.login.hasUris) { + return false; + } + + return ( + cipher.login.matchesUri(rpid, equivalentDomains) && + (!cipher.login.fido2Credentials || cipher.login.fido2Credentials.length === 0) + ); + }); + this.ciphersSubject.next(relevantCiphers); + }) + .catch((error) => this.logService.error(error)); + } + + async addPasskeyToCipher(cipher: CipherView) { + this.session.notifyConfirmCreateCredential(true, cipher); + } + + async confirmPasskey() { + try { + // Retrieve the current UI session to control the flow + if (!this.session) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "unexpectedErrorShort" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeBitwarden" }, + cancelButtonText: null, + }); + if (confirmed) { + await this.closeModal(); + } + } else { + this.session.notifyConfirmCreateCredential(true); + } + + // Not sure this clean up should happen here or in session. + // 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.setModalMode(false); + } catch { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "unableToSavePasskey" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeBitwarden" }, + cancelButtonText: null, + }); + + if (confirmed) { + await this.closeModal(); + } + } + } + + async closeModal() { + await this.router.navigate(["/"]); + await this.desktopSettingsService.setModalMode(false); + this.session.notifyConfirmCreateCredential(false); + this.session.confirmChosenCipher(null); + } +} diff --git a/apps/desktop/src/modal/passkeys/fido2-vault.component.html b/apps/desktop/src/modal/passkeys/fido2-vault.component.html new file mode 100644 index 00000000000..edc241c4d21 --- /dev/null +++ b/apps/desktop/src/modal/passkeys/fido2-vault.component.html @@ -0,0 +1,36 @@ +
+ + +
+ + +

{{ "passkeyLogin" | i18n }}

+
+ +
+
+ + + + + {{ c.subTitle }} + Select + + + +
diff --git a/apps/desktop/src/modal/passkeys/fido2-vault.component.ts b/apps/desktop/src/modal/passkeys/fido2-vault.component.ts new file mode 100644 index 00000000000..0549d70fa0a --- /dev/null +++ b/apps/desktop/src/modal/passkeys/fido2-vault.component.ts @@ -0,0 +1,102 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { RouterModule, Router } from "@angular/router"; +import { firstValueFrom, map, BehaviorSubject, Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield } from "@bitwarden/auth/angular"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + BadgeModule, + ButtonModule, + DialogModule, + IconModule, + ItemModule, + SectionComponent, + TableModule, + BitIconButtonComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; + +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../autofill/services/desktop-fido2-user-interface.service"; +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; + +@Component({ + standalone: true, + imports: [ + CommonModule, + RouterModule, + SectionHeaderComponent, + BitIconButtonComponent, + TableModule, + JslibModule, + IconModule, + ButtonModule, + DialogModule, + SectionComponent, + ItemModule, + BadgeModule, + ], + templateUrl: "fido2-vault.component.html", +}) +export class Fido2VaultComponent implements OnInit, OnDestroy { + session?: DesktopFido2UserInterfaceSession = null; + private ciphersSubject = new BehaviorSubject([]); + ciphers$: Observable = this.ciphersSubject.asObservable(); + private cipherIdsSubject = new BehaviorSubject([]); + cipherIds$: Observable; + readonly Icons = { BitwardenShield }; + + constructor( + private readonly desktopSettingsService: DesktopSettingsService, + private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, + private readonly cipherService: CipherService, + private readonly accountService: AccountService, + private readonly logService: LogService, + private readonly router: Router, + ) {} + + async ngOnInit() { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.session = this.fido2UserInterfaceService.getCurrentSession(); + this.cipherIds$ = this.session?.availableCipherIds$; + + this.cipherIds$.pipe(takeUntilDestroyed()).subscribe((cipherIds) => { + this.cipherService + .getAllDecrypted(activeUserId) + .then((ciphers) => { + this.ciphersSubject.next(ciphers.filter((cipher) => cipherIds.includes(cipher.id))); + }) + .catch((error) => this.logService.error(error)); + }); + } + + ngOnDestroy() { + this.cipherIdsSubject.complete(); // Clean up the BehaviorSubject + } + + async chooseCipher(cipherId: string) { + this.session?.confirmChosenCipher(cipherId, true); + + await this.router.navigate(["/"]); + await this.desktopSettingsService.setModalMode(false); + } + + async closeModal() { + await this.router.navigate(["/"]); + await this.desktopSettingsService.setModalMode(false); + + this.session.notifyConfirmCreateCredential(false); + this.session.confirmChosenCipher(null); + } +} diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 1f871f6c70f..b8aed853eac 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -97,7 +97,7 @@ export abstract class Fido2UserInterfaceSession { */ confirmNewCredential: ( params: NewCredentialParams, - ) => Promise<{ cipherId: string; userVerified: boolean }>; + ) => Promise<{ cipherId?: string; userVerified: boolean }>; /** * Make sure that the vault is unlocked. diff --git a/libs/components/src/icon-button/index.ts b/libs/components/src/icon-button/index.ts index 9da4a3162bf..b753e53c96a 100644 --- a/libs/components/src/icon-button/index.ts +++ b/libs/components/src/icon-button/index.ts @@ -1 +1,2 @@ export * from "./icon-button.module"; +export * from "./icon-button.component"; From d902a0d953fbe2c49d2c28544d07ada6fc02870c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 8 Apr 2025 19:07:46 +0200 Subject: [PATCH 05/98] PM-11455: Trigger sync when user enables OS setting (#14127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implemented a SendNativeStatus command This allows reporting status or asking the electron app to do something. * fmt * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Daniel García * clean up * Don't add empty callbacks * Removed comment --------- Co-authored-by: Daniel García --- .../desktop_native/macos_provider/src/lib.rs | 50 +++++++++++++------ apps/desktop/desktop_native/napi/index.d.ts | 6 ++- apps/desktop/desktop_native/napi/src/lib.rs | 34 ++++++++++++- .../CredentialProviderViewController.swift | 5 ++ .../macos/autofill-extension/Info.plist | 8 +-- apps/desktop/src/autofill/preload.ts | 19 +++++++ .../services/desktop-autofill.service.ts | 23 +++++++++ .../main/autofill/native-autofill.main.ts | 14 ++++++ 8 files changed, 137 insertions(+), 22 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 26409f14a96..1a70b49b69e 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -68,6 +68,13 @@ pub struct MacOSProviderClient { connection_status: Arc, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeStatus { + key: String, + value: String, +} + #[uniffi::export] impl MacOSProviderClient { #[uniffi::constructor] @@ -81,7 +88,7 @@ impl MacOSProviderClient { let client = MacOSProviderClient { to_server_send, - response_callbacks_counter: AtomicU32::new(0), + response_callbacks_counter: AtomicU32::new(1), // 0 is reserved for no callback response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)), }; @@ -149,12 +156,17 @@ impl MacOSProviderClient { client } + pub fn send_native_status(&self, key: String, value: String) { + let status = NativeStatus { key, value }; + self.send_message(status, None); + } + pub fn prepare_passkey_registration( &self, request: PasskeyRegistrationRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); } pub fn prepare_passkey_assertion( @@ -162,7 +174,7 @@ impl MacOSProviderClient { request: PasskeyAssertionRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); } pub fn prepare_passkey_assertion_without_user_interface( @@ -170,7 +182,7 @@ impl MacOSProviderClient { request: PasskeyAssertionWithoutUserInterfaceRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); } pub fn get_connection_status(&self) -> ConnectionStatus { @@ -219,9 +231,13 @@ impl MacOSProviderClient { fn send_message( &self, message: impl Serialize + DeserializeOwned, - callback: Box, + callback: Option>, ) { - let sequence_number = self.add_callback(callback); + let sequence_number = if let Some(cb) = callback { + self.add_callback(cb) + } else { + 0 // Special value indicating "no callback" + }; let message = serde_json::to_string(&SerializedMessage::Message { sequence_number, @@ -231,16 +247,18 @@ impl MacOSProviderClient { if let Err(e) = self.to_server_send.blocking_send(message) { // Make sure we remove the callback from the queue if we can't send the message - if let Some((cb, _)) = self - .response_callbacks_queue - .lock() - .unwrap() - .remove(&sequence_number) - { - cb.error(BitwardenError::Internal(format!( - "Error sending message: {}", - e - ))); + if sequence_number != 0 { + if let Some((cb, _)) = self + .response_callbacks_queue + .lock() + .unwrap() + .remove(&sequence_number) + { + cb.error(BitwardenError::Internal(format!( + "Error sending message: {}", + e + ))); + } } } } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index ca1fe29e254..0d26be18bdb 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -154,6 +154,10 @@ export declare namespace autofill { userVerification: UserVerification windowXy: Position } + export interface NativeStatus { + key: string + value: string + } export interface PasskeyAssertionResponse { rpId: string userHandle: Array @@ -169,7 +173,7 @@ export declare namespace autofill { * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. * @param callback This function will be called whenever a message is received from a client. */ - static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void): Promise + static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void, nativeStatusCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void): Promise /** Return the path to the IPC server. */ getPath(): string /** Stop the IPC server. */ diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index f02be2b27b6..28eaa61df17 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -572,6 +572,14 @@ pub mod autofill { pub window_xy: Position, } + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct NativeStatus { + pub key: String, + pub value: String, + } + #[napi(object)] #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -621,6 +629,13 @@ pub mod autofill { (u32, u32, PasskeyAssertionWithoutUserInterfaceRequest), ErrorStrategy::CalleeHandled, >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" + )] + native_status_callback: ThreadsafeFunction< + (u32, u32, NativeStatus), + ErrorStrategy::CalleeHandled, + >, ) -> napi::Result { let (send, mut recv) = tokio::sync::mpsc::channel::(32); tokio::spawn(async move { @@ -689,7 +704,24 @@ pub mod autofill { continue; } Err(e) => { - println!("[ERROR] Error deserializing message2: {e}"); + println!( + "[ERROR] Error deserializing registration request: {e}" + ); + } + } + + match serde_json::from_str::>(&message) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + native_status_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + println!("[ERROR] Error deserializing native status: {e}"); } } diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index bf541e233d9..0e6c0fc7023 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -155,6 +155,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController { view.isHidden = true self.view = view } + + override func prepareInterfaceForExtensionConfiguration() { + client.sendNativeStatus(key: "request-sync", value: "") + self.extensionContext.completeExtensionConfigurationRequest() + } override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) { let timeoutTimer = createTimer() diff --git a/apps/desktop/macos/autofill-extension/Info.plist b/apps/desktop/macos/autofill-extension/Info.plist index 539cfa35b9d..a8ae0f021ad 100644 --- a/apps/desktop/macos/autofill-extension/Info.plist +++ b/apps/desktop/macos/autofill-extension/Info.plist @@ -9,10 +9,10 @@ ASCredentialProviderExtensionCapabilities ProvidesPasskeys - + + ShowsConfigurationUI + - ASCredentialProviderExtensionShowsConfigurationUI - NSExtensionPointIdentifier com.apple.authentication-services-credential-provider-ui @@ -20,4 +20,4 @@ $(PRODUCT_MODULE_NAME).CredentialProviderViewController - + \ No newline at end of file diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 2c006b5c928..537a4fdaf4e 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -127,4 +127,23 @@ export default { }, ); }, + + listenNativeStatus: ( + fn: (clientId: number, sequenceNumber: number, status: { key: string; value: string }) => void, + ) => { + ipcRenderer.on( + "autofill.nativeStatus", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + status: { key: string; value: string }; + }, + ) => { + const { clientId, sequenceNumber, status } = data; + fn(clientId, sequenceNumber, status); + }, + ); + }, }; diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index b7d9894907e..b72c6974c97 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -77,6 +77,20 @@ export class DesktopAutofillService implements OnDestroy { this.listenIpc(); } + async adHocSync(): Promise { + this.logService.info("Performing AdHoc sync"); + const account = await firstValueFrom(this.accountService.activeAccount$); + const userId = account?.id; + + if (!userId) { + throw new Error("No active user found"); + } + + const cipherViewMap = await firstValueFrom(this.cipherService.cipherViews$(userId)); + this.logService.info("Performing AdHoc sync", Object.values(cipherViewMap ?? [])); + await this.sync(Object.values(cipherViewMap ?? [])); + } + /** Give metadata about all available credentials in the users vault */ async sync(cipherViews: CipherView[]) { const status = await this.status(); @@ -245,6 +259,15 @@ export class DesktopAutofillService implements OnDestroy { callback(error, null); } }); + + // Listen for native status messages + ipc.autofill.listenNativeStatus(async (clientId, sequenceNumber, status) => { + this.logService.info("Received native status", status.key, status.value); + if (status.key === "request-sync") { + // perform ad-hoc sync + await this.adHocSync(); + } + }); } private convertRegistrationRequest( diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index f66eea180cf..ae901d75c1d 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -75,6 +75,20 @@ export class NativeAutofillMain { request, }); }, + // NativeStatusCallback + (error, clientId, sequenceNumber, status) => { + if (error) { + this.logService.error("autofill.IpcServer.nativeStatus", error); + this.ipcServer.completeError(clientId, sequenceNumber, String(error)); + return; + } + this.logService.info("Received native status", status); + this.windowMain.win.webContents.send("autofill.nativeStatus", { + clientId, + sequenceNumber, + status, + }); + }, ); ipcMain.on("autofill.completePasskeyRegistration", (event, data) => { From b676f9b8a5d3f5d88e408ab1e2adaf97801f24b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Wed, 19 Mar 2025 23:52:12 +0100 Subject: [PATCH 06/98] Added support for handling a locked vault Handle unlocktimeout --- .../desktop-fido2-user-interface.service.ts | 38 +++++++++++++++++-- .../src/lock/components/lock.component.ts | 8 +++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 2763e439c7d..a8103a99972 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -253,10 +253,24 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } } - private async showUi(route: string, position?: { x: number; y: number }): Promise { + private async hideUi(): Promise { + await this.desktopSettingsService.setModalMode(false); + await this.router.navigate(["/"]); + } + + private async showUi( + route: string, + position?: { x: number; y: number }, + disableRedirect?: boolean, + ): Promise { // Load the UI: await this.desktopSettingsService.setModalMode(true, position); - await this.router.navigate([route]); + await this.router.navigate([ + route, + { + "disable-redirect": disableRedirect || null, + }, + ]); } /** @@ -323,7 +337,25 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi const status = await firstValueFrom(this.authService.activeAccountStatus$); if (status !== AuthenticationStatus.Unlocked) { - throw new Error("Vault is not unlocked"); + await this.showUi("/lock", this.windowObject.windowXy, true); + + let status2: AuthenticationStatus; + try { + status2 = await lastValueFrom( + this.authService.activeAccountStatus$.pipe( + filter((s) => s === AuthenticationStatus.Unlocked), + take(1), + timeout(1000 * 60 * 5), // 5 minutes + ), + ); + } catch (error) { + this.logService.warning("Error while waiting for vault to unlock", error); + } + + if (status2 !== AuthenticationStatus.Unlocked) { + await this.hideUi(); + throw new Error("Vault is not unlocked"); + } } } diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index b50c7d23337..3185166bbf5 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; +import { Router, ActivatedRoute } from "@angular/router"; import { BehaviorSubject, firstValueFrom, @@ -136,6 +136,7 @@ export class LockComponent implements OnInit, OnDestroy { private keyService: KeyService, private platformUtilsService: PlatformUtilsService, private router: Router, + private activatedRoute: ActivatedRoute, private dialogService: DialogService, private messagingService: MessagingService, private biometricStateService: BiometricStateService, @@ -621,7 +622,10 @@ export class LockComponent implements OnInit, OnDestroy { } // determine success route based on client type - if (this.clientType != null) { + if ( + this.clientType != null && + this.activatedRoute.snapshot.paramMap.get("disable-redirect") === null + ) { const successRoute = clientTypeToSuccessRouteRecord[this.clientType]; await this.router.navigate([successRoute]); } From 411d195386f8a12a9f20640e2287ea9baf3b5500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 8 Apr 2025 19:59:31 +0200 Subject: [PATCH 07/98] PM-19511: Add support for ExcludedCredentials (#14128) * works * Add mapping * remove the build script * cleanup --- .../desktop_native/macos_provider/src/registration.rs | 1 + apps/desktop/desktop_native/napi/index.d.ts | 1 + apps/desktop/desktop_native/napi/src/lib.rs | 1 + .../CredentialProviderViewController.swift | 11 ++++++++++- .../src/autofill/services/desktop-autofill.service.ts | 5 ++++- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/src/registration.rs b/apps/desktop/desktop_native/macos_provider/src/registration.rs index 9e697b75c16..c961566a86c 100644 --- a/apps/desktop/desktop_native/macos_provider/src/registration.rs +++ b/apps/desktop/desktop_native/macos_provider/src/registration.rs @@ -14,6 +14,7 @@ pub struct PasskeyRegistrationRequest { user_verification: UserVerification, supported_algorithms: Vec, window_xy: Position, + excluded_credentials: Vec>, } #[derive(uniffi::Record, Serialize, Deserialize)] diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 0d26be18bdb..e7740b67822 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -130,6 +130,7 @@ export declare namespace autofill { userVerification: UserVerification supportedAlgorithms: Array windowXy: Position + excludedCredentials: Array> } export interface PasskeyRegistrationResponse { rpId: string diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 28eaa61df17..a40a3b58f42 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -534,6 +534,7 @@ pub mod autofill { pub user_verification: UserVerification, pub supported_algorithms: Vec, pub window_xy: Position, + pub excluded_credentials: Vec>, } #[napi(object)] diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 0e6c0fc7023..5befed88563 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -299,6 +299,14 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } + // Convert excluded credentials to an array of credential IDs + var excludedCredentialIds: [Data] = [] + if #available(macOSApplicationExtension 15.0, *) { + if let excludedCreds = request.excludedCredentials { + excludedCredentialIds = excludedCreds.map { $0.credentialID } + } + } + let req = PasskeyRegistrationRequest( rpId: passkeyIdentity.relyingPartyIdentifier, userName: passkeyIdentity.userName, @@ -306,7 +314,8 @@ class CredentialProviderViewController: ASCredentialProviderViewController { clientDataHash: request.clientDataHash, userVerification: userVerification, supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) }, - windowXy: self.getWindowPosition() + windowXy: self.getWindowPosition(), + excludedCredentials: excludedCredentialIds ) logger.log("[autofill-extension] prepareInterface(passkey) calling preparePasskeyRegistration") diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index b72c6974c97..7fcc3425082 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -289,7 +289,10 @@ export class DesktopAutofillService implements OnDestroy { alg, type: "public-key", })), - excludeCredentialDescriptorList: [], + excludeCredentialDescriptorList: request.excludedCredentials.map((credentialId) => ({ + id: new Uint8Array(credentialId), + type: "public-key" as const, + })), requireResidentKey: true, requireUserVerification: request.userVerification === "required" || request.userVerification === "preferred", From f3726e5ac26b822afb54883c492aae0fda90be22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Wed, 9 Apr 2025 20:23:44 +0200 Subject: [PATCH 08/98] simplify updatedCipher (#14179) --- .../services/desktop-fido2-user-interface.service.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index a8103a99972..765ccabe0fd 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -95,8 +95,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private confirmCredentialSubject = new Subject(); - private createdCipher: Cipher = new Cipher(); - private updatedCipher: CipherView = new CipherView(); + private updatedCipher: CipherView; private rpId = new BehaviorSubject(""); private availableCipherIdsSubject = new BehaviorSubject([""]); @@ -238,14 +237,14 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi return { cipherId: this.updatedCipher.id, userVerified: userVerification }; } else { // Create the credential - await this.createCipher({ + const createdCipher = await this.createCipher({ credentialName, userName, rpId, userHandle, userVerification, }); - return { cipherId: this.createdCipher.id, userVerified: userVerification }; + return { cipherId: createdCipher.id, userVerified: userVerification }; } } finally { // Make sure to clean up so the app is never stuck in modal mode? @@ -306,7 +305,6 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi try { const createdCipher = await this.cipherService.createWithServer(encCipher); - this.createdCipher = createdCipher; return createdCipher; } catch { From b7c2419aede2447accdb0bf7f609b1c60942cdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Fri, 11 Apr 2025 09:10:41 +0200 Subject: [PATCH 09/98] Fix base64url decode on MacOS passkeys (#14227) * Add support for padding in base64url decode * whitespace * whitespace --- .../desktop_native/objc/src/native/utils.m | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/desktop/desktop_native/objc/src/native/utils.m b/apps/desktop/desktop_native/objc/src/native/utils.m index 040c723a8ac..7ae84696312 100644 --- a/apps/desktop/desktop_native/objc/src/native/utils.m +++ b/apps/desktop/desktop_native/objc/src/native/utils.m @@ -18,9 +18,25 @@ NSString *serializeJson(NSDictionary *dictionary, NSError *error) { } NSData *decodeBase64URL(NSString *base64URLString) { + if (base64URLString.length == 0) { + return nil; + } + + // Replace URL-safe characters with standard base64 characters NSString *base64String = [base64URLString stringByReplacingOccurrencesOfString:@"-" withString:@"+"]; base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"]; - + + // Add padding if needed + NSUInteger paddingLength = 4 - (base64String.length % 4); + if (paddingLength < 4) { + NSMutableString *paddedString = [NSMutableString stringWithString:base64String]; + for (NSUInteger i = 0; i < paddingLength; i++) { + [paddedString appendString:@"="]; + } + base64String = paddedString; + } + + // Decode the string NSData *nsdataFromBase64String = [[NSData alloc] initWithBase64EncodedString:base64String options:0]; From b62220ace1483c8833f55e403f056ac6a7a20dc7 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:41:30 +0200 Subject: [PATCH 10/98] Autofill/pm 17444 use reprompt (#14004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Passkey stuff Co-authored-by: Anders Åberg * Ugly hacks * Work On Modal State Management * Applying modalStyles * modal * Improved hide/show * fixed promise * File name * fix prettier * Protecting against null API's and undefined data * Only show fake popup to devs * cleanup mock code * rename minmimal-app to modal-app * Added comment * Added comment * removed old comment * Avoided changing minimum size * Add small comment * Rename component * adress feedback * Fixed uppercase file * Fixed build * Added codeowners * added void * commentary * feat: reset setting on app start * Moved reset to be in main / process launch * Add comment to create window * Added a little bit of styling * Use Messaging service to loadUrl * Enable passkeysautofill * Add logging * halfbaked * Integration working * And now it works without extra delay * Clean up * add note about messaging * lb * removed console.logs * Cleanup and adress review feedback * This hides the swift UI * add modal components * update modal with correct ciphers and functionality * add create screen * pick credential, draft * Remove logger * a whole lot of wiring * not working * Improved wiring * Cancel after 90s * Introduced observable * update cipher handling * update to use matchesUri * Launching bitwarden if its not running * Passing position from native to electron * Rename inModalMode to modalMode * remove tap * revert spaces * added back isDev * cleaned up a bit * Cleanup swift file * tweaked logging * clean up * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu * Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts Co-authored-by: Andreas Coroiu * Update apps/desktop/src/platform/services/desktop-settings.service.ts Co-authored-by: Andreas Coroiu * adress position feedback * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu * Removed extra logging * Adjusted error logging * Use .error to log errors * remove dead code * Update desktop-autofill.service.ts * use parseCredentialId instead of guidToRawFormat * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Andreas Coroiu * Change windowXy to a Record instead of [number,number] * Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts Co-authored-by: Andreas Coroiu * Remove unsued dep and comment * changed timeout to be spec recommended maxium, 10 minutes, for now. * Correctly assume UP * Removed extra cancelRequest in deinint * Add timeout and UV to confirmChoseCipher UV is performed by UI, not the service * Improved docs regarding undefined cipherId * cleanup: UP is no longer undefined * Run completeError if ipc messages conversion failed * don't throw, instead return undefined * Disabled passkey provider * Throw error if no activeUserId was found * removed comment * Fixed lint * removed unsued service * reset entitlement formatting * Update entitlements.mas.plist * Fix build issues * Fix import issues * Update route names to use `fido2` * Fix being unable to select a passkey * Fix linting issues * Added support for handling a locked vault * Followup to fix merge issues and other comments * Update `userHandle` value * Add error handling for missing session or other errors * Remove unused route * Fix linting issues * Simplify updateCredential method * Add master password reprompt on passkey create * Followup to remove comments and timeouts and handle errors * Address lint issue by using `takeUntilDestroyed` * Add MP prompt to cipher selection * Change how timeout is handled * Include `of` from rxjs * Hide blue header for passkey popouts (#14095) * Hide blue header for passkey popouts * Fix issue with test * Fix ngOnDestroy complaint * Import OnDestroy correctly * Only require master password if item requires it --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Anders Åberg Co-authored-by: Anders Åberg Co-authored-by: Colton Hurst Co-authored-by: Andreas Coroiu Co-authored-by: Evan Bassler Co-authored-by: Andreas Coroiu --- apps/desktop/src/app/app.component.ts | 3 +- .../desktop-fido2-user-interface.service.ts | 2 +- .../create/fido2-create.component.html | 4 +-- .../passkeys/create/fido2-create.component.ts | 17 +++++++++-- .../modal/passkeys/fido2-vault.component.html | 6 ++-- .../modal/passkeys/fido2-vault.component.ts | 28 +++++++++++++------ apps/desktop/src/scss/header.scss | 8 ++++++ libs/common/spec/fake-account-service.ts | 7 +++++ .../src/auth/abstractions/account.service.ts | 6 ++++ .../src/auth/services/account.service.spec.ts | 10 +++++++ .../src/auth/services/account.service.ts | 7 +++++ .../src/vault/abstractions/cipher.service.ts | 1 + .../src/vault/services/cipher.service.ts | 9 +++++- 13 files changed, 89 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 924bc2dd30f..25d5bcff7d0 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -86,7 +86,7 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours - +
@@ -112,6 +112,7 @@ export class AppComponent implements OnInit, OnDestroy { @ViewChild("loginApproval", { read: ViewContainerRef, static: true }) loginApprovalModalRef: ViewContainerRef; + showHeader$ = this.accountService.showHeader$; loading = false; private lastActivity: Date = null; diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 765ccabe0fd..35ac1fe9571 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -97,7 +97,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private updatedCipher: CipherView; - private rpId = new BehaviorSubject(""); + private rpId = new BehaviorSubject(null); private availableCipherIdsSubject = new BehaviorSubject([""]); /** * Observable that emits available cipher IDs once they're confirmed by the UI diff --git a/apps/desktop/src/modal/passkeys/create/fido2-create.component.html b/apps/desktop/src/modal/passkeys/create/fido2-create.component.html index e3423d6d7f8..8fefae29fd0 100644 --- a/apps/desktop/src/modal/passkeys/create/fido2-create.component.html +++ b/apps/desktop/src/modal/passkeys/create/fido2-create.component.html @@ -3,7 +3,7 @@ disableMargin class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300" > - +
@@ -16,7 +16,7 @@ type="button" bitIconButton="bwi-close" slot="end" - class="tw-mb-4 tw-mr-2" + class="passkey-header-close tw-mb-4 tw-mr-2" (click)="closeModal()" > Close diff --git a/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts b/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts index 776ceae9d85..48047f3a365 100644 --- a/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts +++ b/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, OnDestroy } from "@angular/core"; import { RouterModule, Router } from "@angular/router"; import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; @@ -22,6 +22,7 @@ import { SectionHeaderComponent, BitIconButtonComponent, } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; import { DesktopFido2UserInterfaceService, @@ -47,7 +48,7 @@ import { DesktopSettingsService } from "../../../platform/services/desktop-setti ], templateUrl: "fido2-create.component.html", }) -export class Fido2CreateComponent implements OnInit { +export class Fido2CreateComponent implements OnInit, OnDestroy { session?: DesktopFido2UserInterfaceSession = null; private ciphersSubject = new BehaviorSubject([]); ciphers$: Observable = this.ciphersSubject.asObservable(); @@ -61,10 +62,12 @@ export class Fido2CreateComponent implements OnInit { private readonly dialogService: DialogService, private readonly domainSettingsService: DomainSettingsService, private readonly logService: LogService, + private readonly passwordRepromptService: PasswordRepromptService, private readonly router: Router, ) {} async ngOnInit() { + await this.accountService.setShowHeader(false); this.session = this.fido2UserInterfaceService.getCurrentSession(); const rpid = await this.session.getRpId(); const equivalentDomains = await firstValueFrom( @@ -92,8 +95,16 @@ export class Fido2CreateComponent implements OnInit { .catch((error) => this.logService.error(error)); } + async ngOnDestroy() { + await this.accountService.setShowHeader(true); + } + async addPasskeyToCipher(cipher: CipherView) { - this.session.notifyConfirmCreateCredential(true, cipher); + const userVerified = cipher.reprompt + ? await this.passwordRepromptService.showPasswordPrompt() + : true; + + this.session.notifyConfirmCreateCredential(userVerified, cipher); } async confirmPasskey() { diff --git a/apps/desktop/src/modal/passkeys/fido2-vault.component.html b/apps/desktop/src/modal/passkeys/fido2-vault.component.html index edc241c4d21..5191dcb1b6e 100644 --- a/apps/desktop/src/modal/passkeys/fido2-vault.component.html +++ b/apps/desktop/src/modal/passkeys/fido2-vault.component.html @@ -3,7 +3,7 @@ disableMargin class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300" > - +
@@ -13,7 +13,7 @@ type="button" bitIconButton="bwi-close" slot="end" - class="tw-mb-4 tw-mr-2" + class="passkey-header-close tw-mb-4 tw-mr-2" (click)="closeModal()" > Close @@ -23,7 +23,7 @@ - + + + +
+ +
+ +
+ {{ "passkeyAlreadyExists" | i18n }} + {{ "applicationDoesNotSupportDuplicates" | i18n }} +
+ +
+
+
+
diff --git a/apps/desktop/src/modal/passkeys/fido2-excluded-ciphers.component.ts b/apps/desktop/src/modal/passkeys/fido2-excluded-ciphers.component.ts new file mode 100644 index 00000000000..1872ff16b3c --- /dev/null +++ b/apps/desktop/src/modal/passkeys/fido2-excluded-ciphers.component.ts @@ -0,0 +1,73 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { RouterModule, Router } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield } from "@bitwarden/auth/angular"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { + BadgeModule, + ButtonModule, + DialogModule, + IconModule, + ItemModule, + SectionComponent, + TableModule, + SectionHeaderComponent, + BitIconButtonComponent, +} from "@bitwarden/components"; + +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../autofill/services/desktop-fido2-user-interface.service"; +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; + +import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; + +@Component({ + standalone: true, + imports: [ + CommonModule, + RouterModule, + SectionHeaderComponent, + BitIconButtonComponent, + TableModule, + JslibModule, + IconModule, + ButtonModule, + DialogModule, + SectionComponent, + ItemModule, + BadgeModule, + ], + templateUrl: "fido2-excluded-ciphers.component.html", +}) +export class Fido2ExcludedCiphersComponent implements OnInit, OnDestroy { + session?: DesktopFido2UserInterfaceSession = null; + readonly Icons = { BitwardenShield }; + protected fido2PasskeyExistsIcon = Fido2PasskeyExistsIcon; + + constructor( + private readonly desktopSettingsService: DesktopSettingsService, + private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, + private readonly accountService: AccountService, + private readonly router: Router, + ) {} + + async ngOnInit() { + await this.accountService.setShowHeader(false); + this.session = this.fido2UserInterfaceService.getCurrentSession(); + } + + async ngOnDestroy() { + await this.accountService.setShowHeader(true); + } + + async closeModal() { + await this.router.navigate(["/"]); + await this.desktopSettingsService.setModalMode(false); + this.session.notifyConfirmCreateCredential(false); + this.session.confirmChosenCipher(null); + } +} diff --git a/apps/desktop/src/modal/passkeys/fido2-passkey-exists-icon.ts b/apps/desktop/src/modal/passkeys/fido2-passkey-exists-icon.ts new file mode 100644 index 00000000000..5a179f595fd --- /dev/null +++ b/apps/desktop/src/modal/passkeys/fido2-passkey-exists-icon.ts @@ -0,0 +1,16 @@ +import { svgIcon } from "@bitwarden/components"; + +export const Fido2PasskeyExistsIcon = svgIcon` + + + + + + + + + + + + +`; diff --git a/apps/desktop/src/platform/models/domain/window-state.ts b/apps/desktop/src/platform/models/domain/window-state.ts index 0efc9a1efab..ab52531bb5d 100644 --- a/apps/desktop/src/platform/models/domain/window-state.ts +++ b/apps/desktop/src/platform/models/domain/window-state.ts @@ -14,5 +14,6 @@ export class WindowState { export class ModalModeState { isModalModeActive: boolean; + showTrafficButtons?: boolean; modalPosition?: { x: number; y: number }; // Modal position is often passed from the native UI } diff --git a/apps/desktop/src/platform/popup-modal-styles.ts b/apps/desktop/src/platform/popup-modal-styles.ts index 05a8ec1025f..1ef4b901c76 100644 --- a/apps/desktop/src/platform/popup-modal-styles.ts +++ b/apps/desktop/src/platform/popup-modal-styles.ts @@ -8,10 +8,14 @@ const popupHeight = 600; type Position = { x: number; y: number }; -export function applyPopupModalStyles(window: BrowserWindow, position?: Position) { +export function applyPopupModalStyles( + window: BrowserWindow, + showTrafficButtons: boolean = true, + position?: Position, +) { window.unmaximize(); window.setSize(popupWidth, popupHeight); - window.setWindowButtonVisibility?.(false); + window.setWindowButtonVisibility?.(showTrafficButtons); window.setMenuBarVisibility?.(false); window.setResizable(false); window.setAlwaysOnTop(true); diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index f5789d6f40c..ba59f7d4623 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -306,9 +306,14 @@ 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 setModalMode(value: boolean, modalPosition?: { x: number; y: number }) { + async setModalMode( + value: boolean, + showTrafficButtons?: boolean, + modalPosition?: { x: number; y: number }, + ) { await this.modalModeState.update(() => ({ isModalModeActive: value, + showTrafficButtons, modalPosition, })); } diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index e9e68ca92c3..3e63ec2d3df 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -136,7 +136,7 @@ export interface Fido2AuthenticatorGetAssertionParams { rpId: string; /** The hash of the serialized client data, provided by the client. */ hash: BufferSource; - allowCredentialDescriptorList: PublicKeyCredentialDescriptor[]; + allowCredentialDescriptorList?: PublicKeyCredentialDescriptor[]; /** The effective user verification requirement for assertion, a Boolean value provided by the client. */ requireUserVerification: boolean; /** The constant Boolean value true. It is included here as a pseudo-parameter to simplify applying this abstract authenticator model to implementations that may wish to make a test of user presence optional although WebAuthn does not. */ diff --git a/libs/common/src/platform/services/fido2/fido2-utils.ts b/libs/common/src/platform/services/fido2/fido2-utils.ts index b9f3c8f8c48..59ce7dd723a 100644 --- a/libs/common/src/platform/services/fido2/fido2-utils.ts +++ b/libs/common/src/platform/services/fido2/fido2-utils.ts @@ -1,3 +1,5 @@ +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore export class Fido2Utils { @@ -72,4 +74,16 @@ export class Fido2Utils { return output; } + + /** + * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle + * @param userHandle + */ + static cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { + return true; + } + + return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); + } } From 40c77b6f0591d95c89418ea468a698b34913bfed Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Thu, 15 May 2025 15:33:20 +0200 Subject: [PATCH 24/98] Move modal files to `autofill` and rename dir to `credentials` (#14757) * Show existing login items in the UI * Filter available cipher results (#14399) * Filter available cipher results * Fix linting issues * Update logic for eligible ciphers * Remove unused method to check matching username * PM-20608 update styling for excludedCredentials (#14444) * PM-20608 update styling for excludedCredentials * Have flow correctly move to creation for excluded cipher * Remove duplicate confirmNeCredential call * Revert fido2-authenticator changes and move the excluded check * Create a separate component for excluded cipher view * Display traffic light MacOS buttons when the vault is locked (#14673) * Remove unneccessary filter for excludedCiphers * Remove dead code from the excluded ciphers work * Remove excludedCipher checks from fido2 create and vault * Move modal files to `autofill` and rename dir to `credentials` * Update merge issues --- apps/desktop/src/app/app-routing.module.ts | 6 +++--- .../modal/credentials}/fido2-create.component.html | 0 .../modal/credentials}/fido2-create.component.ts | 4 ++-- .../credentials}/fido2-excluded-ciphers.component.html | 0 .../credentials}/fido2-excluded-ciphers.component.ts | 4 ++-- .../modal/credentials}/fido2-passkey-exists-icon.ts | 0 .../modal/credentials}/fido2-vault.component.html | 0 .../modal/credentials}/fido2-vault.component.ts | 9 ++++++--- 8 files changed, 13 insertions(+), 10 deletions(-) rename apps/desktop/src/{modal/passkeys/create => autofill/modal/credentials}/fido2-create.component.html (100%) rename apps/desktop/src/{modal/passkeys/create => autofill/modal/credentials}/fido2-create.component.ts (98%) rename apps/desktop/src/{modal/passkeys => autofill/modal/credentials}/fido2-excluded-ciphers.component.html (100%) rename apps/desktop/src/{modal/passkeys => autofill/modal/credentials}/fido2-excluded-ciphers.component.ts (92%) rename apps/desktop/src/{modal/passkeys => autofill/modal/credentials}/fido2-passkey-exists-icon.ts (100%) rename apps/desktop/src/{modal/passkeys => autofill/modal/credentials}/fido2-vault.component.html (100%) rename apps/desktop/src/{modal/passkeys => autofill/modal/credentials}/fido2-vault.component.ts (92%) diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index ca4b391579e..ac8b0661506 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -54,10 +54,10 @@ import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.compo import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; +import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create.component"; +import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component"; +import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; -import { Fido2CreateComponent } from "../modal/passkeys/create/fido2-create.component"; -import { Fido2ExcludedCiphersComponent } from "../modal/passkeys/fido2-excluded-ciphers.component"; -import { Fido2VaultComponent } from "../modal/passkeys/fido2-vault.component"; import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; diff --git a/apps/desktop/src/modal/passkeys/create/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html similarity index 100% rename from apps/desktop/src/modal/passkeys/create/fido2-create.component.html rename to apps/desktop/src/autofill/modal/credentials/fido2-create.component.html diff --git a/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts similarity index 98% rename from apps/desktop/src/modal/passkeys/create/fido2-create.component.ts rename to apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index 4548c2da119..db5b49afb62 100644 --- a/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -26,11 +26,11 @@ import { import { PasswordRepromptService } from "@bitwarden/vault"; import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service"; +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; import { DesktopFido2UserInterfaceService, DesktopFido2UserInterfaceSession, -} from "../../../autofill/services/desktop-fido2-user-interface.service"; -import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +} from "../../services/desktop-fido2-user-interface.service"; @Component({ standalone: true, diff --git a/apps/desktop/src/modal/passkeys/fido2-excluded-ciphers.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html similarity index 100% rename from apps/desktop/src/modal/passkeys/fido2-excluded-ciphers.component.html rename to apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html diff --git a/apps/desktop/src/modal/passkeys/fido2-excluded-ciphers.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts similarity index 92% rename from apps/desktop/src/modal/passkeys/fido2-excluded-ciphers.component.ts rename to apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts index 1872ff16b3c..de6372e0457 100644 --- a/apps/desktop/src/modal/passkeys/fido2-excluded-ciphers.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts @@ -17,11 +17,11 @@ import { BitIconButtonComponent, } from "@bitwarden/components"; +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; import { DesktopFido2UserInterfaceService, DesktopFido2UserInterfaceSession, -} from "../../autofill/services/desktop-fido2-user-interface.service"; -import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +} from "../../services/desktop-fido2-user-interface.service"; import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; diff --git a/apps/desktop/src/modal/passkeys/fido2-passkey-exists-icon.ts b/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts similarity index 100% rename from apps/desktop/src/modal/passkeys/fido2-passkey-exists-icon.ts rename to apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts diff --git a/apps/desktop/src/modal/passkeys/fido2-vault.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html similarity index 100% rename from apps/desktop/src/modal/passkeys/fido2-vault.component.html rename to apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html diff --git a/apps/desktop/src/modal/passkeys/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts similarity index 92% rename from apps/desktop/src/modal/passkeys/fido2-vault.component.ts rename to apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index 423d646992a..9a06e040889 100644 --- a/apps/desktop/src/modal/passkeys/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -23,11 +23,11 @@ import { } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; import { DesktopFido2UserInterfaceService, DesktopFido2UserInterfaceSession, -} from "../../autofill/services/desktop-fido2-user-interface.service"; -import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +} from "../../services/desktop-fido2-user-interface.service"; @Component({ standalone: true, @@ -53,6 +53,7 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { private ciphersSubject = new BehaviorSubject([]); ciphers$: Observable = this.ciphersSubject.asObservable(); private cipherIdsSubject = new BehaviorSubject([]); + protected containsExcludedCiphers: boolean = false; cipherIds$: Observable; readonly Icons = { BitwardenShield }; @@ -91,7 +92,9 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { } async chooseCipher(cipher: CipherView) { - if ( + if (this.containsExcludedCiphers) { + this.session?.confirmChosenCipher(cipher.id, false); + } else if ( cipher.reprompt !== CipherRepromptType.None && !(await this.passwordRepromptService.showPasswordPrompt()) ) { From ec6c85d6ad3e7508fcd90d58538a4d117ffb2d8a Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Mon, 19 May 2025 15:35:43 +0200 Subject: [PATCH 25/98] Add tests for `cipherHasNoOtherPasskeys` (#14829) --- .../services/fido2/fido2-utils.spec.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/libs/common/src/platform/services/fido2/fido2-utils.spec.ts b/libs/common/src/platform/services/fido2/fido2-utils.spec.ts index 9bb4ed0a4c5..6b34f772798 100644 --- a/libs/common/src/platform/services/fido2/fido2-utils.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-utils.spec.ts @@ -1,3 +1,9 @@ +import { mock } from "jest-mock-extended"; + +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; + import { Fido2Utils } from "./fido2-utils"; describe("Fido2 Utils", () => { @@ -67,4 +73,62 @@ describe("Fido2 Utils", () => { expect(expectedArray).toBeNull(); }); }); + + describe("cipherHasNoOtherPasskeys(...)", () => { + const emptyPasskeyCipher = mock({ + id: "id-5", + localData: { lastUsedDate: 222 }, + name: "name-5", + type: CipherType.Login, + login: { + username: "username-5", + password: "password", + uri: "https://example.com", + fido2Credentials: [], + }, + }); + + const passkeyCipher = mock({ + id: "id-5", + localData: { lastUsedDate: 222 }, + name: "name-5", + type: CipherType.Login, + login: { + username: "username-5", + password: "password", + uri: "https://example.com", + fido2Credentials: [ + mock({ + credentialId: "credential-id", + rpName: "credential-name", + userHandle: "user-handle-1", + userName: "credential-username", + rpId: "jest-testing-website.com", + }), + mock({ + credentialId: "credential-id", + rpName: "credential-name", + userHandle: "user-handle-2", + userName: "credential-username", + rpId: "jest-testing-website.com", + }), + ], + }, + }); + + it("should return true when there is no userHandle", () => { + const userHandle = "user-handle-1"; + expect(Fido2Utils.cipherHasNoOtherPasskeys(emptyPasskeyCipher, userHandle)).toBeTruthy(); + }); + + it("should return true when userHandle matches", () => { + const userHandle = "user-handle-1"; + expect(Fido2Utils.cipherHasNoOtherPasskeys(passkeyCipher, userHandle)).toBeTruthy(); + }); + + it("should return false when userHandle doesn't match", () => { + const userHandle = "testing"; + expect(Fido2Utils.cipherHasNoOtherPasskeys(passkeyCipher, userHandle)).toBeFalsy(); + }); + }); }); From 1b12836784cb7338075f769c3d661cf5835dbaed Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Tue, 27 May 2025 16:21:57 +0200 Subject: [PATCH 26/98] Adjust spacing to place new login button below other items (#14877) * Adjust spacing to place new login button below other items * Add correct design when no credentials available (#14879) --- .../credentials/fido2-create.component.html | 57 ++++++++++++------- .../credentials/fido2-create.component.ts | 3 + apps/desktop/src/locales/en/messages.json | 6 ++ 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index c52c5db16e9..7451a7c686e 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -1,4 +1,4 @@ -
+
- - - - {{ c.subTitle }} - Save - - - - - - - +
+
+ + + + {{ c.subTitle }} + Save + + + + + +
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index db5b49afb62..bfdffb3005a 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -32,6 +32,8 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; +import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; + @Component({ standalone: true, imports: [ @@ -55,6 +57,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { private ciphersSubject = new BehaviorSubject([]); ciphers$: Observable = this.ciphersSubject.asObservable(); readonly Icons = { BitwardenShield }; + protected fido2PasskeyExistsIcon = Fido2PasskeyExistsIcon; constructor( private readonly desktopSettingsService: DesktopSettingsService, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index f31763429a9..c8f9b9e4088 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3704,6 +3704,12 @@ "saveNewPasskey": { "message": "Save as new login" }, + "savePasskeyNewLogin": { + "message": "Save passkey as new login" + }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "overwritePasskey": { "message": "Overwrite passkey?" }, From 10ae123cb2a56a918b9b444e544fc68e4a8ce413 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Wed, 28 May 2025 15:04:42 +0200 Subject: [PATCH 27/98] Autofill/pm 21903 use translations everywhere for passkeys (#14908) * Adjust spacing to place new login button below other items * Add correct design when no credentials available * Add correct design when no credentials available (#14879) * Remove hardcoded strings and use translations in passkey flow * Remove duplicate `select` translation --- .../autofill/modal/credentials/fido2-create.component.html | 4 ++-- .../modal/credentials/fido2-excluded-ciphers.component.html | 6 ++++-- .../autofill/modal/credentials/fido2-vault.component.html | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index 7451a7c686e..46d2a6da731 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -19,7 +19,7 @@ class="passkey-header-close tw-mb-4 tw-mr-2" (click)="closeModal()" > - Close + {{ "close" | i18n }}
@@ -44,7 +44,7 @@ {{ c.name }} {{ c.subTitle }} - Save + {{ "save" | i18n }} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html index 3a2e9f9bc9b..94bfd7d06d2 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html @@ -19,7 +19,7 @@ class="passkey-header-close tw-mb-4 tw-mr-2" (click)="closeModal()" > - Close + {{ "close" | i18n }}
@@ -34,7 +34,9 @@ {{ "passkeyAlreadyExists" | i18n }} {{ "applicationDoesNotSupportDuplicates" | i18n }}
- +
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html index ac1d01255b4..473b457c13f 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html @@ -16,7 +16,7 @@ class="passkey-header-close tw-mb-4 tw-mr-2" (click)="closeModal()" > - Close + {{ "close" | i18n }} @@ -29,7 +29,7 @@ {{ c.name }} {{ c.subTitle }} - Select + {{ "select" | i18n }} From 0376ead84ebfd5aaf65896da8b41b8f6813e53a6 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:31:34 +0200 Subject: [PATCH 28/98] Autofill/pm 21864 center unlock vault modal (#14867) * Center the Locked Vault modal when using passkeys * Revert swift changes and handle offscreen modals * Remove comments --- .../CredentialProviderViewController.swift | 5 ++-- .../desktop-fido2-user-interface.service.ts | 24 ++++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 5befed88563..4288ca8f3fe 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -138,14 +138,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController { private func getWindowPosition() -> Position { let frame = self.view.window?.frame ?? .zero - let screenHeight = NSScreen.main?.frame.height ?? 0 - + let screenHeight = NSScreen.main?.frame.height ?? 0 + // 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))) - return Position(x: centerX, y:centerY) } diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 6bce6732c09..1b69f6fb57a 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -265,6 +265,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi ): Promise { // Load the UI: await this.desktopSettingsService.setModalMode(true, showTrafficButtons, position); + await this.centerOffscreenPopup(); await this.router.navigate([ route, { @@ -341,7 +342,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi const status = await firstValueFrom(this.authService.activeAccountStatus$); if (status !== AuthenticationStatus.Unlocked) { - await this.showUi("/lock", this.windowObject.windowXy, true, true); + await this.showUi("/lock", undefined, true, true); let status2: AuthenticationStatus; try { @@ -370,4 +371,25 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi async close() { this.logService.warning("close"); } + + private async centerOffscreenPopup() { + if (!this.windowObject.windowXy) { + return; + } + + const popupWidth = 600; + const popupHeight = 600; + + const window = await firstValueFrom(this.desktopSettingsService.window$); + const { width, height } = window.displayBounds; + const { x, y } = this.windowObject.windowXy; + + if (x < popupWidth || x > width - popupWidth || y < popupHeight || y > height - popupHeight) { + const popupHeightOffset = 300; + const { width, height } = window.displayBounds; + const centeredX = width / 2; + const centeredY = (height - popupHeightOffset) / 2; + this.windowObject.windowXy = { x: centeredX, y: centeredY }; + } + } } From 2b1f6e473ba1cac22324edfbbc2f9b2f7e208d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 3 Jun 2025 16:40:26 +0200 Subject: [PATCH 29/98] Add rustup for cicd to work (#15055) --- apps/desktop/desktop_native/macos_provider/build.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/desktop/desktop_native/macos_provider/build.sh b/apps/desktop/desktop_native/macos_provider/build.sh index 21e2e045af4..2f7a2d03541 100755 --- a/apps/desktop/desktop_native/macos_provider/build.sh +++ b/apps/desktop/desktop_native/macos_provider/build.sh @@ -8,6 +8,9 @@ rm -r tmp mkdir -p ./tmp/target/universal-darwin/release/ +rustup target add aarch64-apple-darwin +rustup target add x86_64-apple-darwin + cargo build --package macos_provider --target aarch64-apple-darwin --release cargo build --package macos_provider --target x86_64-apple-darwin --release From 0d7154e69c2ddf6fa7095645260ce4a7d298ce70 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:03:59 +0200 Subject: [PATCH 30/98] Hide credentials that are in the bin (#15034) --- .../src/autofill/modal/credentials/fido2-create.component.ts | 3 ++- .../src/autofill/modal/credentials/fido2-vault.component.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index bfdffb3005a..a6d13d446e9 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -94,7 +94,8 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { return ( cipher.login.matchesUri(rpid, equivalentDomains) && - Fido2Utils.cipherHasNoOtherPasskeys(cipher, userHandle) + Fido2Utils.cipherHasNoOtherPasskeys(cipher, userHandle) && + !cipher.deletedDate ); }); this.ciphersSubject.next(relevantCiphers); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index 9a06e040889..f301946721b 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -80,7 +80,7 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { this.cipherService .getAllDecryptedForIds(activeUserId, cipherIds || []) .then((ciphers) => { - this.ciphersSubject.next(ciphers); + this.ciphersSubject.next(ciphers.filter((cipher) => !cipher.deletedDate)); }) .catch((error) => this.logService.error(error)); }); From c873f5a6e0a829fde62d635060269b401c5c1838 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:05:34 +0200 Subject: [PATCH 31/98] Add tests for passkey components (#15185) * Add tests for passkey components * Reuse cipher in chooseCipher tests and simplify mock creation --- .../fido2-create.component.spec.ts | 241 ++++++++++++++++++ .../fido2-excluded-ciphers.component.spec.ts | 87 +++++++ .../credentials/fido2-vault.component.spec.ts | 201 +++++++++++++++ .../credentials/fido2-vault.component.ts | 5 +- 4 files changed, 530 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts create mode 100644 apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts create mode 100644 apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts new file mode 100644 index 00000000000..fc619ccad14 --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts @@ -0,0 +1,241 @@ +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service"; +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +import { Fido2CreateComponent } from "./fido2-create.component"; + +describe("Fido2CreateComponent", () => { + let component: Fido2CreateComponent; + let mockDesktopSettingsService: MockProxy; + let mockFido2UserInterfaceService: MockProxy; + let mockAccountService: MockProxy; + let mockCipherService: MockProxy; + let mockDesktopAutofillService: MockProxy; + let mockDialogService: MockProxy; + let mockDomainSettingsService: MockProxy; + let mockLogService: MockProxy; + let mockPasswordRepromptService: MockProxy; + let mockRouter: MockProxy; + let mockSession: MockProxy; + let mockI18nService: MockProxy; + + const activeAccountSubject = new BehaviorSubject({ + id: "test-user-id" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + + beforeEach(async () => { + mockDesktopSettingsService = mock(); + mockFido2UserInterfaceService = mock(); + mockAccountService = mock(); + mockCipherService = mock(); + mockDesktopAutofillService = mock(); + mockDialogService = mock(); + mockDomainSettingsService = mock(); + mockLogService = mock(); + mockPasswordRepromptService = mock(); + mockRouter = mock(); + mockSession = mock(); + mockI18nService = mock(); + + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession); + mockAccountService.activeAccount$ = activeAccountSubject; + + await TestBed.configureTestingModule({ + providers: [ + Fido2CreateComponent, + { provide: DesktopSettingsService, useValue: mockDesktopSettingsService }, + { provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: CipherService, useValue: mockCipherService }, + { provide: DesktopAutofillService, useValue: mockDesktopAutofillService }, + { provide: DialogService, useValue: mockDialogService }, + { provide: DomainSettingsService, useValue: mockDomainSettingsService }, + { provide: LogService, useValue: mockLogService }, + { provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, + { provide: Router, useValue: mockRouter }, + { provide: I18nService, useValue: mockI18nService }, + ], + }).compileComponents(); + + component = TestBed.inject(Fido2CreateComponent); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function createMockCiphers(): CipherView[] { + const cipher1 = new CipherView(); + cipher1.id = "cipher-1"; + cipher1.name = "Test Cipher 1"; + cipher1.type = CipherType.Login; + cipher1.login = { + username: "test1@example.com", + uris: [{ uri: "https://example.com", match: null }], + matchesUri: jest.fn().mockReturnValue(true), + get hasFido2Credentials() { + return false; + }, + } as any; + cipher1.reprompt = CipherRepromptType.None; + cipher1.deletedDate = null; + + return [cipher1]; + } + + describe("ngOnInit", () => { + beforeEach(() => { + mockSession.getRpId.mockResolvedValue("example.com"); + Object.defineProperty(mockDesktopAutofillService, "lastRegistrationRequest", { + get: jest.fn().mockReturnValue({ + userHandle: new Uint8Array([1, 2, 3]), + }), + configurable: true, + }); + mockDomainSettingsService.getUrlEquivalentDomains.mockReturnValue(of(new Set())); + }); + + it("should initialize session and set show header to false", async () => { + const mockCiphers = createMockCiphers(); + mockCipherService.getAllDecrypted.mockResolvedValue(mockCiphers); + + await component.ngOnInit(); + + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(false); + expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled(); + expect(component.session).toBe(mockSession); + }); + + it("should throw error when no active session found", async () => { + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(null); + + await expect(component.ngOnInit()).rejects.toThrow( + "Cannot read properties of null (reading 'getRpId')", + ); + }); + }); + + describe("ngOnDestroy", () => { + it("should restore header visibility", async () => { + await component.ngOnDestroy(); + + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(true); + }); + }); + + describe("addPasskeyToCipher", () => { + beforeEach(() => { + component.session = mockSession; + }); + + it("should add passkey to cipher", async () => { + const cipher = createMockCiphers()[0]; + + await component.addPasskeyToCipher(cipher); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true, cipher); + }); + + it("should not add passkey when password reprompt is cancelled", async () => { + const cipher = createMockCiphers()[0]; + cipher.reprompt = CipherRepromptType.Password; + mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false); + + await component.addPasskeyToCipher(cipher); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false, cipher); + }); + + it("should call openSimpleDialog when cipher already has a fido2 credential", async () => { + const cipher = createMockCiphers()[0]; + Object.defineProperty(cipher.login, "hasFido2Credentials", { + get: jest.fn().mockReturnValue(true), + }); + mockDialogService.openSimpleDialog.mockResolvedValue(true); + + await component.addPasskeyToCipher(cipher); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "overwritePasskey" }, + content: { key: "alreadyContainsPasskey" }, + type: "warning", + }); + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true, cipher); + }); + + it("should not add passkey when user cancels overwrite dialog", async () => { + const cipher = createMockCiphers()[0]; + Object.defineProperty(cipher.login, "hasFido2Credentials", { + get: jest.fn().mockReturnValue(true), + }); + mockDialogService.openSimpleDialog.mockResolvedValue(false); + + await component.addPasskeyToCipher(cipher); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false, cipher); + }); + }); + + describe("confirmPasskey", () => { + beforeEach(() => { + component.session = mockSession; + }); + + it("should confirm passkey creation successfully", async () => { + await component.confirmPasskey(); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + }); + + it("should call openSimpleDialog when session is null", async () => { + component.session = null; + + await component.confirmPasskey(); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "unexpectedErrorShort" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeBitwarden" }, + cancelButtonText: null, + }); + }); + }); + + describe("closeModal", () => { + it("should close modal and notify session", async () => { + component.session = mockSession; + + await component.closeModal(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null); + }); + }); +}); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts new file mode 100644 index 00000000000..22dfa0c07d7 --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts @@ -0,0 +1,87 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +import { Fido2ExcludedCiphersComponent } from "./fido2-excluded-ciphers.component"; + +describe("Fido2ExcludedCiphersComponent", () => { + let component: Fido2ExcludedCiphersComponent; + let fixture: ComponentFixture; + let mockDesktopSettingsService: MockProxy; + let mockFido2UserInterfaceService: MockProxy; + let mockAccountService: MockProxy; + let mockRouter: MockProxy; + let mockSession: MockProxy; + let mockI18nService: MockProxy; + + beforeEach(async () => { + mockDesktopSettingsService = mock(); + mockFido2UserInterfaceService = mock(); + mockAccountService = mock(); + mockRouter = mock(); + mockSession = mock(); + mockI18nService = mock(); + + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession); + + await TestBed.configureTestingModule({ + imports: [Fido2ExcludedCiphersComponent], + providers: [ + { provide: DesktopSettingsService, useValue: mockDesktopSettingsService }, + { provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: Router, useValue: mockRouter }, + { provide: I18nService, useValue: mockI18nService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(Fido2ExcludedCiphersComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("ngOnInit", () => { + it("should initialize session and set show header to false", async () => { + await component.ngOnInit(); + + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(false); + expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled(); + expect(component.session).toBe(mockSession); + }); + }); + + describe("ngOnDestroy", () => { + it("should restore header visibility", async () => { + await component.ngOnDestroy(); + + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(true); + }); + }); + + describe("closeModal", () => { + it("should close modal and notify session when session exists", async () => { + component.session = mockSession; + + await component.closeModal(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null); + }); + }); +}); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts new file mode 100644 index 00000000000..42dce17f3f3 --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts @@ -0,0 +1,201 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +import { Fido2VaultComponent } from "./fido2-vault.component"; + +describe("Fido2VaultComponent", () => { + let component: Fido2VaultComponent; + let fixture: ComponentFixture; + let mockDesktopSettingsService: MockProxy; + let mockFido2UserInterfaceService: MockProxy; + let mockCipherService: MockProxy; + let mockAccountService: MockProxy; + let mockLogService: MockProxy; + let mockPasswordRepromptService: MockProxy; + let mockRouter: MockProxy; + let mockSession: MockProxy; + let mockI18nService: MockProxy; + + const mockActiveAccount = { id: "test-user-id", email: "test@example.com" }; + const mockCipherIds = ["cipher-1", "cipher-2", "cipher-3"]; + + beforeEach(async () => { + mockDesktopSettingsService = mock(); + mockFido2UserInterfaceService = mock(); + mockCipherService = mock(); + mockAccountService = mock(); + mockLogService = mock(); + mockPasswordRepromptService = mock(); + mockRouter = mock(); + mockSession = mock(); + mockI18nService = mock(); + + mockAccountService.activeAccount$ = of(mockActiveAccount as Account); + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession); + mockSession.availableCipherIds$ = of(mockCipherIds); + mockCipherService.getAllDecryptedForIds.mockResolvedValue([]); + + await TestBed.configureTestingModule({ + imports: [Fido2VaultComponent], + providers: [ + { provide: DesktopSettingsService, useValue: mockDesktopSettingsService }, + { provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService }, + { provide: CipherService, useValue: mockCipherService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: LogService, useValue: mockLogService }, + { provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, + { provide: Router, useValue: mockRouter }, + { provide: I18nService, useValue: mockI18nService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(Fido2VaultComponent); + component = fixture.componentInstance; + }); + + const mockCiphers: any[] = [ + { + id: "cipher-1", + name: "Test Cipher 1", + type: CipherType.Login, + login: { + username: "test1@example.com", + }, + reprompt: CipherRepromptType.None, + deletedDate: null, + }, + { + id: "cipher-2", + name: "Test Cipher 2", + type: CipherType.Login, + login: { + username: "test2@example.com", + }, + reprompt: CipherRepromptType.None, + deletedDate: null, + }, + { + id: "cipher-3", + name: "Test Cipher 3", + type: CipherType.Login, + login: { + username: "test3@example.com", + }, + reprompt: CipherRepromptType.Password, + deletedDate: null, + }, + ]; + + describe("ngOnInit", () => { + it("should initialize session and load ciphers successfully", async () => { + mockCipherService.getAllDecryptedForIds.mockResolvedValue(mockCiphers); + + await component.ngOnInit(); + + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(false); + expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled(); + expect(component.session).toBe(mockSession); + expect(component.cipherIds$).toBe(mockSession.availableCipherIds$); + }); + + it("should handle error when no active session found", async () => { + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(null); + + await expect(component.ngOnInit()).rejects.toThrow(); + }); + + it("should filter out deleted ciphers", async () => { + mockCiphers[1].deletedDate = new Date(); + mockCipherService.getAllDecryptedForIds.mockResolvedValue(mockCiphers); + + await component.ngOnInit(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + let ciphersResult: CipherView[] = []; + component.ciphers$.subscribe((ciphers) => { + ciphersResult = ciphers; + }); + + expect(ciphersResult).toHaveLength(2); + expect(ciphersResult.every((cipher) => !cipher.deletedDate)).toBe(true); + }); + }); + + describe("ngOnDestroy", () => { + it("should restore header visibility and clean up", async () => { + await component.ngOnInit(); + await component.ngOnDestroy(); + + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(true); + }); + }); + + describe("chooseCipher", () => { + const cipher = mockCiphers[0]; + + beforeEach(() => { + component.session = mockSession; + }); + + it("should choose cipher when access is validated", async () => { + cipher.reprompt = CipherRepromptType.None; + + await component.chooseCipher(cipher); + + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, true); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + }); + + it("should prompt for password when cipher requires reprompt", async () => { + cipher.reprompt = CipherRepromptType.Password; + mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(true); + + await component.chooseCipher(cipher); + + expect(mockPasswordRepromptService.showPasswordPrompt).toHaveBeenCalled(); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, true); + }); + + it("should not choose cipher when password reprompt is cancelled", async () => { + cipher.reprompt = CipherRepromptType.Password; + mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false); + + await component.chooseCipher(cipher); + + expect(mockPasswordRepromptService.showPasswordPrompt).toHaveBeenCalled(); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, false); + }); + }); + + describe("closeModal", () => { + it("should close modal and notify session", async () => { + component.session = mockSession; + + await component.closeModal(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null); + }); + }); +}); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index f301946721b..6c627185f50 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -53,7 +53,6 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { private ciphersSubject = new BehaviorSubject([]); ciphers$: Observable = this.ciphersSubject.asObservable(); private cipherIdsSubject = new BehaviorSubject([]); - protected containsExcludedCiphers: boolean = false; cipherIds$: Observable; readonly Icons = { BitwardenShield }; @@ -92,9 +91,7 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { } async chooseCipher(cipher: CipherView) { - if (this.containsExcludedCiphers) { - this.session?.confirmChosenCipher(cipher.id, false); - } else if ( + if ( cipher.reprompt !== CipherRepromptType.None && !(await this.passwordRepromptService.showPasswordPrompt()) ) { From 7d72b98863704eae20c733064583f6a9b04a8bed Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:44:38 +0200 Subject: [PATCH 32/98] Autofill/pm 22821 center vault modal (#15243) * Center the vault modal for passkeys * Add comments and fix electron-builder.json * Set values to Int32 in the ternaries --- .../CredentialProviderViewController.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 4288ca8f3fe..d2cb14ea91e 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -139,13 +139,20 @@ class CredentialProviderViewController: ASCredentialProviderViewController { private func getWindowPosition() -> Position { let frame = self.view.window?.frame ?? .zero let screenHeight = NSScreen.main?.frame.height ?? 0 + let screenWidth = NSScreen.main?.frame.width ?? 0 // 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))) - return Position(x: centerX, y:centerY) + // passkey modals are 600x600. + let modalHeight: CGFloat = 600; + let modalWidth: CGFloat = 600; + let centerX = round(frame.origin.x + estimatedWidth/2) + let centerY = round(screenHeight - (frame.origin.y + estimatedHeight/2)) + // Check if centerX or centerY are beyond either edge of the screen. If they are find the center of the screen, otherwise use the original value. + let positionX = centerX + modalWidth >= screenWidth || CGFloat(centerX) - modalWidth <= 0 ? Int32(screenWidth/2) : Int32(centerX) + let positionY = centerY + modalHeight >= screenHeight || CGFloat(centerY) - modalHeight <= 0 ? Int32(screenHeight/2) : Int32(centerY) + return Position(x: positionX, y: positionY) } override func loadView() { From cd4f5fbdb9cca5dbe620f0b3f7b5d48f561e69aa Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:41:03 +0200 Subject: [PATCH 33/98] Refactor Fido2 Components (#15105) * Refactor Fido2 Components * Address error message and missing session * Address remaining missing session * Reset modals so subsequent creates work (#15145) * Fix broken test * Rename relevantCiphers to displayedCiphers * Clean up heading settings, errors, and other concerns * Address missing comments and throw error in try block * fix type issue for SimpleDialogType * fix type issue for SimpleDialogType * Revert new type * try using as null to satisfy type issue * Remove use of firstValueFrom in create component --- apps/desktop/electron-builder.json | 4 +- .../credentials/fido2-create.component.html | 2 +- .../fido2-create.component.spec.ts | 41 ++-- .../credentials/fido2-create.component.ts | 209 +++++++++++------- .../fido2-excluded-ciphers.component.spec.ts | 13 +- .../fido2-excluded-ciphers.component.ts | 17 +- .../credentials/fido2-vault.component.spec.ts | 12 - .../credentials/fido2-vault.component.ts | 86 ++++--- .../desktop-fido2-user-interface.service.ts | 5 + 9 files changed, 214 insertions(+), 175 deletions(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 1e96198d4ad..3b42f0f18d5 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -67,7 +67,6 @@ ], "CFBundleDevelopmentRegion": "en" }, - "provisioningProfile": "bitwarden_desktop_developer_id.provisionprofile", "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", "extraFiles": [ { @@ -142,8 +141,7 @@ "extendInfo": { "LSMinimumSystemVersion": "12", "ElectronTeamID": "LTZ2PFU5D6" - }, - "provisioningProfile": "bitwarden_desktop_appstore.provisionprofile" + } }, "nsisWeb": { "oneClick": false, diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index 46d2a6da731..70c6a328f65 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -38,7 +38,7 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + - + + + + diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index d2cb14ea91e..e19d96ac464 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -8,9 +8,12 @@ import AuthenticationServices import os + class CredentialProviderViewController: ASCredentialProviderViewController { let logger: Logger + @IBOutlet weak var statusLabel: NSTextField! + // There is something a bit strange about the initialization/deinitialization in this class. // Sometimes deinit won't be called after a request has successfully finished, // which would leave this class hanging in memory and the IPC connection open. @@ -108,7 +111,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { logger.log("[autofill-extension] initializing extension") - super.init(nibName: nil, bundle: nil) + super.init(nibName: "CredentialProviderViewController", bundle: nil) // Setup connection monitoring now that self is available setupConnectionMonitoring() @@ -126,16 +129,6 @@ class CredentialProviderViewController: ASCredentialProviderViewController { connectionMonitorTimer = nil } - - @IBAction func cancel(_ sender: AnyObject?) { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) - } - - @IBAction func passwordSelected(_ sender: AnyObject?) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) - } - private func getWindowPosition() -> Position { let frame = self.view.window?.frame ?? .zero let screenHeight = NSScreen.main?.frame.height ?? 0 @@ -155,16 +148,27 @@ class CredentialProviderViewController: ASCredentialProviderViewController { return Position(x: positionX, y: positionY) } - override func loadView() { - let view = NSView() - // Hide the native window since we only need the IPC connection - view.isHidden = true - self.view = view + override func viewDidLoad() { + super.viewDidLoad() + + // Initially hide the view + self.view.isHidden = true } override func prepareInterfaceForExtensionConfiguration() { + // Show the configuration UI + self.view.isHidden = false + + // Set the localized message + statusLabel.stringValue = NSLocalizedString("autofillConfigurationMessage", comment: "Message shown when Bitwarden is enabled in system settings") + + // Send the native status request client.sendNativeStatus(key: "request-sync", value: "") - self.extensionContext.completeExtensionConfigurationRequest() + + // Complete the configuration after 2 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + self?.extensionContext.completeExtensionConfigurationRequest() + } } override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) { diff --git a/apps/desktop/macos/autofill-extension/bitwarden-icon.png b/apps/desktop/macos/autofill-extension/bitwarden-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9a05bc7bbdd8061e741cbfdbcbc7148366f3ad61 GIT binary patch literal 3291 zcmV<13?%c3P)|~|L-n`m1W7rbaV|hwUR~~dlUN8$qJiFrO+Ci>VOCTs|UdD5G?{`J57Prm%ko`HD=KKJEs{E8=T^y8ZcJ*DL1t*sT?RE8{? z+srLt2!oAsX$U5w4TfCGg*6RwVUM1+zx>XB@+JS!Co!MI^Z&$8e!+b`@IUbaA7=#D4Ue9l_~_Ta;|IR{r#+36r?=;L;6Ks} z2$|?W6poQ3Ar}(Dfh0;YsgUg$DQ%6A$%K#%Yl#jzwzZ^=NgSJFq2Ty@87-0 zcYpso+O}e&C~ZbAoPGHHzvmtQ>i56S9h_WYXJ=o5Dui(0aLeKI9y{yLW^|^OSPocHF>l%&*FYg6Q}n z47ts@JKN6AKVOG14YHn9$0 zk7w$|U+{bn+<%XcH^+1`&4pYlO*1+>`+N+!&AF>Rz_B48GmXjNIE1+i-}H*taXNF$ zkeOV_49VdK|Jqmj>Mwi9ueE5Nlv}2`G&&%6aPDa(%*TY4$!6}>`Td+dM+X~BOSur{ z(jjMVSI4r(U~b8prr0qIxy`vNY%NlYT$m{tQo`JYLZ^(@hKw;*E;MFyy9oxxMlQ54 zzbbcd?h18;F*MfD&|#Y~cWJ|p6(X?_=F(=4D4$^HkW(D1xv)_fj+?G>?n=$2+>+za z>106%7UpiGN^zP69av>hP?+0P(YQHJ+9;PQPa1NYb60B;6uY1fSu1Gn){b#Fou))% zaf~6i9NTLBc62cWcrTsQK28!fWgafGulccE%0t7Bvvg}J3wPBM3}R%KI%ZJsi>w1pwJId`STf!V0gJZUn= z+_gp!R)-54WhGjYI|;`$sE#pDYBL>kg>zR5DVK)iLd{@)9i%drkEx)|3PL_n2#e-Y zVOU{>+`+jkbYy2#NCsgOQhptTin$c#mR57A$S24e%P|w%qFCmbp@VLE@fSSb?cVmT$EQ@y zF+@&lLmldX+`-8eP8uPX22pM~w#_4Ne$?H#c^plM4jUXQM29V3{X1UZ`S;)B<2~}` zM{Or0A$8E94mpt1T=-fok)L{J?!v|fsUn2Kg&e;14}Ol9e(l3<_dWmS zmoQ0{4x~gXlPy;{xxz`?TBV|pEh@sPN8a-IUi-+qe7cf4kZF}Ir+Ft*b4|(OQ ze$J<(Bb7Q3)f{{Hq5J&sE56FZFMO`s{jDE(eQ*1f8`?~xZMo%uw$$O4Aa`(bg(IaV zbr_U7=s<{K=l#te_=R5k$UA-NYJzguj=lJ|JkJlk;;TIT!soi(PdxHYulyHp*x}5O z10j?P-CSiZgk0t13a1_8SPqt?4#oz@lnY_!_w7&pjsMnL-}M2XnwA66;m>}<7xbQe&`e@I;JRO z4pkP5MF&EVJ2<(sI-Na1$QGj1LE#u7QP}BhZ+Xv+ebZn0F^|0YS3DyfzUz;D@&5BG zU$U?HvWMKs>)-S%zV16-<1N2(lhfJiKsFLm+L{YRA&TRpZ^zuh$rVmFZ#+&&B?m%C z6(Ms>2jX<*E$_YICExKH-}~yH_sRV9-}?sN^)0{M1NYzKPQLHekNCRpc#X$Cd}e27 zstBpWC?bju$%Q0Q??vw5+?{ni@%SS z^JTxw^Y6dUojm+mpW#j(``|~s@>M_UdtdWQI=dF9Nn0zIHY!-Oxg{(^Nl(1R6;7_u z``-8ygb=c|3Ly&FLP!@J+v(&uJ*V&akvI9eZ~tL${pH6!Q?GsfJG|uEf7tiF=6`c` zAKMI$5e^)bt!>#_MIwZ3?RfkxuSM=)uJ#$f=^MZQbvwK7OI?^tB~RL{7;+&q(PqNp zxM9afyz~#f$alW%D}DZNex5sd?1LZixBmH0`@Vnsi#ofHotJH72IbO*h-k1)hc=7G zI&QqZ|Mcg-?0xTh-(&9J+|@pGJn?}aJv+PiOQEzGwoz1=Te3|RO=DEHGso##ulg@< z_u4nSYcG4r7y8bZ{ccZr?1LZieXoAK?|Jnv^w@{3v*+09jBJ8cmtkG=1GA9RIzdY}25zUlj4?{xk-Gs(v+noGl)RPNi)!yLOer_*4jVhkB_VaV-r z&-+DG2$wQqR0j~wTm?qMfmgk0F>NsSE= zZJyNHOxl{Q;e&7ZXWxADJ#YL49IkNY>Gje7`#;{|-sk?}Jr91-AFPhy_A=im#U_ch=3$M1RQxBY2n_kK1jO!K7DHZ;G+qWLuz z&7}pE&3p>dv5pVkeE-tM{ovntyZ-r@W4IqfB*aM#XacSwQD}!_3PJ% zJn7oCYp!3v4nywX+O=!0U%&3!wQH_lzwW}dYu7yG`t|E@6CcG5T*nj0v!3;=XFcng Z`+p}j2wIZUE=m9Z002ovPDHLkV1iQWv4Q{q literal 0 HcmV?d00001 diff --git a/apps/desktop/macos/autofill-extension/en.lproj/Localizable.strings b/apps/desktop/macos/autofill-extension/en.lproj/Localizable.strings new file mode 100644 index 00000000000..95730dff286 --- /dev/null +++ b/apps/desktop/macos/autofill-extension/en.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Message shown during passkey configuration */ +"autofillConfigurationMessage" = "Enabling Bitwarden..."; diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj index ff257097f26..ed19fc9ef5d 100644 --- a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj +++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */; }; 3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */; }; + 9AE299092DF9D82E00AAE454 /* bitwarden-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */; }; + 9AE299122DFB57A200AAE454 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9AE2990D2DFB57A200AAE454 /* Localizable.strings */; }; E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; }; E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; }; E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; }; @@ -18,6 +20,8 @@ 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = ""; }; 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = ""; }; 968ED08A2C52A47200FFFEE6 /* ReleaseAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseAppStore.xcconfig; sourceTree = ""; }; + 9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "bitwarden-icon.png"; sourceTree = ""; }; + 9AE2990C2DFB57A200AAE454 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable.strings; sourceTree = ""; }; D83832AB2D67B9AE003FB9F8 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; D83832AD2D67B9D0003FB9F8 /* ReleaseDeveloper.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseDeveloper.xcconfig; sourceTree = ""; }; E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -41,6 +45,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9AE2990E2DFB57A200AAE454 /* en.lproj */ = { + isa = PBXGroup; + children = ( + 9AE2990D2DFB57A200AAE454 /* Localizable.strings */, + ); + path = en.lproj; + sourceTree = ""; + }; E1DF711D2B342E2800F29026 = { isa = PBXGroup; children = ( @@ -73,6 +85,8 @@ E1DF71402B342F6900F29026 /* autofill-extension */ = { isa = PBXGroup; children = ( + 9AE2990E2DFB57A200AAE454 /* en.lproj */, + 9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */, 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */, E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */, E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */, @@ -124,6 +138,7 @@ knownRegions = ( en, Base, + sv, ); mainGroup = E1DF711D2B342E2800F29026; productRefGroup = E1DF71272B342E2800F29026 /* Products */; @@ -141,6 +156,8 @@ buildActionMask = 2147483647; files = ( E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */, + 9AE299122DFB57A200AAE454 /* Localizable.strings in Resources */, + 9AE299092DF9D82E00AAE454 /* bitwarden-icon.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -159,6 +176,14 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ + 9AE2990D2DFB57A200AAE454 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 9AE2990C2DFB57A200AAE454 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */ = { isa = PBXVariantGroup; children = ( From c0fa664d2e01f8fdacba2563e58d15b6dcfd54e8 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:18:40 +0200 Subject: [PATCH 35/98] Add provisioning profile values to electron build (#15412) --- apps/desktop/electron-builder.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 3b42f0f18d5..1e96198d4ad 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -67,6 +67,7 @@ ], "CFBundleDevelopmentRegion": "en" }, + "provisioningProfile": "bitwarden_desktop_developer_id.provisionprofile", "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", "extraFiles": [ { @@ -141,7 +142,8 @@ "extendInfo": { "LSMinimumSystemVersion": "12", "ElectronTeamID": "LTZ2PFU5D6" - } + }, + "provisioningProfile": "bitwarden_desktop_appstore.provisionprofile" }, "nsisWeb": { "oneClick": false, From 12230c3fdc9bb07053d38127f8a67dfa185d5d2a Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Tue, 1 Jul 2025 18:45:35 +0200 Subject: [PATCH 36/98] Address BitwardenShield icon issue --- .../autofill/modal/credentials/bitwarden-shield.icon.ts | 7 +++++++ .../autofill/modal/credentials/fido2-create.component.ts | 2 +- .../modal/credentials/fido2-excluded-ciphers.component.ts | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/autofill/modal/credentials/bitwarden-shield.icon.ts diff --git a/apps/desktop/src/autofill/modal/credentials/bitwarden-shield.icon.ts b/apps/desktop/src/autofill/modal/credentials/bitwarden-shield.icon.ts new file mode 100644 index 00000000000..86e3a0bb1b2 --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/bitwarden-shield.icon.ts @@ -0,0 +1,7 @@ +import { svgIcon } from "@bitwarden/components"; + +export const BitwardenShield = svgIcon` + + + +`; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index a3af52bdc73..cd751499adb 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -4,7 +4,6 @@ import { RouterModule, Router } from "@angular/router"; import { combineLatest, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BitwardenShield } from "@bitwarden/auth/angular"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; @@ -32,6 +31,7 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; +import { BitwardenShield } from "./bitwarden-shield.icon"; import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; const DIALOG_MESSAGES = { diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts index 33d9237bfee..70c45f3585a 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts @@ -3,7 +3,6 @@ import { Component, OnInit, OnDestroy } from "@angular/core"; import { RouterModule, Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BitwardenShield } from "@bitwarden/auth/angular"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BadgeModule, @@ -23,6 +22,7 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; +import { BitwardenShield } from "./bitwarden-shield.icon"; import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; @Component({ From 5518a3813b55e516b81592000ac18478d17c5237 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Tue, 1 Jul 2025 19:46:12 +0200 Subject: [PATCH 37/98] Fix fido2-vault component --- .../src/autofill/modal/credentials/fido2-vault.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index 5bffc3020e1..e3062b0c7d9 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -4,7 +4,6 @@ import { RouterModule, Router } from "@angular/router"; import { firstValueFrom, map, BehaviorSubject, Observable, Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BitwardenShield } from "@bitwarden/auth/angular"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -30,6 +29,8 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; +import { BitwardenShield } from "./bitwarden-shield.icon"; + @Component({ standalone: true, imports: [ From 448a9292b709e9254837decada43c87874ffec52 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:02:02 +0200 Subject: [PATCH 38/98] Display the vault modal when selecting Bitwarden... (#15257) --- .../src/autofill/services/desktop-autofill.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index f990160599b..fafd5a2127f 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -261,7 +261,7 @@ export class DesktopAutofillService implements OnDestroy { } const response = await this.fido2AuthenticatorService.getAssertion( - this.convertAssertionRequest(request), + this.convertAssertionRequest(request, true), { windowXy: request.windowXy }, controller, ); @@ -359,6 +359,7 @@ export class DesktopAutofillService implements OnDestroy { request: | autofill.PasskeyAssertionRequest | autofill.PasskeyAssertionWithoutUserInterfaceRequest, + assumeUserPresence: boolean = false, ): Fido2AuthenticatorGetAssertionParams { let allowedCredentials; if ("credentialId" in request) { @@ -383,7 +384,7 @@ export class DesktopAutofillService implements OnDestroy { requireUserVerification: request.userVerification === "required" || request.userVerification === "preferred", fallbackSupported: false, - assumeUserPresence: true, // For desktop assertions, it's safe to assume UP has been checked by OS dialogues + assumeUserPresence, }; } From c63913d28ce80d5d9a5ae7000b563ef27a6794cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 3 Jul 2025 22:11:10 +0200 Subject: [PATCH 39/98] Passkeys filtering breaks on SSH keys (#15448) --- .../src/autofill/modal/credentials/fido2-create.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index cd751499adb..d85a90e20ea 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -8,6 +8,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService, @@ -172,6 +173,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { const allCiphers = await this.cipherService.getAllDecrypted(activeUserId); return allCiphers.filter( (cipher) => + cipher.type == CipherType.Login && cipher.login?.matchesUri(rpid, equivalentDomains) && Fido2Utils.cipherHasNoOtherPasskeys(cipher, userHandle) && !cipher.deletedDate, From 9eb55c44656ce6626a29639bb133a7b385a5ce18 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:48:37 +0200 Subject: [PATCH 40/98] Display the blue header on the locked vault passkey flow (#15655) --- .../autofill/services/desktop-fido2-user-interface.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 2d1ca87a048..b087816746f 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -263,13 +263,13 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private async showUi( route: string, position?: { x: number; y: number }, - showTrafficButtons?: boolean, + showTrafficButtons: boolean = false, disableRedirect?: boolean, ): Promise { // Load the UI: await this.desktopSettingsService.setModalMode(true, showTrafficButtons, position); await this.centerOffscreenPopup(); - await this.accountService.setShowHeader(false); + await this.accountService.setShowHeader(showTrafficButtons); await this.router.navigate([ route, { From fa3483d33f2668e19bb351cab3a189fb329333b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Wed, 30 Jul 2025 18:04:13 +0200 Subject: [PATCH 41/98] PM-23848: Use the MacOS UI-friendly API instead (#15650) * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential --- .../CredentialProviderViewController.xib | 2 +- .../CredentialProviderViewController.swift | 37 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib index 70d6d77fa54..132882c6477 100644 --- a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib +++ b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib @@ -29,7 +29,7 @@ - + diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index e19d96ac464..a81b9c35440 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -13,6 +13,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { let logger: Logger @IBOutlet weak var statusLabel: NSTextField! + @IBOutlet weak var logoImageView: NSImageView! // There is something a bit strange about the initialization/deinitialization in this class. // Sometimes deinit won't be called after a request has successfully finished, @@ -170,14 +171,30 @@ class CredentialProviderViewController: ASCredentialProviderViewController { self?.extensionContext.completeExtensionConfigurationRequest() } } - + + /* + In order to implement this method, we need to query the state of the vault to be unlocked and have one and only one matching credential so that it doesn't need to show ui. + If we do show UI, it's going to fail and disconnect after the platform timeout which is 3s. + For now we just claim to always need UI displayed. + */ override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) { + let error = ASExtensionError(.userInteractionRequired) + self.extensionContext.cancelRequest(withError: error) + return + } + + /* + Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with + ASExtensionError.userInteractionRequired. In this case, the system may present your extension's + UI and call this method. Show appropriate UI for authenticating the user then provide the password + by completing the extension request with the associated ASPasswordCredential. + */ + override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) { let timeoutTimer = createTimer() - if let request = credentialRequest as? ASPasskeyCredentialRequest { if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity { - - logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(passkey) called \(request)") + + logger.log("[autofill-extension] prepareInterfaceToProvideCredential (passkey) called \(request)") class CallbackImpl: PreparePasskeyAssertionCallback { let ctx: ASCredentialProviderExtensionContext @@ -217,6 +234,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } + /* + We're still using the old request type here, because we're sending the same data, we're expecting a single credential to be used + */ let req = PasskeyAssertionWithoutUserInterfaceRequest( rpId: passkeyIdentity.relyingPartyIdentifier, credentialId: passkeyIdentity.credentialID, @@ -238,16 +258,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong") self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request")) } - - /* - Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with - ASExtensionError.userInteractionRequired. In this case, the system may present your extension's - UI and call this method. Show appropriate UI for authenticating the user then provide the password - by completing the extension request with the associated ASPasswordCredential. - override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { - } - */ private func createTimer() -> DispatchWorkItem { // Create a timer for 600 second timeout From 34cdcf231b684cd64c4332e7f6f698b2d44efdcb Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:05:08 +0200 Subject: [PATCH 42/98] Fix action text and close vault modal (#15634) * Fix action text and close vault modal * Fix broken tests --- .../fido2-create.component.spec.ts | 6 +- .../credentials/fido2-create.component.ts | 58 ++++++++++--------- .../credentials/fido2-vault.component.ts | 2 +- apps/desktop/src/locales/en/messages.json | 6 +- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts index 54a637ec974..778215895ee 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts @@ -137,7 +137,8 @@ describe("Fido2CreateComponent", () => { title: { key: "unableToSavePasskey" }, content: { key: "closeThisBitwardenWindow" }, type: "danger", - acceptButtonText: { key: "closeBitwarden" }, + acceptButtonText: { key: "closeThisWindow" }, + acceptAction: expect.any(Function), cancelButtonText: null, }); }); @@ -217,7 +218,8 @@ describe("Fido2CreateComponent", () => { title: { key: "unableToSavePasskey" }, content: { key: "closeThisBitwardenWindow" }, type: "danger", - acceptButtonText: { key: "closeBitwarden" }, + acceptButtonText: { key: "closeThisWindow" }, + acceptAction: expect.any(Function), cancelButtonText: null, }); }); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index d85a90e20ea..f3342d6bc24 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -35,28 +35,6 @@ import { import { BitwardenShield } from "./bitwarden-shield.icon"; import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; -const DIALOG_MESSAGES = { - unexpectedErrorShort: { - title: { key: "unexpectedErrorShort" }, - content: { key: "closeThisBitwardenWindow" }, - type: "danger", - acceptButtonText: { key: "closeBitwarden" }, - cancelButtonText: null as null, - }, - unableToSavePasskey: { - title: { key: "unableToSavePasskey" }, - content: { key: "closeThisBitwardenWindow" }, - type: "danger", - acceptButtonText: { key: "closeBitwarden" }, - cancelButtonText: null as null, - }, - overwritePasskey: { - title: { key: "overwritePasskey" }, - content: { key: "alreadyContainsPasskey" }, - type: "warning", - }, -} as const satisfies Record; - @Component({ standalone: true, imports: [ @@ -81,6 +59,32 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { readonly Icons = { BitwardenShield }; protected fido2PasskeyExistsIcon = Fido2PasskeyExistsIcon; + private get DIALOG_MESSAGES() { + return { + unexpectedErrorShort: { + title: { key: "unexpectedErrorShort" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeThisWindow" }, + cancelButtonText: null as null, + acceptAction: async () => this.dialogService.closeAll(), + }, + unableToSavePasskey: { + title: { key: "unableToSavePasskey" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeThisWindow" }, + cancelButtonText: null as null, + acceptAction: async () => this.dialogService.closeAll(), + }, + overwritePasskey: { + title: { key: "overwritePasskey" }, + content: { key: "alreadyContainsPasskey" }, + type: "warning", + }, + } as const satisfies Record; + } + constructor( private readonly desktopSettingsService: DesktopSettingsService, private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, @@ -98,7 +102,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { const rpid = await this.session?.getRpId(); if (!this.session) { - await this.showErrorDialog(DIALOG_MESSAGES.unableToSavePasskey); + await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey); return; } @@ -119,7 +123,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { this.session.notifyConfirmCreateCredential(isConfirmed, cipher); } catch { - await this.showErrorDialog(DIALOG_MESSAGES.unableToSavePasskey); + await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey); return; } @@ -134,7 +138,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { this.session.notifyConfirmCreateCredential(true); } catch { - await this.showErrorDialog(DIALOG_MESSAGES.unableToSavePasskey); + await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey); } await this.closeModal(); @@ -179,7 +183,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { !cipher.deletedDate, ); } catch { - await this.showErrorDialog(DIALOG_MESSAGES.unexpectedErrorShort); + await this.showErrorDialog(this.DIALOG_MESSAGES.unexpectedErrorShort); return []; } }), @@ -189,7 +193,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { private async validateCipherAccess(cipher: CipherView): Promise { if (cipher.login.hasFido2Credentials) { const overwriteConfirmed = await this.dialogService.openSimpleDialog( - DIALOG_MESSAGES.overwritePasskey, + this.DIALOG_MESSAGES.overwritePasskey, ); if (!overwriteConfirmed) { diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index e3062b0c7d9..a91ee15b021 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -85,7 +85,7 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { title: { key: "unexpectedErrorShort" }, content: { key: "closeThisBitwardenWindow" }, type: "danger", - acceptButtonText: { key: "closeBitwarden" }, + acceptButtonText: { key: "closeThisWindow" }, cancelButtonText: null, }); await this.closeModal(); diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 7a92fc61dcc..1dc559b88ad 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3211,7 +3211,7 @@ "orgTrustWarning1": { "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." }, - "trustUser":{ + "trustUser": { "message": "Trust user" }, "inputRequired": { @@ -3781,8 +3781,8 @@ "applicationDoesNotSupportDuplicates": { "message": "This application does not support duplicates." }, - "closeBitwarden": { - "message": "Close Bitwarden" + "closeThisWindow": { + "message": "Close this window" }, "allowScreenshots": { "message": "Allow screen capture" From d6621f27f6c38ae900ba810cfb0d2a26323d7581 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:11:26 +0200 Subject: [PATCH 43/98] Update SVG to support dark mode (#15805) --- .../credentials/fido2-create.component.html | 2 +- .../fido2-excluded-ciphers.component.html | 2 +- .../credentials/fido2-passkey-exists-icon.ts | 32 ++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index 70c6a328f65..d4241679604 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -27,7 +27,7 @@
- +
{{ "noMatchingLoginsForSite" | i18n }}
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html index 94bfd7d06d2..4ddf7cc4840 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html @@ -29,7 +29,7 @@ class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-start tw-items-center tw-gap-2 tw-h-full tw-px-5" >
- +
{{ "passkeyAlreadyExists" | i18n }} {{ "applicationDoesNotSupportDuplicates" | i18n }} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts b/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts index 5a179f595fd..66c3ebe0f5b 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts @@ -1,16 +1,24 @@ import { svgIcon } from "@bitwarden/components"; -export const Fido2PasskeyExistsIcon = svgIcon` - - - - - - - - - - - +export const Fido2PasskeyExistsIcon = svgIcon` + + + + + + + + + + + `; From 036771c50f29d7ccbfa0c69be129a29ca7cfbf2a Mon Sep 17 00:00:00 2001 From: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:00:39 +0200 Subject: [PATCH 44/98] When a locked vault is unlocked displays correctly (#15612) * When a locked vault is unlocked displays correctly * Keep old behavior while checking for recently unlocked vault * Revert the electron-builder * Simplify by using a simple redirect when vault unlocked * Remove single use of `userSelectedCipher` * Add a guard clause to unlock * Revert to original spacing * Add reactive guard to unlock vault * Fix for passkey picker closing prematurely * Remove unneeded root navigation in ensureUnlockedVault * Fix vault not unlocking --- apps/desktop/src/app/app-routing.module.ts | 3 +- .../autofill/guards/reactive-vault-guard.ts | 42 +++++++++++++++++++ .../credentials/fido2-create.component.ts | 5 ++- .../fido2-excluded-ciphers.component.spec.ts | 4 +- .../fido2-excluded-ciphers.component.ts | 8 +++- .../credentials/fido2-vault.component.ts | 5 ++- .../desktop-fido2-user-interface.service.ts | 6 ++- 7 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 apps/desktop/src/autofill/guards/reactive-vault-guard.ts diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 75fccbd400d..40cfff10563 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -47,6 +47,7 @@ import { LockComponent } from "@bitwarden/key-management-ui"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; +import { reactiveUnlockVaultGuard } from "../autofill/guards/reactive-vault-guard"; import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create.component"; import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component"; import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component"; @@ -296,7 +297,7 @@ const routes: Routes = [ }, { path: "lock", - canActivate: [lockGuard()], + canActivate: [lockGuard(), reactiveUnlockVaultGuard], data: { pageIcon: Icons.LockIcon, pageTitle: { diff --git a/apps/desktop/src/autofill/guards/reactive-vault-guard.ts b/apps/desktop/src/autofill/guards/reactive-vault-guard.ts new file mode 100644 index 00000000000..d16787ef46a --- /dev/null +++ b/apps/desktop/src/autofill/guards/reactive-vault-guard.ts @@ -0,0 +1,42 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { combineLatest, map, switchMap, distinctUntilChanged } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; + +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; + +/** + * Reactive route guard that redirects to the unlocked vault. + * Redirects to vault when unlocked in main window. + */ +export const reactiveUnlockVaultGuard: CanActivateFn = () => { + const router = inject(Router); + const authService = inject(AuthService); + const accountService = inject(AccountService); + const desktopSettingsService = inject(DesktopSettingsService); + + return combineLatest([accountService.activeAccount$, desktopSettingsService.modalMode$]).pipe( + switchMap(([account, modalMode]) => { + if (!account) { + return [true]; + } + + // Monitor when the vault has been unlocked. + return authService.authStatusFor$(account.id).pipe( + distinctUntilChanged(), + map((authStatus) => { + // If vault is unlocked and we're not in modal mode, redirect to vault + if (authStatus === AuthenticationStatus.Unlocked && !modalMode?.isModalModeActive) { + return router.createUrlTree(["/vault"]); + } + + // Otherwise keep user on the lock screen + return true; + }), + ); + }), + ); +}; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index f3342d6bc24..14d31cca99e 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -145,12 +145,15 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { } async closeModal(): Promise { - await this.router.navigate(["/"]); + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); if (this.session) { this.session.notifyConfirmCreateCredential(false); this.session.confirmChosenCipher(null); } + + await this.router.navigate(["/"]); } private initializeCiphersObservable(rpid: string): void { diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts index b2deef2ce1e..6a465136458 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts @@ -69,8 +69,10 @@ describe("Fido2ExcludedCiphersComponent", () => { await component.closeModal(); + expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(true); expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); - expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); }); }); }); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts index 70c45f3585a..450e2dc186b 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts @@ -64,11 +64,17 @@ export class Fido2ExcludedCiphersComponent implements OnInit, OnDestroy { } async closeModal(): Promise { - await this.router.navigate(["/"]); + // Clean up modal state + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); + // Clean up session state if (this.session) { this.session.notifyConfirmCreateCredential(false); this.session.confirmChosenCipher(null); } + + // Navigate away + await this.router.navigate(["/"]); } } diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index a91ee15b021..fc582798043 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -100,6 +100,9 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { } async closeModal(): Promise { + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); + if (this.session) { this.session.notifyConfirmCreateCredential(false); this.session.confirmChosenCipher(null); @@ -127,8 +130,6 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { this.logService.error("Failed to load ciphers", error); }); }); - - await this.closeModal(); } private async validateCipherAccess(cipher: CipherView): Promise { diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index b087816746f..48cec5a764e 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -338,8 +338,8 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi // make the cipherIds available to the UI. this.availableCipherIdsSubject.next(existingCipherIds); + await this.accountService.setShowHeader(false); await this.showUi("/fido2-excluded", this.windowObject.windowXy, false); - await this.accountService.setShowHeader(true); } async ensureUnlockedVault(): Promise { @@ -362,6 +362,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi this.logService.warning("Error while waiting for vault to unlock", error); } + if (status2 === AuthenticationStatus.Unlocked) { + await this.router.navigate(["/"]); + } + if (status2 !== AuthenticationStatus.Unlocked) { await this.hideUi(); throw new Error("Vault is not unlocked"); From 56cd0f0f1606522947b6c2c6b5375f7b3f5b12b4 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Tue, 5 Aug 2025 15:24:40 +0200 Subject: [PATCH 45/98] Update broken tests for lock component --- .../fido2/fido2-user-interface.service.abstraction.ts | 2 +- libs/common/src/vault/abstractions/cipher.service.ts | 1 + .../src/lock/components/lock.component.spec.ts | 10 +++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 28b199da78f..b8be164c837 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -95,7 +95,7 @@ export abstract class Fido2UserInterfaceSession { */ abstract confirmNewCredential( params: NewCredentialParams, - ): Promise<{ cipherId: string; userVerified: boolean }>; + ): Promise<{ cipherId?: string; userVerified: boolean }>; /** * Make sure that the vault is unlocked. diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 2f4fcf0ef51..78ea9203473 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -66,6 +66,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + abstract getAllDecryptedForIds(userId: UserId, ids: string[]): Promise; abstract filterCiphersForUrl( ciphers: C[], url: string, diff --git a/libs/key-management-ui/src/lock/components/lock.component.spec.ts b/libs/key-management-ui/src/lock/components/lock.component.spec.ts index 8c8429d3788..0f86481aeb9 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.spec.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.spec.ts @@ -2,7 +2,7 @@ import { DebugElement } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { By } from "@angular/platform-browser"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; import { firstValueFrom, interval, map, of, takeWhile, timeout } from "rxjs"; import { ZXCVBNResult } from "zxcvbn"; @@ -91,6 +91,13 @@ describe("LockComponent", () => { const mockLockComponentService = mock(); const mockAnonLayoutWrapperDataService = mock(); const mockBroadcasterService = mock(); + const mockActivatedRoute = { + snapshot: { + paramMap: { + get: jest.fn().mockReturnValue(null), // return null for 'disable-redirect' param + }, + }, + }; beforeEach(async () => { jest.clearAllMocks(); @@ -148,6 +155,7 @@ describe("LockComponent", () => { { provide: LockComponentService, useValue: mockLockComponentService }, { provide: AnonLayoutWrapperDataService, useValue: mockAnonLayoutWrapperDataService }, { provide: BroadcasterService, useValue: mockBroadcasterService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, ], }) .overrideProvider(DialogService, { useValue: mockDialogService }) From 13ce601225adc0ec2e6a63f5a04a46b2e9a506f2 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Tue, 5 Aug 2025 15:49:30 -0400 Subject: [PATCH 46/98] Add missing brace to preload.ts --- apps/desktop/src/autofill/preload.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index d63d27661b0..4a00a73a793 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -147,6 +147,7 @@ export default { fn(clientId, sequenceNumber, status); }, ); + }, configureAutotype: (enabled: boolean) => { ipcRenderer.send("autofill.configureAutotype", { enabled }); }, From 6f63a6dc2cd8adb61469e7ec1f5ede3ffcb68556 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Tue, 5 Aug 2025 15:56:42 -0400 Subject: [PATCH 47/98] Run lint --- apps/desktop/src/autofill/preload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 4a00a73a793..5bc0847bc75 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -144,7 +144,7 @@ export default { }, ) => { const { clientId, sequenceNumber, status } = data; - fn(clientId, sequenceNumber, status); + fn(clientId, sequenceNumber, status); }, ); }, From 7236e48f82df825880baa94272420d88eee95f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 12 Aug 2025 11:47:11 +0200 Subject: [PATCH 48/98] Added explainer --- .../docs/macos-passkey-provider-explainer.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 apps/desktop/desktop_native/macos_provider/docs/macos-passkey-provider-explainer.md diff --git a/apps/desktop/desktop_native/macos_provider/docs/macos-passkey-provider-explainer.md b/apps/desktop/desktop_native/macos_provider/docs/macos-passkey-provider-explainer.md new file mode 100644 index 00000000000..9947788e9fa --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/docs/macos-passkey-provider-explainer.md @@ -0,0 +1,32 @@ +# Explainer: Mac OS Native Passkey Provider + +This explainer explains the changes introduced in https://github.com/bitwarden/clients/pull/13963, where we introduce the MacOS Native Passkey Provider. It gives the high level explanation of the architecture and some of the quirks and additional good to know context. + +## The high level +MacOS has native API’s (similar to iOS) to allow Credential Managers to provide credentials to the MacOS autofill system (in this PR, we only provide passkeys). + +We’ve written a Swift-based native autofill-extension. It’s bundled in the app-bundle in PlugIns, similar to the safari-extension. + +This swift extension currently communicates with our Electron app through IPC based on a unix socket. The IPC implementation is done in Rust and utilized through RustFFI + NAPI bindings. We're not using the IPC framework as our implementation pre-dates the IPC framework. We're also actively looking into alternative like XPC or CFMessagePort to have better support for when the app is sandboxed. + +Electron receives the messages and passes it to Angular (through the electron-renderer event system). + +Our existing fido2 services in the renderer responds to events, displaying UI as necessary, and returns the signature back through the same mechanism, allowing people to authenticate with passkeys through the native system + UI. See +[Mac OS Native Passkey Workflows](https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1828356098/Mac+OS+Native+Passkey+Workflows) for demo videos. + +Note: We have not implemented a new fido2 authenticator or service, we have “only plugged in” to the existing ones and enabled communication between the OS and desktop related UI. + +## Typescript + UI implementations + +We utilize the same FIDO2 implementation and interface that is already present for our browser authentication. It was designed by @coroiu with multiple ‘ui environments' in mind. + +Therefore, a lot of the plumbing is implement in /autofill/services/desktop-fido2-user-interface.service.ts, which implements the interface that our fido2 authenticator/client expects to drive UI related behaviors. + +We’ve also implemented a couple FIDO2 UI components to handle registration/sign in flows, but also improved the “modal mode” of the desktop app. + +## Modal mode + +When modal mode is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position. + +Some modal modes may hide the traffic buttons due to design requirements. + From 3d48e4a37cf54cec4695f2f292009d3b4b61fef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 12 Aug 2025 17:24:42 +0200 Subject: [PATCH 49/98] Moved the explainer --- .../{docs/macos-passkey-provider-explainer.md => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/desktop/desktop_native/macos_provider/{docs/macos-passkey-provider-explainer.md => README.md} (100%) diff --git a/apps/desktop/desktop_native/macos_provider/docs/macos-passkey-provider-explainer.md b/apps/desktop/desktop_native/macos_provider/README.md similarity index 100% rename from apps/desktop/desktop_native/macos_provider/docs/macos-passkey-provider-explainer.md rename to apps/desktop/desktop_native/macos_provider/README.md From 8b94512e4e4634b0417e5f4527467eaa82b3eece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Wed, 13 Aug 2025 16:44:38 +0200 Subject: [PATCH 50/98] Tidying up readme --- .../desktop_native/macos_provider/README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/README.md b/apps/desktop/desktop_native/macos_provider/README.md index 9947788e9fa..b3f5d74170d 100644 --- a/apps/desktop/desktop_native/macos_provider/README.md +++ b/apps/desktop/desktop_native/macos_provider/README.md @@ -3,24 +3,27 @@ This explainer explains the changes introduced in https://github.com/bitwarden/clients/pull/13963, where we introduce the MacOS Native Passkey Provider. It gives the high level explanation of the architecture and some of the quirks and additional good to know context. ## The high level -MacOS has native API’s (similar to iOS) to allow Credential Managers to provide credentials to the MacOS autofill system (in this PR, we only provide passkeys). +MacOS has native APIs (similar to iOS) to allow Credential Managers to provide credentials to the MacOS autofill system (in this PR, we only provide passkeys). We’ve written a Swift-based native autofill-extension. It’s bundled in the app-bundle in PlugIns, similar to the safari-extension. -This swift extension currently communicates with our Electron app through IPC based on a unix socket. The IPC implementation is done in Rust and utilized through RustFFI + NAPI bindings. We're not using the IPC framework as our implementation pre-dates the IPC framework. We're also actively looking into alternative like XPC or CFMessagePort to have better support for when the app is sandboxed. +This swift extension currently communicates with our Electron app through IPC based on a unix socket. The IPC implementation is done in Rust and utilized through RustFFI + NAPI bindings. + +Footnotes: + +* We're not using the IPC framework as the implementation pre-dates the IPC framework. +* Alternatives like XPC or CFMessagePort may have better support for when the app is sandboxed. Electron receives the messages and passes it to Angular (through the electron-renderer event system). -Our existing fido2 services in the renderer responds to events, displaying UI as necessary, and returns the signature back through the same mechanism, allowing people to authenticate with passkeys through the native system + UI. See +Our existing fido2 services in the renderer respond to events, displaying UI as necessary, and returns the signature back through the same mechanism, allowing people to authenticate with passkeys through the native system + UI. See [Mac OS Native Passkey Workflows](https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1828356098/Mac+OS+Native+Passkey+Workflows) for demo videos. -Note: We have not implemented a new fido2 authenticator or service, we have “only plugged in” to the existing ones and enabled communication between the OS and desktop related UI. - ## Typescript + UI implementations We utilize the same FIDO2 implementation and interface that is already present for our browser authentication. It was designed by @coroiu with multiple ‘ui environments' in mind. -Therefore, a lot of the plumbing is implement in /autofill/services/desktop-fido2-user-interface.service.ts, which implements the interface that our fido2 authenticator/client expects to drive UI related behaviors. +Therefore, a lot of the plumbing is implemented in /autofill/services/desktop-fido2-user-interface.service.ts, which implements the interface that our fido2 authenticator/client expects to drive UI related behaviors. We’ve also implemented a couple FIDO2 UI components to handle registration/sign in flows, but also improved the “modal mode” of the desktop app. @@ -28,5 +31,5 @@ We’ve also implemented a couple FIDO2 UI components to handle registration/sig When modal mode is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position. -Some modal modes may hide the traffic buttons due to design requirements. +Some modal modes may hide the 'traffic buttons' (window controls) due to design requirements. From a1228b6b7e69d405f6af3937f2baa117c4dd30ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Fri, 15 Aug 2025 15:28:36 +0200 Subject: [PATCH 51/98] Add feature flag to short-circuit the passkey provider (#16003) * Add feature flag to short-circuit the passkey provider * Check FF in renderer instead --- .../services/desktop-autofill.service.ts | 33 ++++++++++++++++++- .../main/autofill/native-autofill.main.ts | 2 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index ef86a26b56f..aec09f03021 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -65,7 +65,7 @@ export class DesktopAutofillService implements OnDestroy { .getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync) .pipe( distinctUntilChanged(), - //filter((enabled) => enabled === true), // Only proceed if feature is enabled + filter((enabled) => enabled === true), // Only proceed if feature is enabled switchMap(() => { return combineLatest([ this.accountService.activeAccount$.pipe( @@ -191,6 +191,14 @@ export class DesktopAutofillService implements OnDestroy { listenIpc() { ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => { + if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) { + this.logService.debug( + "listenPasskeyRegistration: MacOsNativeCredentialSync feature flag is disabled", + ); + callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null); + return; + } + this.registrationRequest = request; this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request); @@ -217,6 +225,14 @@ export class DesktopAutofillService implements OnDestroy { ipc.autofill.listenPasskeyAssertionWithoutUserInterface( async (clientId, sequenceNumber, request, callback) => { + if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) { + this.logService.debug( + "listenPasskeyAssertionWithoutUserInterface: MacOsNativeCredentialSync feature flag is disabled", + ); + callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null); + return; + } + this.logService.warning( "listenPasskeyAssertion without user interface", clientId, @@ -276,6 +292,14 @@ export class DesktopAutofillService implements OnDestroy { ); ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => { + if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) { + this.logService.debug( + "listenPasskeyAssertion: MacOsNativeCredentialSync feature flag is disabled", + ); + callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null); + return; + } + this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request); const controller = new AbortController(); @@ -295,6 +319,13 @@ export class DesktopAutofillService implements OnDestroy { // Listen for native status messages ipc.autofill.listenNativeStatus(async (clientId, sequenceNumber, status) => { + if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) { + this.logService.debug( + "listenNativeStatus: MacOsNativeCredentialSync feature flag is disabled", + ); + return; + } + this.logService.info("Received native status", status.key, status.value); if (status.key === "request-sync") { // perform ad-hoc sync diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index d14efefa81f..61b3bc5f48b 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -129,7 +129,7 @@ export class NativeAutofillMain { }, ); - ipcMain.on("autofill.listenerReady", async () => { + ipcMain.on("autofill.listenerReady", () => { this.listenerReady = true; this.logService.info( `Listener is ready, flushing ${this.messageBuffer.length} buffered messages`, From 22d2432b327844e5835959bd2989a07becc44272 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Fri, 15 Aug 2025 16:53:39 +0200 Subject: [PATCH 52/98] Lint fixes --- .../components/lit-stories/.lit-docs/action-button.mdx | 1 - .../components/lit-stories/.lit-docs/badge-button.mdx | 1 - .../components/lit-stories/.lit-docs/close-button.mdx | 1 - .../components/lit-stories/.lit-docs/edit-button.mdx | 1 - .../services/local-backed-session-storage.service.spec.ts | 8 ++++---- libs/components/src/icon/icon.mdx | 5 ----- libs/components/src/stories/introduction.mdx | 1 - 7 files changed, 4 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx index 73cd6fb93a9..fcec5bb7a82 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx @@ -25,7 +25,6 @@ It is designed with accessibility and responsive design in mind. ## Installation and Setup 1. Ensure you have the necessary dependencies installed: - - `lit`: Used to render the component. - `@emotion/css`: Used for styling the component. diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/badge-button.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/badge-button.mdx index 47d82ad68da..b5ea41b283c 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/badge-button.mdx +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/badge-button.mdx @@ -25,7 +25,6 @@ handling, and a disabled state. The component is optimized for accessibility and ## Installation and Setup 1. Ensure you have the necessary dependencies installed: - - `lit`: Used to render the component. - `@emotion/css`: Used for styling the component. diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/close-button.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/close-button.mdx index da9c15246fd..03a7b72001a 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/close-button.mdx +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/close-button.mdx @@ -22,7 +22,6 @@ a close icon for visual clarity. The component is designed to be intuitive and a ## Installation and Setup 1. Ensure you have the necessary dependencies installed: - - `lit`: Used to render the component. - `@emotion/css`: Used for styling the component. diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/edit-button.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/edit-button.mdx index c6c4262806b..a5a791ffbe1 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/edit-button.mdx +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/edit-button.mdx @@ -25,7 +25,6 @@ or settings where inline editing is required. ## Installation and Setup 1. Ensure you have the necessary dependencies installed: - - `lit`: Used to render the component. - `@emotion/css`: Used for styling the component. diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 1b4665b3222..947fecb5aac 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -50,8 +50,8 @@ describe("LocalBackedSessionStorage", () => { const result = await sut.get("test"); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-expressions - expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey), - expect(result).toEqual("decrypted"); + (expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey), + expect(result).toEqual("decrypted")); }); it("caches the decrypted value when one is stored in local storage", async () => { @@ -69,8 +69,8 @@ describe("LocalBackedSessionStorage", () => { const result = await sut.get("test"); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-expressions - expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey), - expect(result).toEqual("decrypted"); + (expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey), + expect(result).toEqual("decrypted")); }); it("caches the decrypted value when one is stored in local storage", async () => { diff --git a/libs/components/src/icon/icon.mdx b/libs/components/src/icon/icon.mdx index bf350d96e81..e4186b5e4a9 100644 --- a/libs/components/src/icon/icon.mdx +++ b/libs/components/src/icon/icon.mdx @@ -19,7 +19,6 @@ import { IconModule } from "@bitwarden/components"; ## Developer Instructions 1. **Download the SVG** and import it as an `.svg` initially into the IDE of your choice. - - The SVG should be formatted using either a built-in formatter or an external tool like [SVG Formatter Beautifier](https://codebeautify.org/svg-formatter-beautifier) to make applying classes easier. @@ -35,7 +34,6 @@ import { IconModule } from "@bitwarden/components"; ``` 5. **Replace any hardcoded strokes or fills** with the appropriate Tailwind class. - - **Note:** Stroke is used when styling the outline of an SVG path, while fill is used when styling the inside of an SVG path. @@ -75,14 +73,11 @@ import { IconModule } from "@bitwarden/components"; 6. **Remove any hardcoded width or height attributes** if your SVG has a configured [viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox) attribute in order to allow the SVG to scale to fit its container. - - **Note:** Scaling is required for any SVG used as an [AnonLayout](?path=/docs/auth-anon-layout--docs) `pageIcon`. 7. **Import your SVG const** anywhere you want to use the SVG. - - **Angular Component Example:** - - **TypeScript:** ```typescript diff --git a/libs/components/src/stories/introduction.mdx b/libs/components/src/stories/introduction.mdx index eb7328cd76d..7580262a6ef 100644 --- a/libs/components/src/stories/introduction.mdx +++ b/libs/components/src/stories/introduction.mdx @@ -153,7 +153,6 @@ what would be helpful to you if you were consuming this component for the first 2. (For team-owned components) Check if your file path is already included in the `.storybook/main.ts` config -- if not, add it 3. Write the docs `*.mdx` page - - What is the component intended to be used for? - How to import and use it? What inputs and slots are available? - Are there other usage guidelines, such as pointing out similar components and when to use each? From fca0c8e7addddeac2991cb0be3a6f9832c251895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 19 Aug 2025 11:36:41 +0200 Subject: [PATCH 53/98] PM-22175: Improve launch of app + window positioning (#15658) * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Implement prepareInterfaceToProvideCredential * Fix launch of app + window pos * Wait for animation to complete and use proper position * Wait for animation to complete and use proper position * Added commentary * Remove console.log * Remove call to removed function --------- Co-authored-by: Jeffrey Holland Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com> --- .../CredentialProviderViewController.swift | 247 +++++++++++------- .../services/desktop-autofill.service.ts | 17 +- .../desktop-fido2-user-interface.service.ts | 24 +- .../src/platform/popup-modal-styles.ts | 1 + 4 files changed, 174 insertions(+), 115 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index a81b9c35440..b230e502db0 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -15,56 +15,75 @@ class CredentialProviderViewController: ASCredentialProviderViewController { @IBOutlet weak var statusLabel: NSTextField! @IBOutlet weak var logoImageView: NSImageView! - // There is something a bit strange about the initialization/deinitialization in this class. - // Sometimes deinit won't be called after a request has successfully finished, - // which would leave this class hanging in memory and the IPC connection open. - // - // 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. - let client: MacOsProviderClient = { - let logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") + private var client: MacOsProviderClient? + + // We made the the getclient method async + // This is so that we can check if the app is running, and launch it, without blocking the main thread + // Blocking the main thread caused MacOS layouting to 'fail' or at least be very delayed, which caused our getWindowPositioning code to sent 0,0. + // We also properly retry the IPC connection which sometimes would take some time to be up and running, depending on CPU load, phase of jupiters moon, etc. + private func getClient() async -> MacOsProviderClient { + if let client = self.client { + return client + } + 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 + logger.log("[autofill-extension] Bitwarden Desktop not running, attempting to launch") + + // Launch the app and wait for it to be ready 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") + await withCheckedContinuation { continuation in + 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 { + logger.log("[autofill-extension] Successfully launched Bitwarden Desktop") + } + continuation.resume() } - 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") + + // Retry connecting to the Bitwarden IPC with an increasing delay + let maxRetries = 20 + let delayMs = 500 + var newClient: MacOsProviderClient? + + for attempt in 1...maxRetries { + logger.log("[autofill-extension] Connection attempt \(attempt)") + + // Create a new client instance for each retry + newClient = MacOsProviderClient.connect() + try? await Task.sleep(nanoseconds: UInt64(100 * attempt + (delayMs * 1_000_000))) // Convert ms to nanoseconds + let connectionStatus = newClient!.getConnectionStatus() + + logger.log("[autofill-extension] Connection attempt \(attempt), status: \(connectionStatus == .connected ? "connected" : "disconnected")") + + if connectionStatus == .connected { + logger.log("[autofill-extension] Successfully connected to Bitwarden (attempt \(attempt))") + break + } else { + if attempt < maxRetries { + logger.log("[autofill-extension] Retrying connection") + } else { + logger.error("[autofill-extension] Failed to connect after \(maxRetries) attempts, final status: \(connectionStatus == .connected ? "connected" : "disconnected")") + } + } } - logger.log("[autofill-extension] Connecting to Bitwarden over IPC") - - return MacOsProviderClient.connect() - }() + self.client = newClient + return newClient! + } // Timer for checking connection status private var connectionMonitorTimer: Timer? @@ -86,6 +105,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController { // Check the connection status by calling into Rust private func checkConnectionStatus() { + // Only check if we have a client + guard let client = self.client else { + return + } + // Get the current connection status from Rust let currentStatus = client.getConnectionStatus() @@ -130,23 +154,52 @@ class CredentialProviderViewController: ASCredentialProviderViewController { connectionMonitorTimer = nil } - private func getWindowPosition() -> Position { - let frame = self.view.window?.frame ?? .zero - let screenHeight = NSScreen.main?.frame.height ?? 0 - let screenWidth = NSScreen.main?.frame.width ?? 0 - - // frame.width and frame.height is always 0. Estimating works OK for now. - let estimatedWidth:CGFloat = 400; - let estimatedHeight:CGFloat = 200; - // passkey modals are 600x600. - let modalHeight: CGFloat = 600; - let modalWidth: CGFloat = 600; - let centerX = round(frame.origin.x + estimatedWidth/2) - let centerY = round(screenHeight - (frame.origin.y + estimatedHeight/2)) - // Check if centerX or centerY are beyond either edge of the screen. If they are find the center of the screen, otherwise use the original value. - let positionX = centerX + modalWidth >= screenWidth || CGFloat(centerX) - modalWidth <= 0 ? Int32(screenWidth/2) : Int32(centerX) - let positionY = centerY + modalHeight >= screenHeight || CGFloat(centerY) - modalHeight <= 0 ? Int32(screenHeight/2) : Int32(centerY) - return Position(x: positionX, y: positionY) + private func getWindowPosition() async -> Position { + let screenHeight = NSScreen.main?.frame.height ?? 1440 + + logger.log("[autofill-extension] position: Getting window position") + + // To whomever is reading this. Sorry. But MacOS couldn't give us an accurate window positioning, possibly due to animations + // So I added some retry logic, as well as a fall back to the mouse position which is likely at the sort of the right place. + // In my testing we often succed after 4-7 attempts. + // Wait for window frame to stabilize (animation to complete) + var lastFrame: CGRect = .zero + var stableCount = 0 + let requiredStableChecks = 3 + let maxAttempts = 20 + var attempts = 0 + + while stableCount < requiredStableChecks && attempts < maxAttempts { + let currentFrame: CGRect = self.view.window?.frame ?? .zero + + if currentFrame.equalTo(lastFrame) && !currentFrame.equalTo(.zero) { + stableCount += 1 + } else { + stableCount = 0 + lastFrame = currentFrame + } + + try? await Task.sleep(nanoseconds: 16_666_666) // ~60fps (16.67ms) + attempts += 1 + } + + let finalWindowFrame = self.view.window?.frame ?? .zero + logger.log("[autofill-extension] position: Final window frame: \(NSStringFromRect(finalWindowFrame))") + + // Use stabilized window frame if available, otherwise fallback to mouse position + if finalWindowFrame.origin.x != 0 || finalWindowFrame.origin.y != 0 { + let centerX = Int32(round(finalWindowFrame.origin.x)) + let centerY = Int32(round(screenHeight - finalWindowFrame.origin.y)) + logger.log("[autofill-extension] position: Using window position: x=\(centerX), y=\(centerY)") + return Position(x: centerX, y: centerY) + } else { + // Fallback to mouse position + let mouseLocation = NSEvent.mouseLocation + let mouseX = Int32(round(mouseLocation.x)) + let mouseY = Int32(round(screenHeight - mouseLocation.y)) + logger.log("[autofill-extension] position: Using mouse position fallback: x=\(mouseX), y=\(mouseY)") + return Position(x: mouseX, y: mouseY) + } } override func viewDidLoad() { @@ -163,8 +216,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController { // Set the localized message statusLabel.stringValue = NSLocalizedString("autofillConfigurationMessage", comment: "Message shown when Bitwarden is enabled in system settings") - // Send the native status request - client.sendNativeStatus(key: "request-sync", value: "") + // Send the native status request asynchronously + Task { + let client = await getClient() + client.sendNativeStatus(key: "request-sync", value: "") + } // Complete the configuration after 2 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in @@ -237,18 +293,22 @@ class CredentialProviderViewController: ASCredentialProviderViewController { /* We're still using the old request type here, because we're sending the same data, we're expecting a single credential to be used */ - let req = PasskeyAssertionWithoutUserInterfaceRequest( - rpId: passkeyIdentity.relyingPartyIdentifier, - credentialId: passkeyIdentity.credentialID, - userName: passkeyIdentity.userName, - userHandle: passkeyIdentity.userHandle, - recordIdentifier: passkeyIdentity.recordIdentifier, - clientDataHash: request.clientDataHash, - userVerification: userVerification, - windowXy: self.getWindowPosition() - ) - - self.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + Task { + let windowPosition = await self.getWindowPosition() + let req = PasskeyAssertionWithoutUserInterfaceRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + credentialId: passkeyIdentity.credentialID, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + recordIdentifier: passkeyIdentity.recordIdentifier, + clientDataHash: request.clientDataHash, + userVerification: userVerification, + windowXy: windowPosition + ) + + let client = await getClient() + client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + } return } } @@ -328,19 +388,24 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } } - let req = PasskeyRegistrationRequest( - rpId: passkeyIdentity.relyingPartyIdentifier, - userName: passkeyIdentity.userName, - userHandle: passkeyIdentity.userHandle, - clientDataHash: request.clientDataHash, - userVerification: userVerification, - supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) }, - windowXy: self.getWindowPosition(), - excludedCredentials: excludedCredentialIds - ) logger.log("[autofill-extension] prepareInterface(passkey) calling preparePasskeyRegistration") - self.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + Task { + let windowPosition = await self.getWindowPosition() + let req = PasskeyRegistrationRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + clientDataHash: request.clientDataHash, + userVerification: userVerification, + supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) }, + windowXy: windowPosition, + excludedCredentials: excludedCredentialIds + ) + + let client = await getClient() + client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + } return } } @@ -393,18 +458,22 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } - let req = PasskeyAssertionRequest( - rpId: requestParameters.relyingPartyIdentifier, - clientDataHash: requestParameters.clientDataHash, - userVerification: userVerification, - allowedCredentials: requestParameters.allowedCredentials, - windowXy: self.getWindowPosition() - //extensionInput: requestParameters.extensionInput, - ) - let timeoutTimer = createTimer() - self.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + Task { + let windowPosition = await self.getWindowPosition() + let req = PasskeyAssertionRequest( + rpId: requestParameters.relyingPartyIdentifier, + clientDataHash: requestParameters.clientDataHash, + userVerification: userVerification, + allowedCredentials: requestParameters.allowedCredentials, + windowXy: windowPosition + //extensionInput: requestParameters.extensionInput, + ) + + let client = await getClient() + client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + } return } } diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index aec09f03021..a2418e9fdf8 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -212,7 +212,7 @@ export class DesktopAutofillService implements OnDestroy { try { const response = await this.fido2AuthenticatorService.makeCredential( this.convertRegistrationRequest(request), - { windowXy: request.windowXy }, + { windowXy: normalizePosition(request.windowXy) }, controller, ); @@ -278,7 +278,7 @@ export class DesktopAutofillService implements OnDestroy { const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request, true), - { windowXy: request.windowXy }, + { windowXy: normalizePosition(request.windowXy) }, controller, ); @@ -306,7 +306,7 @@ export class DesktopAutofillService implements OnDestroy { try { const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request), - { windowXy: request.windowXy }, + { windowXy: normalizePosition(request.windowXy) }, controller, ); @@ -440,3 +440,14 @@ export class DesktopAutofillService implements OnDestroy { this.destroy$.complete(); } } + +function normalizePosition(position: { x: number; y: number }): { x: number; y: number } { + // if macOS, the position we're sending is too far left and too far down. + // It's the left bottom corner of the parent window. + // so we need to add half of the estimated width of the nativeOS dialog to the x position + // and remove half of the estimated height of the nativeOS dialog to the y position. + return { + x: Math.round(position.x + 100), // add half of estimated width of the nativeOS dialog + y: Math.round(position.y), // remove half of estimated height of the nativeOS dialog + }; +} diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 48cec5a764e..4a4e4c6bf3b 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -268,7 +268,6 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi ): Promise { // Load the UI: await this.desktopSettingsService.setModalMode(true, showTrafficButtons, position); - await this.centerOffscreenPopup(); await this.accountService.setShowHeader(showTrafficButtons); await this.router.navigate([ route, @@ -347,7 +346,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi const status = await firstValueFrom(this.authService.activeAccountStatus$); if (status !== AuthenticationStatus.Unlocked) { - await this.showUi("/lock", undefined, true, true); + await this.showUi("/lock", this.windowObject.windowXy, true, true); let status2: AuthenticationStatus; try { @@ -380,25 +379,4 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi async close() { this.logService.warning("close"); } - - private async centerOffscreenPopup() { - if (!this.windowObject.windowXy) { - return; - } - - const popupWidth = 600; - const popupHeight = 600; - - const window = await firstValueFrom(this.desktopSettingsService.window$); - const { width, height } = window.displayBounds; - const { x, y } = this.windowObject.windowXy; - - if (x < popupWidth || x > width - popupWidth || y < popupHeight || y > height - popupHeight) { - const popupHeightOffset = 300; - const { width, height } = window.displayBounds; - const centeredX = width / 2; - const centeredY = (height - popupHeightOffset) / 2; - this.windowObject.windowXy = { x: centeredX, y: centeredY }; - } - } } diff --git a/apps/desktop/src/platform/popup-modal-styles.ts b/apps/desktop/src/platform/popup-modal-styles.ts index 1ef4b901c76..fdd5406b453 100644 --- a/apps/desktop/src/platform/popup-modal-styles.ts +++ b/apps/desktop/src/platform/popup-modal-styles.ts @@ -39,6 +39,7 @@ function positionWindow(window: BrowserWindow, position?: Position) { const centeredY = position.y - popupHeight / 2; window.setPosition(centeredX, centeredY); } else { + this.logService.warning("No position provided, centering window"); window.center(); } } From 69d24d29c2b6042cd89b8fd33b2565ffab018c13 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 20 Aug 2025 14:01:22 +0200 Subject: [PATCH 54/98] Update fido2-vault and fido2-service implementations --- .../credentials/fido2-vault.component.ts | 30 ++++++++++++------- .../desktop-fido2-user-interface.service.ts | 7 +---- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index fc582798043..c595f1bbfde 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -120,16 +120,26 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { return; } - this.cipherIds$.pipe(takeUntil(this.destroy$)).subscribe((cipherIds) => { - this.cipherService - .getAllDecryptedForIds(activeUserId, cipherIds || []) - .then((ciphers) => { - this.ciphersSubject.next(ciphers.filter((cipher) => !cipher.deletedDate)); - }) - .catch((error) => { - this.logService.error("Failed to load ciphers", error); - }); - }); + // Combine cipher list with optional cipher IDs filter + combineLatest([this.cipherService.cipherListViews$(activeUserId), this.cipherIds$ || of(null)]) + .pipe( + map(([ciphers, cipherIds]) => { + // Filter out deleted ciphers + const activeCiphers = ciphers.filter((cipher) => !cipher.deletedDate); + + // If specific IDs provided, filter by them + if (cipherIds?.length > 0) { + return activeCiphers.filter((cipher) => cipherIds.includes(cipher.id)); + } + + return activeCiphers; + }), + takeUntil(this.destroy$), + ) + .subscribe({ + next: (ciphers) => this.ciphersSubject.next(ciphers as CipherView[]), + error: (error: unknown) => this.logService.error("Failed to load ciphers", error), + }); } private async validateCipherAccess(cipher: CipherView): Promise { diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 4a4e4c6bf3b..8d4a2bdab68 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -157,12 +157,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } async getRpId(): Promise { - return lastValueFrom( - this.rpId.pipe( - filter((id) => id != null), - take(1), - ), - ); + return firstValueFrom(this.rpId.pipe(filter((id) => id != null))); } confirmChosenCipher(cipherId: string, userVerified: boolean = false): void { From 81690617c5d686c381e87986fdf1228b37bd73e0 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 20 Aug 2025 16:20:33 +0200 Subject: [PATCH 55/98] Use tailwind-alike classes for new styles --- apps/desktop/electron-builder.json | 4 +--- .../credentials/fido2-create.component.html | 6 +++--- .../fido2-excluded-ciphers.component.html | 4 ++-- .../credentials/fido2-vault.component.html | 6 +++--- .../modal/credentials/fido2-vault.component.ts | 11 ++++++++++- apps/desktop/src/scss/header.scss | 14 -------------- eslint.config.mjs | 8 +++++++- libs/components/src/tw-theme.css | 18 ++++++++++++++++++ 8 files changed, 44 insertions(+), 27 deletions(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 800cdd848a7..a6480b54370 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -67,7 +67,6 @@ ], "CFBundleDevelopmentRegion": "en" }, - "provisioningProfile": "bitwarden_desktop_developer_id.provisionprofile", "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", "extraFiles": [ { @@ -146,8 +145,7 @@ "extendInfo": { "LSMinimumSystemVersion": "12", "ElectronTeamID": "LTZ2PFU5D6" - }, - "provisioningProfile": "bitwarden_desktop_appstore.provisionprofile" + } }, "nsisWeb": { "oneClick": false, diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index d4241679604..0f9d27dd3c8 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -1,9 +1,9 @@
- +
@@ -16,7 +16,7 @@ type="button" bitIconButton="bwi-close" slot="end" - class="passkey-header-close tw-mb-4 tw-mr-2" + class="tw-app-region-no-drag tw-mb-4 tw-mr-2" (click)="closeModal()" > {{ "close" | i18n }} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html index 4ddf7cc4840..22a00c602f8 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html @@ -3,7 +3,7 @@ disableMargin class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300" > - +
@@ -16,7 +16,7 @@ type="button" bitIconButton="bwi-close" slot="end" - class="passkey-header-close tw-mb-4 tw-mr-2" + class="tw-app-region-no-drag tw-mb-4 tw-mr-2" (click)="closeModal()" > {{ "close" | i18n }} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html index 473b457c13f..e43320dcb65 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html @@ -1,9 +1,9 @@
- +
@@ -13,7 +13,7 @@ type="button" bitIconButton="bwi-close" slot="end" - class="passkey-header-close tw-mb-4 tw-mr-2" + class="tw-app-region-no-drag tw-mb-4 tw-mr-2" (click)="closeModal()" > {{ "close" | i18n }} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index c595f1bbfde..27dcab1b3c0 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -1,7 +1,16 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit, OnDestroy } from "@angular/core"; import { RouterModule, Router } from "@angular/router"; -import { firstValueFrom, map, BehaviorSubject, Observable, Subject, takeUntil } from "rxjs"; +import { + firstValueFrom, + map, + combineLatest, + of, + BehaviorSubject, + Observable, + Subject, + takeUntil, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; diff --git a/apps/desktop/src/scss/header.scss b/apps/desktop/src/scss/header.scss index c0c763ad429..cdd579a6554 100644 --- a/apps/desktop/src/scss/header.scss +++ b/apps/desktop/src/scss/header.scss @@ -232,17 +232,3 @@ font-size: $font-size-small; } } - -.passkey-header { - -webkit-app-region: drag; -} - -.passkey-header-close { - -webkit-app-region: no-drag; -} - -.passkey-header-sticky { - position: sticky; - top: 0; - z-index: 1; -} diff --git a/eslint.config.mjs b/eslint.config.mjs index c4018b7625e..05a7d4b2394 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -182,7 +182,12 @@ export default tseslint.config( { // uses negative lookahead to whitelist any class that doesn't start with "tw-" // in other words: classnames that start with tw- must be valid TailwindCSS classes - whitelist: ["(?!(tw)\\-).*"], + whitelist: [ + "(?!(tw)\\-).*", + "tw-app-region-drag", + "tw-app-region-no-drag", + "tw-app-region-header-sticky", + ], }, ], "tailwindcss/enforces-negative-arbitrary-values": "error", @@ -325,6 +330,7 @@ export default tseslint.config( "file-selector", "mfaType.*", "filter.*", // Temporary until filters are migrated + "tw-app-region*", // Custom utility for native passkey modals ], }, ], diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index bfdd976366b..ae8b36a24bc 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -172,6 +172,24 @@ text-align: unset; } + /** + * tw-app-region-drag and tw-app-region-no-drag are used for Electron window dragging behavior + * These will replace direct -webkit-app-region usage as part of the migration to Tailwind CSS + */ + .tw-app-region-drag { + -webkit-app-region: drag; + } + + .tw-app-region-no-drag { + -webkit-app-region: no-drag; + } + + .tw-app-region-header-sticky { + position: sticky; + top: 0; + z-index: 1; + } + /** * Bootstrap uses z-index: 1050 for modals, dialogs and drag-and-drop previews should appear above them. * When bootstrap is removed, test if these styles are still needed and that overlays display properly over other content. From c522fa2cb7f320225a8df5a1d60879240fcc787f Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 20 Aug 2025 16:52:41 +0200 Subject: [PATCH 56/98] Add label to biticons in passkey modals --- .../src/autofill/modal/credentials/fido2-create.component.html | 1 + .../modal/credentials/fido2-excluded-ciphers.component.html | 1 + .../src/autofill/modal/credentials/fido2-vault.component.html | 1 + 3 files changed, 3 insertions(+) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index 0f9d27dd3c8..fbd1d89eb01 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -18,6 +18,7 @@ slot="end" class="tw-app-region-no-drag tw-mb-4 tw-mr-2" (click)="closeModal()" + [label]="'close' | i18n" > {{ "close" | i18n }} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html index 22a00c602f8..f438f1035cc 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html @@ -18,6 +18,7 @@ slot="end" class="tw-app-region-no-drag tw-mb-4 tw-mr-2" (click)="closeModal()" + [label]="'close' | i18n" > {{ "close" | i18n }} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html index e43320dcb65..e2989db5740 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html @@ -15,6 +15,7 @@ slot="end" class="tw-app-region-no-drag tw-mb-4 tw-mr-2" (click)="closeModal()" + [label]="'close' | i18n" > {{ "close" | i18n }} From 5d55b8feb3b8bd40db4e9c128cfc0641a5f212ea Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 20 Aug 2025 18:20:28 +0200 Subject: [PATCH 57/98] Fix broken vault test --- .../credentials/fido2-vault.component.spec.ts | 19 +++++++++++++------ .../credentials/fido2-vault.component.ts | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts index 8085303e9c4..70ef4461f6a 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts @@ -50,7 +50,7 @@ describe("Fido2VaultComponent", () => { mockAccountService.activeAccount$ = of(mockActiveAccount as Account); mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession); mockSession.availableCipherIds$ = of(mockCipherIds); - mockCipherService.getAllDecryptedForIds.mockResolvedValue([]); + mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of([])); await TestBed.configureTestingModule({ imports: [Fido2VaultComponent], @@ -106,24 +106,31 @@ describe("Fido2VaultComponent", () => { describe("ngOnInit", () => { it("should initialize session and load ciphers successfully", async () => { - mockCipherService.getAllDecryptedForIds.mockResolvedValue(mockCiphers); + mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of(mockCiphers)); await component.ngOnInit(); expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled(); expect(component.session).toBe(mockSession); expect(component.cipherIds$).toBe(mockSession.availableCipherIds$); + expect(mockCipherService.cipherListViews$).toHaveBeenCalledWith(mockActiveAccount.id); }); - it("should handle error when no active session found", async () => { + it("should handle when no active session found", async () => { mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(null); - await expect(component.ngOnInit()).rejects.toThrow(); + await component.ngOnInit(); + + expect(component.session).toBeNull(); }); it("should filter out deleted ciphers", async () => { - mockCiphers[1].deletedDate = new Date(); - mockCipherService.getAllDecryptedForIds.mockResolvedValue(mockCiphers); + const ciphersWithDeleted = [ + ...mockCiphers.slice(0, 1), + { ...mockCiphers[1], deletedDate: new Date() }, + ...mockCiphers.slice(2), + ]; + mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of(ciphersWithDeleted)); await component.ngOnInit(); await new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index 27dcab1b3c0..a88a65b39fd 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -79,7 +79,7 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { async ngOnInit(): Promise { this.session = this.fido2UserInterfaceService.getCurrentSession(); - this.cipherIds$ = this.session.availableCipherIds$; + this.cipherIds$ = this.session?.availableCipherIds$; await this.loadCiphers(); } From 31b7a60bded8609e9ac72755b2eb69f24637bd13 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Thu, 21 Aug 2025 13:56:24 +0200 Subject: [PATCH 58/98] Revert to original `isDev` function --- apps/desktop/src/utils.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/utils.ts b/apps/desktop/src/utils.ts index 2e32a82df55..de5cd2daebc 100644 --- a/apps/desktop/src/utils.ts +++ b/apps/desktop/src/utils.ts @@ -20,12 +20,11 @@ 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; + // 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() { From 383c0ec25c7fcda7190868499079a7963f4147a5 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Thu, 21 Aug 2025 15:33:31 +0200 Subject: [PATCH 59/98] Add comment to lock component describing `disable-redirect` param --- libs/key-management-ui/src/lock/components/lock.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 764ff77e0bb..df1d0892e51 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -653,6 +653,9 @@ export class LockComponent implements OnInit, OnDestroy { } // determine success route based on client type + // The disable-redirect parameter allows callers to prevent automatic navigation after unlock, + // useful when the lock component is used in contexts where custom post-unlock behavior is needed + // such as passkey modals. if ( this.clientType != null && this.activatedRoute.snapshot.paramMap.get("disable-redirect") === null From 9d8cf9421554908e664382812108bcfe7f568d77 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Fri, 22 Aug 2025 16:00:02 +0200 Subject: [PATCH 60/98] Use tailwind classes instead of custom sticky header class --- .../autofill/modal/credentials/fido2-create.component.html | 2 +- .../autofill/modal/credentials/fido2-vault.component.html | 2 +- eslint.config.mjs | 7 +------ libs/components/src/tw-theme.css | 6 ------ 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index fbd1d89eb01..6986fb12cc7 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -1,7 +1,7 @@
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html index e2989db5740..f1e73c7280d 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html @@ -1,7 +1,7 @@
diff --git a/eslint.config.mjs b/eslint.config.mjs index 577eacb92ff..e3f9c9272bb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -184,12 +184,7 @@ export default tseslint.config( { // uses negative lookahead to whitelist any class that doesn't start with "tw-" // in other words: classnames that start with tw- must be valid TailwindCSS classes - whitelist: [ - "(?!(tw)\\-).*", - "tw-app-region-drag", - "tw-app-region-no-drag", - "tw-app-region-header-sticky", - ], + whitelist: ["(?!(tw)\\-).*", "tw-app-region-drag", "tw-app-region-no-drag"], }, ], "tailwindcss/enforces-negative-arbitrary-values": "error", diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index ae8b36a24bc..a4dbe8fe794 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -184,12 +184,6 @@ -webkit-app-region: no-drag; } - .tw-app-region-header-sticky { - position: sticky; - top: 0; - z-index: 1; - } - /** * Bootstrap uses z-index: 1050 for modals, dialogs and drag-and-drop previews should appear above them. * When bootstrap is removed, test if these styles are still needed and that overlays display properly over other content. From f556a49ba63b5a053766ad30b99e4e2bcf27ae6b Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Fri, 22 Aug 2025 16:03:17 +0200 Subject: [PATCH 61/98] Use standard `tw-z-10` for z-index --- .../src/autofill/modal/credentials/fido2-create.component.html | 2 +- .../src/autofill/modal/credentials/fido2-vault.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index 6986fb12cc7..022088feb8d 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -1,7 +1,7 @@
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html index f1e73c7280d..ed04993d09f 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html @@ -1,7 +1,7 @@
From 69ddbb1b51d2654505f79bbccb9aee817ebf2733 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Mon, 25 Aug 2025 13:29:01 +0200 Subject: [PATCH 62/98] Change log service levels --- apps/desktop/desktop_native/macos_provider/README.md | 2 +- apps/desktop/src/autofill/services/desktop-autofill.service.ts | 2 +- .../autofill/services/desktop-fido2-user-interface.service.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/README.md b/apps/desktop/desktop_native/macos_provider/README.md index b3f5d74170d..c042dd5fe6f 100644 --- a/apps/desktop/desktop_native/macos_provider/README.md +++ b/apps/desktop/desktop_native/macos_provider/README.md @@ -7,7 +7,7 @@ MacOS has native APIs (similar to iOS) to allow Credential Managers to provide c We’ve written a Swift-based native autofill-extension. It’s bundled in the app-bundle in PlugIns, similar to the safari-extension. -This swift extension currently communicates with our Electron app through IPC based on a unix socket. The IPC implementation is done in Rust and utilized through RustFFI + NAPI bindings. +This swift extension currently communicates with our Electron app through IPC based on a unix socket. The IPC implementation is done in Rust and utilized through UniFFI + NAPI bindings. Footnotes: diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index a2418e9fdf8..f4b60064a3b 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -155,7 +155,7 @@ export class DesktopAutofillService implements OnDestroy { })); } - this.logService.warning("Syncing autofill credentials", { + this.logService.info("Syncing autofill credentials", { fido2Credentials, passwordCredentials, }); diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 8d4a2bdab68..12182570670 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -313,7 +313,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } async updateCredential(cipher: CipherView): Promise { - this.logService.warning("updateCredential"); + this.logService.info("updateCredential"); await firstValueFrom( this.accountService.activeAccount$.pipe( map(async (a) => { From 87fc48b370f6c54ae954a9760001ede5f82a150e Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Thu, 28 Aug 2025 16:39:05 +0200 Subject: [PATCH 63/98] Mock svg icons for CI --- .../modal/credentials/fido2-create.component.spec.ts | 7 +++++++ .../credentials/fido2-excluded-ciphers.component.spec.ts | 7 +++++++ .../modal/credentials/fido2-vault.component.spec.ts | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts index 778215895ee..c5713275551 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts @@ -23,6 +23,13 @@ import { import { Fido2CreateComponent } from "./fido2-create.component"; +jest.mock("./bitwarden-shield.icon", () => ({ + BitwardenShield: {}, +})); +jest.mock("./fido2-passkey-exists-icon", () => ({ + Fido2PasskeyExistsIcon: {}, +})); + describe("Fido2CreateComponent", () => { let component: Fido2CreateComponent; let mockDesktopSettingsService: MockProxy; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts index 6a465136458..911d8a64934 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts @@ -14,6 +14,13 @@ import { import { Fido2ExcludedCiphersComponent } from "./fido2-excluded-ciphers.component"; +jest.mock("./bitwarden-shield.icon", () => ({ + BitwardenShield: {}, +})); +jest.mock("./fido2-passkey-exists-icon", () => ({ + Fido2PasskeyExistsIcon: {}, +})); + describe("Fido2ExcludedCiphersComponent", () => { let component: Fido2ExcludedCiphersComponent; let fixture: ComponentFixture; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts index 70ef4461f6a..ebd68facdf6 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts @@ -18,6 +18,10 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; +jest.mock("./bitwarden-shield.icon", () => ({ + BitwardenShield: {}, +})); + import { Fido2VaultComponent } from "./fido2-vault.component"; describe("Fido2VaultComponent", () => { From df6163873141340d0c34bcb943a93a5f705350e5 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Fri, 29 Aug 2025 12:18:34 +0200 Subject: [PATCH 64/98] Add back provisioning profiles --- apps/desktop/electron-builder.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index a6480b54370..800cdd848a7 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -67,6 +67,7 @@ ], "CFBundleDevelopmentRegion": "en" }, + "provisioningProfile": "bitwarden_desktop_developer_id.provisionprofile", "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", "extraFiles": [ { @@ -145,7 +146,8 @@ "extendInfo": { "LSMinimumSystemVersion": "12", "ElectronTeamID": "LTZ2PFU5D6" - } + }, + "provisioningProfile": "bitwarden_desktop_appstore.provisionprofile" }, "nsisWeb": { "oneClick": false, From a93d71523ac9f53edbfc8108a68d576c4bfe4482 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Tue, 2 Sep 2025 16:27:23 +0200 Subject: [PATCH 65/98] Remove `--break-system-packages` and simplify commands --- .github/workflows/build-desktop.yml | 25 ++++++++++++++++++++----- apps/desktop/package.json | 14 ++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 2c512c52503..38e263bc226 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -685,8 +685,13 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.13' + - name: Set up Node-gyp - run: python3 -m pip install setuptools --break-system-packages + run: python -m pip install setuptools - name: Print environment run: | @@ -912,8 +917,13 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.13' + - name: Set up Node-gyp - run: python3 -m pip install setuptools --break-system-packages + run: python -m pip install setuptools - name: Print environment run: | @@ -1109,7 +1119,7 @@ jobs: APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 CSC_FOR_PULL_REQUEST: true - run: npm run pack:mac:with-extension + run: npm run pack:mac - name: Upload .zip artifact uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 @@ -1171,8 +1181,13 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.13' + - name: Set up Node-gyp - run: python3 -m pip install setuptools --break-system-packages + run: python -m pip install setuptools - name: Print environment run: | @@ -1375,7 +1390,7 @@ jobs: APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 CSC_FOR_PULL_REQUEST: true - run: npm run pack:mac:mas:with-extension + run: npm run pack:mac:mas - name: Upload .pkg artifact uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6bfe71d59f4..d99af85adb9 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -40,25 +40,19 @@ "pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/", "pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .", - "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", - "pack:mac:with-extension": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never", + "pack:mac": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", - "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", - "pack:mac:mas:with-extension": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never", - "pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never", - "pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never", - "pack:local:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never -c.mac.provisioningProfile=\"\" -c.mas.provisioningProfile=\"\"", + "pack:mac:mas": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never", + "pack:mac:masdev": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never", + "pack:local:mac": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never -c.mac.provisioningProfile=\"\" -c.mas.provisioningProfile=\"\"", "pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"", "pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", "dist:dir": "npm run build && npm run pack:dir", "dist:lin": "npm run build && npm run pack:lin", "dist:lin:arm64": "npm run build && npm run pack:lin:arm64", "dist:mac": "npm run build && npm run pack:mac", - "dist:mac:with-extension": "npm run build && npm run pack:mac:with-extension", "dist:mac:mas": "npm run build && npm run pack:mac:mas", - "dist:mac:mas:with-extension": "npm run build && npm run pack:mac:mas:with-extension", "dist:mac:masdev": "npm run build && npm run pack:mac:masdev", - "dist:mac:masdev:with-extension": "npm run build && npm run pack:mac:masdev:with-extension", "dist:win": "npm run build && npm run pack:win", "dist:win:ci": "npm run build && npm run pack:win:ci", "publish:lin": "npm run build && npm run clean:dist && electron-builder --linux --x64 -p always", From a885f65d716cd555caed22d1aa81fae920588e52 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 3 Sep 2025 14:14:35 +0200 Subject: [PATCH 66/98] Revert `cipherId` param for `confirmNewCredential` --- .../autofill/services/desktop-fido2-user-interface.service.ts | 2 +- .../fido2/fido2-user-interface.service.abstraction.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 12182570670..8b64e448738 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -209,7 +209,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi userHandle, userVerification, rpId, - }: NewCredentialParams): Promise<{ cipherId?: string; userVerified: boolean }> { + }: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { this.logService.warning( "confirmNewCredential", credentialName, diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index b8be164c837..28b199da78f 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -95,7 +95,7 @@ export abstract class Fido2UserInterfaceSession { */ abstract confirmNewCredential( params: NewCredentialParams, - ): Promise<{ cipherId?: string; userVerified: boolean }>; + ): Promise<{ cipherId: string; userVerified: boolean }>; /** * Make sure that the vault is unlocked. From 6caefb55498930a257bf573df2f4e0bb513266dc Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 3 Sep 2025 15:27:47 +0200 Subject: [PATCH 67/98] Remove placeholder UI --- .../components/fido2placeholder.component.ts | 120 ------------------ 1 file changed, 120 deletions(-) delete mode 100644 apps/desktop/src/app/components/fido2placeholder.component.ts diff --git a/apps/desktop/src/app/components/fido2placeholder.component.ts b/apps/desktop/src/app/components/fido2placeholder.component.ts deleted file mode 100644 index 2982b380939..00000000000 --- a/apps/desktop/src/app/components/fido2placeholder.component.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { BehaviorSubject, Observable } from "rxjs"; - -import { - DesktopFido2UserInterfaceService, - DesktopFido2UserInterfaceSession, -} from "../../autofill/services/desktop-fido2-user-interface.service"; -import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; - -@Component({ - standalone: true, - imports: [CommonModule], - template: ` -
-

Select your passkey

- -
- -
- -
- - -
- `, -}) -export class Fido2PlaceholderComponent implements OnInit, OnDestroy { - session?: DesktopFido2UserInterfaceSession = null; - private cipherIdsSubject = new BehaviorSubject([]); - cipherIds$: Observable; - - constructor( - private readonly desktopSettingsService: DesktopSettingsService, - private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, - private readonly router: Router, - ) {} - - ngOnInit() { - this.session = this.fido2UserInterfaceService.getCurrentSession(); - this.cipherIds$ = this.session?.availableCipherIds$; - } - - async chooseCipher(cipherId: string) { - // For now: Set UV to true - this.session?.confirmChosenCipher(cipherId, true); - - await this.router.navigate(["/"]); - await this.desktopSettingsService.setModalMode(false); - } - - ngOnDestroy() { - this.cipherIdsSubject.complete(); // Clean up the BehaviorSubject - } - - async confirmPasskey() { - try { - // Retrieve the current UI session to control the flow - if (!this.session) { - // todo: handle error - throw new Error("No session found"); - } - - // If we want to we could submit information to the session in order to create the credential - // const cipher = await session.createCredential({ - // userHandle: "userHandle2", - // userName: "username2", - // credentialName: "zxsd2", - // rpId: "webauthn.io", - // userVerification: true, - // }); - - this.session.notifyConfirmCreateCredential(true); - - // Not sure this clean up should happen here or in session. - // 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.setModalMode(false); - } catch { - // TODO: Handle error appropriately - } - } - - async closeModal() { - await this.router.navigate(["/"]); - await this.desktopSettingsService.setModalMode(false); - - this.session.notifyConfirmCreateCredential(false); - // little bit hacky: - this.session.confirmChosenCipher(null); - } -} From f4b3aec3a9af33d3dbe395adb6950564a576a59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 4 Sep 2025 11:34:38 +0200 Subject: [PATCH 68/98] Small improvements to the readme --- apps/desktop/desktop_native/macos_provider/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/README.md b/apps/desktop/desktop_native/macos_provider/README.md index c042dd5fe6f..9c23418d318 100644 --- a/apps/desktop/desktop_native/macos_provider/README.md +++ b/apps/desktop/desktop_native/macos_provider/README.md @@ -1,9 +1,9 @@ # Explainer: Mac OS Native Passkey Provider -This explainer explains the changes introduced in https://github.com/bitwarden/clients/pull/13963, where we introduce the MacOS Native Passkey Provider. It gives the high level explanation of the architecture and some of the quirks and additional good to know context. +This document describes the changes introduced in https://github.com/bitwarden/clients/pull/13963, where we introduce the MacOS Native Passkey Provider. It gives the high level explanation of the architecture and some of the quirks and additional good to know context. ## The high level -MacOS has native APIs (similar to iOS) to allow Credential Managers to provide credentials to the MacOS autofill system (in this PR, we only provide passkeys). +MacOS has native APIs (similar to iOS) to allow Credential Managers to provide credentials to the MacOS autofill system (in the PR referenced above, we only provide passkeys). We’ve written a Swift-based native autofill-extension. It’s bundled in the app-bundle in PlugIns, similar to the safari-extension. @@ -16,8 +16,8 @@ Footnotes: Electron receives the messages and passes it to Angular (through the electron-renderer event system). -Our existing fido2 services in the renderer respond to events, displaying UI as necessary, and returns the signature back through the same mechanism, allowing people to authenticate with passkeys through the native system + UI. See -[Mac OS Native Passkey Workflows](https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1828356098/Mac+OS+Native+Passkey+Workflows) for demo videos. +Our existing fido2 services in the renderer respond to events, displaying UI as necessary, and returns the signature back through the same mechanism, allowing people to authenticate with passkeys through the native system + UI. See [Mac OS Native Passkey Workflows](https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1828356098/Mac+OS+Native+Passkey+Workflows) for demo videos. + ## Typescript + UI implementations From d05167b738cc906d00d9ba0f24c5a617cca82a10 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Thu, 4 Sep 2025 13:44:41 +0200 Subject: [PATCH 69/98] Remove optional userId and deprecated method --- libs/common/src/vault/services/cipher.service.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 3d29a5065aa..73290072568 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -598,7 +598,7 @@ export class CipherService implements CipherServiceAbstraction { async getAllDecryptedForUrl( url: string, - userId?: UserId, + userId: UserId, includeOtherTypes?: CipherType[], defaultMatch: UriMatchStrategySetting = null, ): Promise { @@ -619,10 +619,12 @@ export class CipherService implements CipherServiceAbstraction { } async getAllDecryptedForIds(userId: UserId, ids: string[]): Promise { - if (userId) { - const ciphers = await this.getAllDecrypted(userId); - return ciphers.filter((cipher) => ids.includes(cipher.id)); - } + return firstValueFrom( + this.cipherViews$(userId).pipe( + filter((ciphers) => ciphers != null), + map((ciphers) => ciphers.filter((cipher) => ids.includes(cipher.id))), + ), + ); } async filterCiphersForUrl( From 67f909e560eac7ea279544ad505fbfc172a46c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 4 Sep 2025 18:43:29 +0200 Subject: [PATCH 70/98] Autofill should own the macos_provider (#16271) * Autofill should own the macos_provider * Autofill should own the macos_provider --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4b956fd577a..1101bf9cab6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,6 +7,7 @@ ## Desktop native module ## apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev +apps/desktop/desktop_native/macos_provider @bitwarden/team-autofill-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev ## No ownership for Cargo.lock and Cargo.toml to allow dependency updates apps/desktop/desktop_native/Cargo.lock From bcfca5708907e826fe011b8e2807492c62c15ea1 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Fri, 5 Sep 2025 11:51:59 +0200 Subject: [PATCH 71/98] Remove unnecessary logs, no magic numbers, revert `cipherId?` --- .../desktop/src/platform/main/autofill/native-autofill.main.ts | 3 --- apps/desktop/src/platform/popup-modal-styles.ts | 3 +-- .../fido2/fido2-user-interface.service.abstraction.ts | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index 61b3bc5f48b..113ae37f2d0 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -37,9 +37,6 @@ export class NativeAutofillMain { if (this.listenerReady && this.windowMain.win?.webContents) { this.windowMain.win.webContents.send(channel, data); } else { - this.logService.info( - `Buffering message to ${channel} until server is ready. Call .listenerReady() to flush.`, - ); this.messageBuffer.push({ channel, data }); } } diff --git a/apps/desktop/src/platform/popup-modal-styles.ts b/apps/desktop/src/platform/popup-modal-styles.ts index fdd5406b453..e1d3bb566f5 100644 --- a/apps/desktop/src/platform/popup-modal-styles.ts +++ b/apps/desktop/src/platform/popup-modal-styles.ts @@ -39,13 +39,12 @@ function positionWindow(window: BrowserWindow, position?: Position) { const centeredY = position.y - popupHeight / 2; window.setPosition(centeredX, centeredY); } else { - this.logService.warning("No position provided, centering window"); window.center(); } } export function applyMainWindowStyles(window: BrowserWindow, existingWindowState: WindowState) { - window.setMinimumSize(600, 500); + window.setMinimumSize(popupWidth, popupHeight); // need to guard against null/undefined values diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 28b199da78f..b8be164c837 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -95,7 +95,7 @@ export abstract class Fido2UserInterfaceSession { */ abstract confirmNewCredential( params: NewCredentialParams, - ): Promise<{ cipherId: string; userVerified: boolean }>; + ): Promise<{ cipherId?: string; userVerified: boolean }>; /** * Make sure that the vault is unlocked. From 40ce33d6b7b74fb93c4dd3abdeaf00feeb3be2a1 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Tue, 9 Sep 2025 21:44:11 +0200 Subject: [PATCH 72/98] Fixes for broken build --- apps/desktop/desktop_native/napi/index.d.ts | 4 +- .../credentials/bitwarden-shield.icon.ts | 7 --- .../credentials/fido2-create.component.html | 2 +- .../credentials/fido2-create.component.ts | 7 +-- .../fido2-excluded-ciphers.component.html | 2 +- .../fido2-excluded-ciphers.component.ts | 7 +-- .../credentials/fido2-passkey-exists-icon.ts | 48 +++++++++---------- .../credentials/fido2-vault.component.ts | 5 +- 8 files changed, 34 insertions(+), 48 deletions(-) delete mode 100644 apps/desktop/src/autofill/modal/credentials/bitwarden-shield.icon.ts diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index a64fae4f804..3f34b7ba4f4 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -233,8 +233,8 @@ export declare namespace chromium_importer { login?: Login failure?: LoginImportFailure } - export function getInstalledBrowsers(): Promise> - export function getAvailableProfiles(browser: string): Promise> + export function getInstalledBrowsers(): Array + export function getAvailableProfiles(browser: string): Array export function importLogins(browser: string, profileId: string): Promise> } export declare namespace autotype { diff --git a/apps/desktop/src/autofill/modal/credentials/bitwarden-shield.icon.ts b/apps/desktop/src/autofill/modal/credentials/bitwarden-shield.icon.ts deleted file mode 100644 index 86e3a0bb1b2..00000000000 --- a/apps/desktop/src/autofill/modal/credentials/bitwarden-shield.icon.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { svgIcon } from "@bitwarden/components"; - -export const BitwardenShield = svgIcon` - - - -`; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html index 022088feb8d..67fc76aa317 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -28,7 +28,7 @@
- +
{{ "noMatchingLoginsForSite" | i18n }}
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index 14d31cca99e..9edea2764e0 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -4,6 +4,7 @@ import { RouterModule, Router } from "@angular/router"; import { combineLatest, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield, NoResults } from "@bitwarden/assets/svg"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; @@ -32,9 +33,6 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; -import { BitwardenShield } from "./bitwarden-shield.icon"; -import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; - @Component({ standalone: true, imports: [ @@ -56,8 +54,7 @@ import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; export class Fido2CreateComponent implements OnInit, OnDestroy { session?: DesktopFido2UserInterfaceSession = null; ciphers$: Observable; - readonly Icons = { BitwardenShield }; - protected fido2PasskeyExistsIcon = Fido2PasskeyExistsIcon; + readonly Icons = { BitwardenShield, NoResults }; private get DIALOG_MESSAGES() { return { diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html index f438f1035cc..792934deedc 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html @@ -30,7 +30,7 @@ class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-start tw-items-center tw-gap-2 tw-h-full tw-px-5" >
- +
{{ "passkeyAlreadyExists" | i18n }} {{ "applicationDoesNotSupportDuplicates" | i18n }} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts index 450e2dc186b..ddcf95d7d08 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit, OnDestroy } from "@angular/core"; import { RouterModule, Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield, NoResults } from "@bitwarden/assets/svg"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BadgeModule, @@ -22,9 +23,6 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; -import { BitwardenShield } from "./bitwarden-shield.icon"; -import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; - @Component({ standalone: true, imports: [ @@ -45,8 +43,7 @@ import { Fido2PasskeyExistsIcon } from "./fido2-passkey-exists-icon"; }) export class Fido2ExcludedCiphersComponent implements OnInit, OnDestroy { session?: DesktopFido2UserInterfaceSession = null; - readonly Icons = { BitwardenShield }; - protected fido2PasskeyExistsIcon = Fido2PasskeyExistsIcon; + readonly Icons = { BitwardenShield, NoResults }; constructor( private readonly desktopSettingsService: DesktopSettingsService, diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts b/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts index 66c3ebe0f5b..c32674cd1d0 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts @@ -1,24 +1,24 @@ -import { svgIcon } from "@bitwarden/components"; - -export const Fido2PasskeyExistsIcon = svgIcon` - - - - - - - - - - - - -`; +// import { svgIcon } from "@bitwarden/components"; +// +// export const Fido2PasskeyExistsIcon = svgIcon` +// +// +// +// +// +// +// +// +// +// +// +// +// `; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index a88a65b39fd..da1f1de57ba 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -13,6 +13,7 @@ import { } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield } from "@bitwarden/assets/svg"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -38,8 +39,6 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; -import { BitwardenShield } from "./bitwarden-shield.icon"; - @Component({ standalone: true, imports: [ @@ -138,7 +137,7 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { // If specific IDs provided, filter by them if (cipherIds?.length > 0) { - return activeCiphers.filter((cipher) => cipherIds.includes(cipher.id)); + return activeCiphers.filter((cipher) => cipherIds.includes(cipher.id as string)); } return activeCiphers; From 42057ec97fe0268cebdd92720c221726682a5dde Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 10 Sep 2025 07:33:54 +0200 Subject: [PATCH 73/98] Update test issues --- .../fido2-create.component.spec.ts | 7 ------ .../fido2-excluded-ciphers.component.spec.ts | 7 ------ .../credentials/fido2-passkey-exists-icon.ts | 24 ------------------- .../credentials/fido2-vault.component.spec.ts | 4 ---- 4 files changed, 42 deletions(-) delete mode 100644 apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts index c5713275551..778215895ee 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts @@ -23,13 +23,6 @@ import { import { Fido2CreateComponent } from "./fido2-create.component"; -jest.mock("./bitwarden-shield.icon", () => ({ - BitwardenShield: {}, -})); -jest.mock("./fido2-passkey-exists-icon", () => ({ - Fido2PasskeyExistsIcon: {}, -})); - describe("Fido2CreateComponent", () => { let component: Fido2CreateComponent; let mockDesktopSettingsService: MockProxy; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts index 911d8a64934..6a465136458 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts @@ -14,13 +14,6 @@ import { import { Fido2ExcludedCiphersComponent } from "./fido2-excluded-ciphers.component"; -jest.mock("./bitwarden-shield.icon", () => ({ - BitwardenShield: {}, -})); -jest.mock("./fido2-passkey-exists-icon", () => ({ - Fido2PasskeyExistsIcon: {}, -})); - describe("Fido2ExcludedCiphersComponent", () => { let component: Fido2ExcludedCiphersComponent; let fixture: ComponentFixture; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts b/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts deleted file mode 100644 index c32674cd1d0..00000000000 --- a/apps/desktop/src/autofill/modal/credentials/fido2-passkey-exists-icon.ts +++ /dev/null @@ -1,24 +0,0 @@ -// import { svgIcon } from "@bitwarden/components"; -// -// export const Fido2PasskeyExistsIcon = svgIcon` -// -// -// -// -// -// -// -// -// -// -// -// -// `; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts index ebd68facdf6..70ef4461f6a 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts @@ -18,10 +18,6 @@ import { DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; -jest.mock("./bitwarden-shield.icon", () => ({ - BitwardenShield: {}, -})); - import { Fido2VaultComponent } from "./fido2-vault.component"; describe("Fido2VaultComponent", () => { From 733ba46bfc79f375db9fb0e673e26d6890383e04 Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:51:10 -0600 Subject: [PATCH 74/98] [BEEEP] Use tracing in macOS provider --- apps/desktop/desktop_native/Cargo.lock | 15 ++++++++++++++- .../desktop_native/macos_provider/Cargo.toml | 3 ++- .../desktop_native/macos_provider/src/lib.rs | 18 ++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 9020e08362e..640474f03c6 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -1793,13 +1793,14 @@ version = "0.0.0" dependencies = [ "desktop_core", "futures", - "log", "oslog", "serde", "serde_json", "tokio", "tokio-util", "tracing", + "tracing-oslog", + "tracing-subscriber", "uniffi", ] @@ -3445,6 +3446,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-oslog" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76902d2a8d5f9f55a81155c08971734071968c90f2d9bfe645fe700579b2950" +dependencies = [ + "cc", + "cfg-if", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml index 9f042209b06..97a8b7d545a 100644 --- a/apps/desktop/desktop_native/macos_provider/Cargo.toml +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -16,12 +16,13 @@ bench = false [dependencies] desktop_core = { path = "../core" } futures = { workspace = true } -log = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["sync"] } tokio-util = { workspace = true } tracing = { workspace = true } +tracing-oslog = "0.3.0" +tracing-subscriber = { workspace = true } uniffi = { workspace = true, features = ["cli"] } [target.'cfg(target_os = "macos")'.dependencies] diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index ded133bcb54..4da1174131c 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -9,6 +9,11 @@ use std::{ use futures::FutureExt; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tracing::{error, info}; +use tracing_subscriber::{ + filter::{EnvFilter, LevelFilter}, + layer::SubscriberExt, + util::SubscriberInitExt, +}; uniffi::setup_scaffolding!(); @@ -65,8 +70,17 @@ impl MacOSProviderClient { #[allow(clippy::unwrap_used)] #[uniffi::constructor] pub fn connect() -> Self { - let _ = oslog::OsLogger::new("com.bitwarden.desktop.autofill-extension") - .level_filter(log::LevelFilter::Trace) + let filter = EnvFilter::builder() + // Everything logs at `TRACE` + .with_default_directive(LevelFilter::TRACE.into()) + .from_env_lossy(); + + tracing_subscriber::registry() + .with(filter) + .with(tracing_oslog::OsLogger::new( + "com.bitwarden.desktop.autofill-extension", + "default", + )) .init(); let (from_server_send, mut from_server_recv) = tokio::sync::mpsc::channel(32); From f496b8356d33e6c4f5e893f103869965ef020d2a Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Fri, 10 Oct 2025 16:33:29 +0200 Subject: [PATCH 75/98] Update comments and add null check for ciphers --- .../modal/credentials/fido2-create.component.ts | 1 + .../autofill/services/desktop-autofill.service.ts | 14 ++++++-------- .../desktop-fido2-user-interface.service.ts | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index 9edea2764e0..e3e0fba73fa 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -177,6 +177,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { const allCiphers = await this.cipherService.getAllDecrypted(activeUserId); return allCiphers.filter( (cipher) => + cipher != null && cipher.type == CipherType.Login && cipher.login?.matchesUri(rpid, equivalentDomains) && Fido2Utils.cipherHasNoOtherPasskeys(cipher, userHandle) && diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index f4b60064a3b..c2f81252145 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -89,7 +89,7 @@ export class DesktopAutofillService implements OnDestroy { ) .subscribe(); - // Listen for sign out to clear credentials? + // Listen for sign out to clear credentials this.authService.activeAccountStatus$ .pipe( filter((status) => status === AuthenticationStatus.LoggedOut), @@ -102,7 +102,7 @@ export class DesktopAutofillService implements OnDestroy { } async adHocSync(): Promise { - this.logService.info("Performing AdHoc sync"); + this.logService.debug("Performing AdHoc sync"); const account = await firstValueFrom(this.accountService.activeAccount$); const userId = account?.id; @@ -442,12 +442,10 @@ export class DesktopAutofillService implements OnDestroy { } function normalizePosition(position: { x: number; y: number }): { x: number; y: number } { - // if macOS, the position we're sending is too far left and too far down. - // It's the left bottom corner of the parent window. - // so we need to add half of the estimated width of the nativeOS dialog to the x position - // and remove half of the estimated height of the nativeOS dialog to the y position. + // If macOS, the position we're sending is too far left. + // Add 100 pixels to the x-coordinate to offset the native OS dialog positioning. return { - x: Math.round(position.x + 100), // add half of estimated width of the nativeOS dialog - y: Math.round(position.y), // remove half of estimated height of the nativeOS dialog + x: Math.round(position.x + 100), // Offset for native dialog positioning + y: Math.round(position.y), }; } diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 8b64e448738..2ac51781e9a 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -233,7 +233,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi await this.updateCredential(this.updatedCipher); return { cipherId: this.updatedCipher.id, userVerified: userVerification }; } else { - // Create the credential + // Create the cipher const createdCipher = await this.createCipher({ credentialName, userName, @@ -273,7 +273,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } /** - * Can be called by the UI to create a new credential with user input etc. + * Can be called by the UI to create a new cipher with user input etc. * @param param0 */ async createCipher({ credentialName, userName, rpId }: NewCredentialParams): Promise { From 8f067bdce4ff5475796cbc18ce85991288c56055 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Sun, 12 Oct 2025 14:57:22 +0200 Subject: [PATCH 76/98] Update status comments and readme --- apps/desktop/desktop_native/macos_provider/README.md | 4 +--- apps/desktop/desktop_native/macos_provider/src/lib.rs | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/README.md b/apps/desktop/desktop_native/macos_provider/README.md index 9c23418d318..77f666ff71a 100644 --- a/apps/desktop/desktop_native/macos_provider/README.md +++ b/apps/desktop/desktop_native/macos_provider/README.md @@ -18,7 +18,6 @@ Electron receives the messages and passes it to Angular (through the electron-re Our existing fido2 services in the renderer respond to events, displaying UI as necessary, and returns the signature back through the same mechanism, allowing people to authenticate with passkeys through the native system + UI. See [Mac OS Native Passkey Workflows](https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1828356098/Mac+OS+Native+Passkey+Workflows) for demo videos. - ## Typescript + UI implementations We utilize the same FIDO2 implementation and interface that is already present for our browser authentication. It was designed by @coroiu with multiple ‘ui environments' in mind. @@ -29,7 +28,6 @@ We’ve also implemented a couple FIDO2 UI components to handle registration/sig ## Modal mode -When modal mode is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position. +When (modal mode)[https://www.electronjs.org/docs/latest/api/browser-window#modal-windows] is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position. Some modal modes may hide the 'traffic buttons' (window controls) due to design requirements. - diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index b72e8cabb75..816f23e7170 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -50,6 +50,8 @@ trait Callback: Send + Sync { } #[derive(uniffi::Enum, Debug)] +/// Store the connection status between the macOS credential provider extension +/// and the desktop application's IPC server. pub enum ConnectionStatus { Connected, Disconnected, @@ -70,6 +72,8 @@ pub struct MacOSProviderClient { #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +/// Store native desktop status information to use for IPC communication +/// between the application and the macOS credential provider. pub struct NativeStatus { key: String, value: String, From 85f7351c402fb3edab42b9a043d0426319af5cb8 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Mon, 13 Oct 2025 13:14:57 +0200 Subject: [PATCH 77/98] Remove electron modal mode link --- apps/desktop/desktop_native/macos_provider/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/desktop_native/macos_provider/README.md b/apps/desktop/desktop_native/macos_provider/README.md index 77f666ff71a..3a00f64b733 100644 --- a/apps/desktop/desktop_native/macos_provider/README.md +++ b/apps/desktop/desktop_native/macos_provider/README.md @@ -28,6 +28,6 @@ We’ve also implemented a couple FIDO2 UI components to handle registration/sig ## Modal mode -When (modal mode)[https://www.electronjs.org/docs/latest/api/browser-window#modal-windows] is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position. +When modal mode is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position. Some modal modes may hide the 'traffic buttons' (window controls) due to design requirements. From 50449cf8ae44e371a34aac0d2561d65589510e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 13 Oct 2025 15:07:33 -0700 Subject: [PATCH 78/98] Clarify modal mode use --- apps/desktop/desktop_native/macos_provider/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/desktop/desktop_native/macos_provider/README.md b/apps/desktop/desktop_native/macos_provider/README.md index 3a00f64b733..1d4c1902465 100644 --- a/apps/desktop/desktop_native/macos_provider/README.md +++ b/apps/desktop/desktop_native/macos_provider/README.md @@ -30,4 +30,6 @@ We’ve also implemented a couple FIDO2 UI components to handle registration/sig When modal mode is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position. +We are not using electron modal windows, for a couple reason. It would require us to send data in yet another layer of IPC, but also because we'd need to bootstrap entire renderer/app instead of reusing the existing window. + Some modal modes may hide the 'traffic buttons' (window controls) due to design requirements. From 8d6dc009fe138dd6972da43f45ba7c5daa31bc2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 13 Oct 2025 15:21:33 -0700 Subject: [PATCH 79/98] Add comment about usernames --- .../desktop_native/objc/src/native/autofill/commands/sync.m | 6 +++--- .../CredentialProviderViewController.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m index 34b8bf6d6b0..2580c2c8a6f 100644 --- a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m @@ -24,7 +24,7 @@ void runSync(void* context, NSDictionary *params) { NSString *uri = credential[@"uri"]; NSString *username = credential[@"username"]; - // Skip credentials with null username + // Skip credentials with null username since MacOS crashes if we send credentials with empty usernames if ([username isKindOfClass:[NSNull class]] || username.length == 0) { NSLog(@"Skipping credential, username is empty: %@", credential); continue; @@ -42,8 +42,8 @@ void runSync(void* context, NSDictionary *params) { NSString *cipherId = credential[@"cipherId"]; NSString *rpId = credential[@"rpId"]; NSString *userName = credential[@"userName"]; - - // Skip credentials with null userName + + // Skip credentials with null username since MacOS crashes if we send credentials with empty usernames if ([userName isKindOfClass:[NSNull class]] || userName.length == 0) { NSLog(@"Skipping credential, username is empty: %@", credential); continue; diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index b230e502db0..25871ceb5b2 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -467,8 +467,8 @@ class CredentialProviderViewController: ASCredentialProviderViewController { clientDataHash: requestParameters.clientDataHash, userVerification: userVerification, allowedCredentials: requestParameters.allowedCredentials, - windowXy: windowPosition - //extensionInput: requestParameters.extensionInput, + windowXy: windowPosition, + extensionInput: requestParameters.extensionInput ) let client = await getClient() From b0f5665240a5dfc816a886e456b5fec05eb64a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 13 Oct 2025 15:22:28 -0700 Subject: [PATCH 80/98] Add comment that we don't support extensions yet --- .../autofill-extension/CredentialProviderViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 25871ceb5b2..72d6679f0e7 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -467,8 +467,8 @@ class CredentialProviderViewController: ASCredentialProviderViewController { clientDataHash: requestParameters.clientDataHash, userVerification: userVerification, allowedCredentials: requestParameters.allowedCredentials, - windowXy: windowPosition, - extensionInput: requestParameters.extensionInput + windowXy: windowPosition + //extensionInput: requestParameters.extensionInput, // We don't support extensions yet ) let client = await getClient() From c8d55d9c4bf23a4c7abecee1c91b02cc5f3b2f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 13 Oct 2025 15:25:08 -0700 Subject: [PATCH 81/98] Added comment about base64 format --- apps/desktop/desktop_native/objc/src/native/utils.m | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/desktop_native/objc/src/native/utils.m b/apps/desktop/desktop_native/objc/src/native/utils.m index 7ae84696312..8f9493a7afb 100644 --- a/apps/desktop/desktop_native/objc/src/native/utils.m +++ b/apps/desktop/desktop_native/objc/src/native/utils.m @@ -27,6 +27,7 @@ NSData *decodeBase64URL(NSString *base64URLString) { base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"]; // Add padding if needed + // Base 64 strings should be a multiple of 4 in length NSUInteger paddingLength = 4 - (base64String.length % 4); if (paddingLength < 4) { NSMutableString *paddedString = [NSMutableString stringWithString:base64String]; From f97e7cf6018276151d8ffabfd4a856c877e41ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 13 Oct 2025 15:32:20 -0700 Subject: [PATCH 82/98] Use NO_CALLBACK_INDICATOR --- apps/desktop/desktop_native/macos_provider/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 816f23e7170..9a882a8fab1 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -79,6 +79,9 @@ pub struct NativeStatus { value: String, } +// In our callback management, 0 is a reserved sequence number indicating that a message does not have a callback. +const NO_CALLBACK_INDICATOR: u32 = 0; + #[uniffi::export] impl MacOSProviderClient { // FIXME: Remove unwraps! They panic and terminate the whole application. @@ -94,7 +97,7 @@ impl MacOSProviderClient { let client = MacOSProviderClient { to_server_send, - response_callbacks_counter: AtomicU32::new(1), // 0 is reserved for no callback + response_callbacks_counter: AtomicU32::new(1), // Start at 1 since 0 is reserved for "no callback" scenarios response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)), }; @@ -246,7 +249,7 @@ impl MacOSProviderClient { let sequence_number = if let Some(cb) = callback { self.add_callback(cb) } else { - 0 // Special value indicating "no callback" + NO_CALLBACK_INDICATOR }; let message = serde_json::to_string(&SerializedMessage::Message { @@ -257,7 +260,7 @@ impl MacOSProviderClient { if let Err(e) = self.to_server_send.blocking_send(message) { // Make sure we remove the callback from the queue if we can't send the message - if sequence_number != 0 { + if sequence_number != NO_CALLBACK_INDICATOR { if let Some((cb, _)) = self .response_callbacks_queue .lock() From 6aaeb999809fe695b809f7976fccaf61f516b87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 13 Oct 2025 15:40:44 -0700 Subject: [PATCH 83/98] cb -> callback --- apps/desktop/desktop_native/macos_provider/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 9a882a8fab1..285022395d4 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -246,8 +246,8 @@ impl MacOSProviderClient { message: impl Serialize + DeserializeOwned, callback: Option>, ) { - let sequence_number = if let Some(cb) = callback { - self.add_callback(cb) + let sequence_number = if let Some(callback) = callback { + self.add_callback(callback) } else { NO_CALLBACK_INDICATOR }; @@ -261,13 +261,13 @@ impl MacOSProviderClient { if let Err(e) = self.to_server_send.blocking_send(message) { // Make sure we remove the callback from the queue if we can't send the message if sequence_number != NO_CALLBACK_INDICATOR { - if let Some((cb, _)) = self + if let Some((callback, _)) = self .response_callbacks_queue .lock() .unwrap() .remove(&sequence_number) { - cb.error(BitwardenError::Internal(format!( + callback.error(BitwardenError::Internal(format!( "Error sending message: {e}" ))); } From 9e7c798d0e827d6e8948bbab3ce679455170a922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 14 Oct 2025 00:55:29 +0200 Subject: [PATCH 84/98] Update apps/desktop/desktop_native/napi/src/lib.rs Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com> --- apps/desktop/desktop_native/napi/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index f5048827e79..a1ddf304113 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -754,8 +754,8 @@ pub mod autofill { .call(value, ThreadsafeFunctionCallMode::NonBlocking); continue; } - Err(e) => { - println!("[ERROR] Error deserializing native status: {e}"); + Err(error) => { + error!(%error, "Unable to deserialze native status."); } } From 81b3190c55a423269f4bd96ff2fbcb927e61d661 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Tue, 14 Oct 2025 14:41:31 +0200 Subject: [PATCH 85/98] Clean up Fido2Create subscriptions and update comments --- .../objc/src/native/autofill/commands/sync.m | 5 ++++- .../modal/credentials/fido2-create.component.ts | 14 ++++++++------ .../autofill/services/desktop-autofill.service.ts | 5 ++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m index 2580c2c8a6f..30c8c1d8d59 100644 --- a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m @@ -38,6 +38,9 @@ void runSync(void* context, NSDictionary *params) { [mappedCredentials addObject:passwordIdentity]; } else if (@available(macos 14, *)) { + // Fido2CredentialView uses `userName` (camelCase) while Login uses `username`. + // This is intentional. Fido2 fields are flattened from the FIDO2 spec's nested structure + // (user.name -> userName, rp.id -> rpId) to maintain a clear distinction between these fields. if ([type isEqualToString:@"fido2"]) { NSString *cipherId = credential[@"cipherId"]; NSString *rpId = credential[@"rpId"]; @@ -78,4 +81,4 @@ void runSync(void* context, NSDictionary *params) { _return(context, _success(@{@"added": @([mappedCredentials count])})); }]; -} \ No newline at end of file +} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index e3e0fba73fa..f25fcc40f35 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit, OnDestroy } from "@angular/core"; import { RouterModule, Router } from "@angular/router"; -import { combineLatest, map, Observable, switchMap } from "rxjs"; +import { combineLatest, map, Observable, Subject, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { BitwardenShield, NoResults } from "@bitwarden/assets/svg"; @@ -54,6 +54,7 @@ import { export class Fido2CreateComponent implements OnInit, OnDestroy { session?: DesktopFido2UserInterfaceSession = null; ciphers$: Observable; + private destroy$ = new Subject(); readonly Icons = { BitwardenShield, NoResults }; private get DIALOG_MESSAGES() { @@ -96,17 +97,18 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { async ngOnInit(): Promise { this.session = this.fido2UserInterfaceService.getCurrentSession(); - const rpid = await this.session?.getRpId(); - if (!this.session) { + if (this.session) { + const rpid = await this.session.getRpId(); + this.initializeCiphersObservable(rpid); + } else { await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey); - return; } - - this.initializeCiphersObservable(rpid); } async ngOnDestroy(): Promise { + this.destroy$.next(); + this.destroy$.complete(); await this.closeModal(); } diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index c2f81252145..29ecb83f3a1 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -43,6 +43,7 @@ import { NativeAutofillPasswordCredential, NativeAutofillSyncCommand, } from "../../platform/main/autofill/sync.command"; +import { isMac } from "../../utils"; import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"; @@ -444,8 +445,10 @@ export class DesktopAutofillService implements OnDestroy { function normalizePosition(position: { x: number; y: number }): { x: number; y: number } { // If macOS, the position we're sending is too far left. // Add 100 pixels to the x-coordinate to offset the native OS dialog positioning. + const xPostionOffset = isMac() ? 100 : 0; // Offset for native dialog positioning + return { - x: Math.round(position.x + 100), // Offset for native dialog positioning + x: Math.round(position.x + xPostionOffset), y: Math.round(position.y), }; } From 67aeeb3a3b28e9544c1c94dce1e2545d2d0346e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 14 Oct 2025 15:40:46 -0700 Subject: [PATCH 86/98] added comment to clarify silent exception --- .../desktop_native/objc/src/native/autofill/commands/sync.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m index 30c8c1d8d59..037a97c7590 100644 --- a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m @@ -68,6 +68,8 @@ void runSync(void* context, NSDictionary *params) { } } @catch (NSException *exception) { // Silently skip any credential that causes an exception + // to make sure we don't fail the entire sync + // There is likely some invalid data in the credential, and not something the user should/could be asked to correct. NSLog(@"ERROR: Exception processing credential: %@ - %@", exception.name, exception.reason); continue; } From a68fa10a477fc4ede72dbbf5a593e30728614d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 14 Oct 2025 15:44:20 -0700 Subject: [PATCH 87/98] Add comments --- .../CredentialProviderViewController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 72d6679f0e7..3d0603ec52a 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -104,8 +104,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } // Check the connection status by calling into Rust + // If the connection is has changed and is now disconnected, cancel the request private func checkConnectionStatus() { - // Only check if we have a client + // Only check connection status if the client has been initialized. + // Initialization is done asynchronously, so we might be called before it's ready + // In that case we just skip this check and wait for the next timer tick and re-check guard let client = self.client else { return } From e184d44eca14f795a08368f9742bfe8c9022858c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 14 Oct 2025 15:48:38 -0700 Subject: [PATCH 88/98] clean up unwrap() --- apps/desktop/desktop_native/macos_provider/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 285022395d4..bae74a1d6fa 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -224,7 +224,6 @@ enum SerializedMessage { } impl MacOSProviderClient { - // FIXME: Remove unwraps! They panic and terminate the whole application. #[allow(clippy::unwrap_used)] fn add_callback(&self, callback: Box) -> u32 { let sequence_number = self @@ -233,13 +232,12 @@ impl MacOSProviderClient { self.response_callbacks_queue .lock() - .unwrap() + .expect("response callbacks queue mutex should not be poisoned") .insert(sequence_number, (callback, Instant::now())); sequence_number } - // FIXME: Remove unwraps! They panic and terminate the whole application. #[allow(clippy::unwrap_used)] fn send_message( &self, @@ -264,7 +262,7 @@ impl MacOSProviderClient { if let Some((callback, _)) = self .response_callbacks_queue .lock() - .unwrap() + .expect("response callbacks queue mutex should not be poisoned") .remove(&sequence_number) { callback.error(BitwardenError::Internal(format!( From 6704dcb2e000202b52ca86937f36fdb460058276 Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Wed, 15 Oct 2025 08:28:22 -0600 Subject: [PATCH 89/98] set log level filter to INFO --- apps/desktop/desktop_native/macos_provider/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 4da1174131c..cca2a27ba99 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -71,8 +71,8 @@ impl MacOSProviderClient { #[uniffi::constructor] pub fn connect() -> Self { let filter = EnvFilter::builder() - // Everything logs at `TRACE` - .with_default_directive(LevelFilter::TRACE.into()) + // Everything logs at `INFO` + .with_default_directive(LevelFilter::INFO.into()) .from_env_lossy(); tracing_subscriber::registry() From e542a97529a170c9f1fbecce414856d02a476a19 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Fri, 17 Oct 2025 14:00:08 +0200 Subject: [PATCH 90/98] Address modal popup issue --- .../desktop/src/autofill/services/desktop-autofill.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 29ecb83f3a1..1d826bd5f2a 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -43,7 +43,6 @@ import { NativeAutofillPasswordCredential, NativeAutofillSyncCommand, } from "../../platform/main/autofill/sync.command"; -import { isMac } from "../../utils"; import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"; @@ -443,9 +442,8 @@ export class DesktopAutofillService implements OnDestroy { } function normalizePosition(position: { x: number; y: number }): { x: number; y: number } { - // If macOS, the position we're sending is too far left. // Add 100 pixels to the x-coordinate to offset the native OS dialog positioning. - const xPostionOffset = isMac() ? 100 : 0; // Offset for native dialog positioning + const xPostionOffset = 100; return { x: Math.round(position.x + xPostionOffset), From aa96ec78351a9d5a16146f8dfc4d1253c0c634b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 20 Oct 2025 20:47:14 +0200 Subject: [PATCH 91/98] plutil on Info.plist --- apps/desktop/macos/autofill-extension/Info.plist | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/Info.plist b/apps/desktop/macos/autofill-extension/Info.plist index a8ae0f021ad..7de0d4d152b 100644 --- a/apps/desktop/macos/autofill-extension/Info.plist +++ b/apps/desktop/macos/autofill-extension/Info.plist @@ -9,9 +9,9 @@ ASCredentialProviderExtensionCapabilities ProvidesPasskeys - + ShowsConfigurationUI - + NSExtensionPointIdentifier @@ -20,4 +20,4 @@ $(PRODUCT_MODULE_NAME).CredentialProviderViewController - \ No newline at end of file + From d754acd863674ed3481f3f86e246ea2f75772b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 20 Oct 2025 20:51:32 +0200 Subject: [PATCH 92/98] Adhere to style guides --- .../CredentialProviderViewController.swift | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 3d0603ec52a..3de9468c8ab 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -8,16 +8,20 @@ import AuthenticationServices import os - class CredentialProviderViewController: ASCredentialProviderViewController { let logger: Logger @IBOutlet weak var statusLabel: NSTextField! @IBOutlet weak var logoImageView: NSImageView! + // The IPC client to communicate with the Bitwarden desktop app private var client: MacOsProviderClient? - // We made the the getclient method async + // Timer for checking connection status + private var connectionMonitorTimer: Timer? + private var lastConnectionStatus: ConnectionStatus = .disconnected + + // We changed the getClient method to be async, here's why: // This is so that we can check if the app is running, and launch it, without blocking the main thread // Blocking the main thread caused MacOS layouting to 'fail' or at least be very delayed, which caused our getWindowPositioning code to sent 0,0. // We also properly retry the IPC connection which sometimes would take some time to be up and running, depending on CPU load, phase of jupiters moon, etc. @@ -83,11 +87,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { self.client = newClient return newClient! - } - - // Timer for checking connection status - private var connectionMonitorTimer: Timer? - private var lastConnectionStatus: ConnectionStatus = .disconnected + } // Setup the connection monitoring timer private func setupConnectionMonitoring() { @@ -321,8 +321,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong") self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request")) } - - + private func createTimer() -> DispatchWorkItem { // Create a timer for 600 second timeout let timeoutTimer = DispatchWorkItem { [weak self] in From 3abf0695f69d39dfccd34297cb42c7fa1370962a Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 22 Oct 2025 16:32:52 +0200 Subject: [PATCH 93/98] Fix broken lock ui component tests --- .../lock/components/lock.component.spec.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/libs/key-management-ui/src/lock/components/lock.component.spec.ts b/libs/key-management-ui/src/lock/components/lock.component.spec.ts index 5bac4a002a0..f123ca70848 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.spec.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.spec.ts @@ -473,6 +473,15 @@ describe("LockComponent", () => { component.clientType = clientType; mockLockComponentService.getPreviousUrl.mockReturnValue(null); + // Mock doContinue to include the navigation and required service calls + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); + await mockRouter.navigate([navigateUrl]); + }); + await component.successfulMasterPasswordUnlock({ userKey: mockUserKey, masterPassword }); assertUnlocked(); @@ -484,6 +493,17 @@ describe("LockComponent", () => { component.shouldClosePopout = true; mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.FirefoxExtension); + // Mock doContinue to include the popout close and required service calls + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded( + component.activeAccount!.id, + ); + mockLockComponentService.closeBrowserExtensionPopout(); + }); + await component.successfulMasterPasswordUnlock({ userKey: mockUserKey, masterPassword }); assertUnlocked(); @@ -618,6 +638,33 @@ describe("LockComponent", () => { ])( "should unlock and force set password change = %o when master password on login = %o and evaluated password against policy = %o and policy set during user verification by master password", async (forceSetPassword, masterPasswordPolicyOptions, evaluatedMasterPassword) => { + // Mock doContinue to handle password policy evaluation and required service calls + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + + if (masterPasswordPolicyOptions?.enforceOnLogin) { + const passwordStrengthResult = mockPasswordStrengthService.getPasswordStrength( + masterPassword, + component.activeAccount!.email, + ); + const evaluated = mockPolicyService.evaluateMasterPassword( + passwordStrengthResult.score, + masterPassword, + masterPasswordPolicyOptions, + ); + if (!evaluated) { + await mockMasterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.WeakMasterPassword, + userId, + ); + } + } + + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); + }); + mockUserVerificationService.verifyUserByMasterPassword.mockResolvedValue({ ...masterPasswordVerificationResponse, policyOptions: @@ -732,6 +779,15 @@ describe("LockComponent", () => { component.clientType = clientType; mockLockComponentService.getPreviousUrl.mockReturnValue(null); + // Mock doContinue to include the navigation and required service calls + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); + await mockRouter.navigate([navigateUrl]); + }); + await component.unlockViaMasterPassword(); assertUnlocked(); @@ -743,6 +799,17 @@ describe("LockComponent", () => { component.shouldClosePopout = true; mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.FirefoxExtension); + // Mock doContinue to include the popout close and required service calls + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded( + component.activeAccount!.id, + ); + mockLockComponentService.closeBrowserExtensionPopout(); + }); + await component.unlockViaMasterPassword(); assertUnlocked(); From c9597c212166fecd09e3af0441926697bc7bd139 Mon Sep 17 00:00:00 2001 From: Jeffrey Holland Date: Wed, 22 Oct 2025 16:35:19 +0200 Subject: [PATCH 94/98] Fix broken lock ui component tests --- .../src/lock/components/lock.component.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libs/key-management-ui/src/lock/components/lock.component.spec.ts b/libs/key-management-ui/src/lock/components/lock.component.spec.ts index f123ca70848..e381f378ec6 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.spec.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.spec.ts @@ -473,7 +473,6 @@ describe("LockComponent", () => { component.clientType = clientType; mockLockComponentService.getPreviousUrl.mockReturnValue(null); - // Mock doContinue to include the navigation and required service calls jest.spyOn(component as any, "doContinue").mockImplementation(async () => { await mockBiometricStateService.resetUserPromptCancelled(); mockMessagingService.send("unlocked"); @@ -493,7 +492,6 @@ describe("LockComponent", () => { component.shouldClosePopout = true; mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.FirefoxExtension); - // Mock doContinue to include the popout close and required service calls jest.spyOn(component as any, "doContinue").mockImplementation(async () => { await mockBiometricStateService.resetUserPromptCancelled(); mockMessagingService.send("unlocked"); @@ -638,7 +636,6 @@ describe("LockComponent", () => { ])( "should unlock and force set password change = %o when master password on login = %o and evaluated password against policy = %o and policy set during user verification by master password", async (forceSetPassword, masterPasswordPolicyOptions, evaluatedMasterPassword) => { - // Mock doContinue to handle password policy evaluation and required service calls jest.spyOn(component as any, "doContinue").mockImplementation(async () => { await mockBiometricStateService.resetUserPromptCancelled(); mockMessagingService.send("unlocked"); @@ -779,7 +776,6 @@ describe("LockComponent", () => { component.clientType = clientType; mockLockComponentService.getPreviousUrl.mockReturnValue(null); - // Mock doContinue to include the navigation and required service calls jest.spyOn(component as any, "doContinue").mockImplementation(async () => { await mockBiometricStateService.resetUserPromptCancelled(); mockMessagingService.send("unlocked"); @@ -799,7 +795,6 @@ describe("LockComponent", () => { component.shouldClosePopout = true; mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.FirefoxExtension); - // Mock doContinue to include the popout close and required service calls jest.spyOn(component as any, "doContinue").mockImplementation(async () => { await mockBiometricStateService.resetUserPromptCancelled(); mockMessagingService.send("unlocked"); From ae1eef6227a3978698403443ad961f415a0f8359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Thu, 23 Oct 2025 18:59:30 +0200 Subject: [PATCH 95/98] Added codeowners entry --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ae5d62a90c2..88435a3dc7b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,8 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-desktop-dev +apps/desktop/desktop_native/macos_provider @bitwarden/team-autofill-desktop-dev + ## No ownership for Cargo.lock and Cargo.toml to allow dependency updates apps/desktop/desktop_native/Cargo.lock apps/desktop/desktop_native/Cargo.toml From 21b13be3a553f0f1d19b736e9947a2fd460f6fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Fri, 24 Oct 2025 10:00:37 +0200 Subject: [PATCH 96/98] logservice.warning -> debug --- .../autofill/services/desktop-autofill.service.ts | 11 ++++------- .../desktop-fido2-user-interface.service.ts | 14 +++++++------- .../platform/main/autofill/native-autofill.main.ts | 6 +++--- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 1d826bd5f2a..0f3298cf64b 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -201,11 +201,8 @@ export class DesktopAutofillService implements OnDestroy { this.registrationRequest = request; - this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request); - this.logService.warning( - "listenPasskeyRegistration2", - this.convertRegistrationRequest(request), - ); + this.logService.debug("listenPasskeyRegistration", clientId, sequenceNumber, request); + this.logService.debug("listenPasskeyRegistration2", this.convertRegistrationRequest(request)); const controller = new AbortController(); @@ -233,7 +230,7 @@ export class DesktopAutofillService implements OnDestroy { return; } - this.logService.warning( + this.logService.debug( "listenPasskeyAssertion without user interface", clientId, sequenceNumber, @@ -300,7 +297,7 @@ export class DesktopAutofillService implements OnDestroy { return; } - this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request); + this.logService.debug("listenPasskeyAssertion", clientId, sequenceNumber, request); const controller = new AbortController(); try { diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 2ac51781e9a..19946ab590c 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -66,7 +66,7 @@ export class DesktopFido2UserInterfaceService nativeWindowObject: NativeWindowObject, abortController?: AbortController, ): Promise { - this.logService.warning("newSession", fallbackSupported, abortController, nativeWindowObject); + this.logService.debug("newSession", fallbackSupported, abortController, nativeWindowObject); const session = new DesktopFido2UserInterfaceSession( this.authService, this.cipherService, @@ -116,7 +116,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi assumeUserPresence, masterPasswordRepromptRequired, }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { - this.logService.warning("pickCredential desktop function", { + this.logService.debug("pickCredential desktop function", { cipherIds, userVerification, assumeUserPresence, @@ -210,7 +210,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi userVerification, rpId, }: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { - this.logService.warning( + this.logService.debug( "confirmNewCredential", credentialName, userName, @@ -327,7 +327,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } async informExcludedCredential(existingCipherIds: string[]): Promise { - this.logService.warning("informExcludedCredential", existingCipherIds); + this.logService.debug("informExcludedCredential", existingCipherIds); // make the cipherIds available to the UI. this.availableCipherIdsSubject.next(existingCipherIds); @@ -337,7 +337,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } async ensureUnlockedVault(): Promise { - this.logService.warning("ensureUnlockedVault"); + this.logService.debug("ensureUnlockedVault"); const status = await firstValueFrom(this.authService.activeAccountStatus$); if (status !== AuthenticationStatus.Unlocked) { @@ -368,10 +368,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } async informCredentialNotFound(): Promise { - this.logService.warning("informCredentialNotFound"); + this.logService.debug("informCredentialNotFound"); } async close() { - this.logService.warning("close"); + this.logService.debug("close"); } } diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index 113ae37f2d0..f5462f71e42 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -135,19 +135,19 @@ export class NativeAutofillMain { }); ipcMain.on("autofill.completePasskeyRegistration", (event, data) => { - this.logService.warning("autofill.completePasskeyRegistration", data); + this.logService.debug("autofill.completePasskeyRegistration", data); const { clientId, sequenceNumber, response } = data; this.ipcServer.completeRegistration(clientId, sequenceNumber, response); }); ipcMain.on("autofill.completePasskeyAssertion", (event, data) => { - this.logService.warning("autofill.completePasskeyAssertion", data); + this.logService.debug("autofill.completePasskeyAssertion", data); const { clientId, sequenceNumber, response } = data; this.ipcServer.completeAssertion(clientId, sequenceNumber, response); }); ipcMain.on("autofill.completeError", (event, data) => { - this.logService.warning("autofill.completeError", data); + this.logService.debug("autofill.completeError", data); const { clientId, sequenceNumber, error } = data; this.ipcServer.completeError(clientId, sequenceNumber, String(error)); }); From 34bff4a694d60f5aa44edb875a595720c34c2754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Fri, 24 Oct 2025 10:01:03 +0200 Subject: [PATCH 97/98] Uint8Array -> ArrayBuffer --- .../desktop/src/autofill/services/desktop-autofill.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 0f3298cf64b..1ccc64db251 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -393,13 +393,13 @@ export class DesktopAutofillService implements OnDestroy { if ("credentialId" in request) { allowedCredentials = [ { - id: new Uint8Array(request.credentialId), + id: new Uint8Array(request.credentialId).buffer, type: "public-key" as const, }, ]; } else { allowedCredentials = request.allowedCredentials.map((credentialId) => ({ - id: new Uint8Array(credentialId), + id: new Uint8Array(credentialId).buffer, type: "public-key" as const, })); } From 39845552f3d98203ec0f3cb0a8b987ee155c50fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 28 Oct 2025 17:24:27 +0100 Subject: [PATCH 98/98] Remove autofill entitlement --- .../autofill_extension.entitlements | 8 +++----- apps/desktop/resources/entitlements.mac.plist | 2 -- .../resources/entitlements.mas.inherit.plist | 4 +--- apps/desktop/resources/entitlements.mas.plist | 14 +++++--------- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements index 86c7195768e..d5c7b8a2cc8 100644 --- a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements +++ b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements @@ -2,11 +2,9 @@ - com.apple.developer.authentication-services.autofill-credential-provider - - com.apple.security.app-sandbox - - com.apple.security.application-groups + com.apple.security.app-sandbox + + com.apple.security.application-groups LTZ2PFU5D6.com.bitwarden.desktop diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist index fe49256d71c..7763b84624d 100644 --- a/apps/desktop/resources/entitlements.mac.plist +++ b/apps/desktop/resources/entitlements.mac.plist @@ -6,8 +6,6 @@ LTZ2PFU5D6.com.bitwarden.desktop com.apple.developer.team-identifier LTZ2PFU5D6 - com.apple.developer.authentication-services.autofill-credential-provider - com.apple.security.cs.allow-jit diff --git a/apps/desktop/resources/entitlements.mas.inherit.plist b/apps/desktop/resources/entitlements.mas.inherit.plist index e9a28f8f327..7194d9409fc 100644 --- a/apps/desktop/resources/entitlements.mas.inherit.plist +++ b/apps/desktop/resources/entitlements.mas.inherit.plist @@ -4,11 +4,9 @@ com.apple.security.app-sandbox - com.apple.security.inherit - com.apple.security.cs.allow-jit - com.apple.developer.authentication-services.autofill-credential-provider + com.apple.security.inherit diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index d915034b100..6d6d37d643e 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -6,21 +6,19 @@ LTZ2PFU5D6.com.bitwarden.desktop com.apple.developer.team-identifier LTZ2PFU5D6 - com.apple.developer.authentication-services.autofill-credential-provider - com.apple.security.app-sandbox com.apple.security.application-groups LTZ2PFU5D6.com.bitwarden.desktop - com.apple.security.network.client - - com.apple.security.files.user-selected.read-write + com.apple.security.cs.allow-jit com.apple.security.device.usb - com.apple.developer.authentication-services.autofill-credential-provider + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client com.apple.security.temporary-exception.files.home-relative-path.read-write @@ -34,10 +32,8 @@ /Library/Application Support/Microsoft Edge Beta/NativeMessagingHosts/ /Library/Application Support/Microsoft Edge Dev/NativeMessagingHosts/ /Library/Application Support/Microsoft Edge Canary/NativeMessagingHosts/ - /Library/Application Support/Vivaldi/NativeMessagingHosts/ + /Library/Application Support/Vivaldi/NativeMessagingHosts/ /Library/Application Support/Zen/NativeMessagingHosts/ - com.apple.security.cs.allow-jit -