mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
* Passkey stuff Co-authored-by: Anders Åberg <github@andersaberg.com> * 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 * pick credential, draft * Remove logger * a whole lot of wiring * not working * Improved wiring * Cancel after 90s * Introduced observable * 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 <acoroiu@bitwarden.com> * Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Update apps/desktop/src/platform/services/desktop-settings.service.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * adress position feedback * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * 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 <acoroiu@bitwarden.com> * 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 <acoroiu@bitwarden.com> * 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 --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Colton Hurst <colton@coltonhurst.com> Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com> Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
import { Router } from "@angular/router";
|
|
import {
|
|
lastValueFrom,
|
|
firstValueFrom,
|
|
map,
|
|
Subject,
|
|
filter,
|
|
take,
|
|
BehaviorSubject,
|
|
timeout,
|
|
} 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 {
|
|
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
|
|
Fido2UserInterfaceSession,
|
|
NewCredentialParams,
|
|
PickCredentialParams,
|
|
} from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { CipherRepromptType, CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
|
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
|
|
|
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
|
|
|
/**
|
|
* This type is used to pass the window position from the native UI
|
|
*/
|
|
export type NativeWindowObject = {
|
|
/**
|
|
* The position of the window, first entry is the x position, second is the y position
|
|
*/
|
|
windowXy?: { x: number; y: number };
|
|
};
|
|
|
|
export class DesktopFido2UserInterfaceService
|
|
implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject>
|
|
{
|
|
constructor(
|
|
private authService: AuthService,
|
|
private cipherService: CipherService,
|
|
private accountService: AccountService,
|
|
private logService: LogService,
|
|
private messagingService: MessagingService,
|
|
private router: Router,
|
|
private desktopSettingsService: DesktopSettingsService,
|
|
) {}
|
|
private currentSession: any;
|
|
|
|
getCurrentSession(): DesktopFido2UserInterfaceSession | undefined {
|
|
return this.currentSession;
|
|
}
|
|
|
|
async newSession(
|
|
fallbackSupported: boolean,
|
|
nativeWindowObject: NativeWindowObject,
|
|
abortController?: AbortController,
|
|
): Promise<DesktopFido2UserInterfaceSession> {
|
|
this.logService.warning("newSession", fallbackSupported, abortController, nativeWindowObject);
|
|
const session = new DesktopFido2UserInterfaceSession(
|
|
this.authService,
|
|
this.cipherService,
|
|
this.accountService,
|
|
this.logService,
|
|
this.router,
|
|
this.desktopSettingsService,
|
|
nativeWindowObject,
|
|
);
|
|
|
|
this.currentSession = session;
|
|
return session;
|
|
}
|
|
}
|
|
|
|
export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSession {
|
|
constructor(
|
|
private authService: AuthService,
|
|
private cipherService: CipherService,
|
|
private accountService: AccountService,
|
|
private logService: LogService,
|
|
private router: Router,
|
|
private desktopSettingsService: DesktopSettingsService,
|
|
private windowObject: NativeWindowObject,
|
|
) {}
|
|
|
|
private confirmCredentialSubject = new Subject<boolean>();
|
|
private createdCipher: Cipher;
|
|
|
|
private availableCipherIdsSubject = new BehaviorSubject<string[]>(null);
|
|
/**
|
|
* Observable that emits available cipher IDs once they're confirmed by the UI
|
|
*/
|
|
availableCipherIds$ = this.availableCipherIdsSubject.pipe(
|
|
filter((ids) => ids != null),
|
|
take(1),
|
|
);
|
|
|
|
private chosenCipherSubject = new Subject<{ cipherId: string; userVerified: boolean }>();
|
|
|
|
// Method implementation
|
|
async pickCredential({
|
|
cipherIds,
|
|
userVerification,
|
|
assumeUserPresence,
|
|
masterPasswordRepromptRequired,
|
|
}: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
|
this.logService.warning("pickCredential desktop function", {
|
|
cipherIds,
|
|
userVerification,
|
|
assumeUserPresence,
|
|
masterPasswordRepromptRequired,
|
|
});
|
|
|
|
try {
|
|
// Check if we can return the credential without user interaction
|
|
if (assumeUserPresence && cipherIds.length === 1 && !masterPasswordRepromptRequired) {
|
|
this.logService.debug(
|
|
"shortcut - Assuming user presence and returning cipherId",
|
|
cipherIds[0],
|
|
);
|
|
return { cipherId: cipherIds[0], userVerified: userVerification };
|
|
}
|
|
|
|
this.logService.debug("Could not shortcut, showing UI");
|
|
|
|
// make the cipherIds available to the UI.
|
|
this.availableCipherIdsSubject.next(cipherIds);
|
|
|
|
await this.showUi("/passkeys", this.windowObject.windowXy);
|
|
|
|
const chosenCipherResponse = await this.waitForUiChosenCipher();
|
|
|
|
this.logService.debug("Received chosen cipher", chosenCipherResponse);
|
|
|
|
return {
|
|
cipherId: chosenCipherResponse.cipherId,
|
|
userVerified: chosenCipherResponse.userVerified,
|
|
};
|
|
} finally {
|
|
// Make sure to clean up so the app is never stuck in modal mode?
|
|
await this.desktopSettingsService.setModalMode(false);
|
|
}
|
|
}
|
|
|
|
confirmChosenCipher(cipherId: string, userVerified: boolean = false): void {
|
|
this.chosenCipherSubject.next({ cipherId, userVerified });
|
|
this.chosenCipherSubject.complete();
|
|
}
|
|
|
|
private async waitForUiChosenCipher(
|
|
timeoutMs: number = 60000,
|
|
): Promise<{ cipherId: string; userVerified: boolean } | undefined> {
|
|
try {
|
|
return await lastValueFrom(this.chosenCipherSubject.pipe(timeout(timeoutMs)));
|
|
} catch {
|
|
// If we hit a timeout, return undefined instead of throwing
|
|
this.logService.warning("Timeout: User did not select a cipher within the allowed time", {
|
|
timeoutMs,
|
|
});
|
|
return { cipherId: undefined, userVerified: false };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifies the Fido2UserInterfaceSession that the UI operations has completed and it can return to the OS.
|
|
*/
|
|
notifyConfirmNewCredential(confirmed: boolean): void {
|
|
this.confirmCredentialSubject.next(confirmed);
|
|
this.confirmCredentialSubject.complete();
|
|
}
|
|
|
|
/**
|
|
* Returns once the UI has confirmed and completed the operation
|
|
* @returns
|
|
*/
|
|
private async waitForUiNewCredentialConfirmation(): Promise<boolean> {
|
|
return lastValueFrom(this.confirmCredentialSubject);
|
|
}
|
|
|
|
/**
|
|
* This is called by the OS. It loads the UI and waits for the user to confirm the new credential. Once the UI has confirmed, it returns to the the OS.
|
|
* @param param0
|
|
* @returns
|
|
*/
|
|
async confirmNewCredential({
|
|
credentialName,
|
|
userName,
|
|
userVerification,
|
|
rpId,
|
|
}: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
|
this.logService.warning(
|
|
"confirmNewCredential",
|
|
credentialName,
|
|
userName,
|
|
userVerification,
|
|
rpId,
|
|
);
|
|
|
|
try {
|
|
await this.showUi("/passkeys", 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 };
|
|
} finally {
|
|
// Make sure to clean up so the app is never stuck in modal mode?
|
|
await this.desktopSettingsService.setModalMode(false);
|
|
}
|
|
}
|
|
|
|
private async showUi(route: string, position?: { x: number; y: number }): Promise<void> {
|
|
// Load the UI:
|
|
await this.desktopSettingsService.setModalMode(true, position);
|
|
await this.router.navigate(["/passkeys"]);
|
|
}
|
|
|
|
/**
|
|
* Can be called by the UI to create a new credential with user input etc.
|
|
* @param param0
|
|
*/
|
|
async createCredential({ credentialName, userName, rpId }: NewCredentialParams): Promise<Cipher> {
|
|
// Store the passkey on a new cipher to avoid replacing something important
|
|
const cipher = new CipherView();
|
|
cipher.name = credentialName;
|
|
|
|
cipher.type = CipherType.Login;
|
|
cipher.login = new LoginView();
|
|
cipher.login.username = userName;
|
|
cipher.login.uris = [new LoginUriView()];
|
|
cipher.login.uris[0].uri = "https://" + rpId;
|
|
cipher.card = new CardView();
|
|
cipher.identity = new IdentityView();
|
|
cipher.secureNote = new SecureNoteView();
|
|
cipher.secureNote.type = SecureNoteType.Generic;
|
|
cipher.reprompt = CipherRepromptType.None;
|
|
|
|
const activeUserId = await firstValueFrom(
|
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
|
);
|
|
|
|
const encCipher = await this.cipherService.encrypt(cipher, activeUserId);
|
|
const createdCipher = await this.cipherService.createWithServer(encCipher);
|
|
|
|
this.createdCipher = createdCipher;
|
|
|
|
return createdCipher;
|
|
}
|
|
|
|
async informExcludedCredential(existingCipherIds: string[]): Promise<void> {
|
|
this.logService.warning("informExcludedCredential", existingCipherIds);
|
|
}
|
|
|
|
async ensureUnlockedVault(): Promise<void> {
|
|
this.logService.warning("ensureUnlockedVault");
|
|
|
|
const status = await firstValueFrom(this.authService.activeAccountStatus$);
|
|
if (status !== AuthenticationStatus.Unlocked) {
|
|
throw new Error("Vault is not unlocked");
|
|
}
|
|
}
|
|
|
|
async informCredentialNotFound(): Promise<void> {
|
|
this.logService.warning("informCredentialNotFound");
|
|
}
|
|
|
|
async close() {
|
|
this.logService.warning("close");
|
|
}
|
|
}
|