diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a69517a1f1f..659b2a5dd5f 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -528,7 +528,11 @@ export default class MainBackground { ); this.popupUtilsService = new PopupUtilsService(this.isPrivateMode); - this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.popupUtilsService); + this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService( + this.popupUtilsService, + this.authService, + this.syncService + ); this.fido2AuthenticatorService = new Fido2AuthenticatorService( this.cipherService, this.fido2UserInterfaceService, diff --git a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts index 4f630f84e57..7f9651ea918 100644 --- a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts @@ -4,12 +4,18 @@ import { filter, firstValueFrom, fromEvent, + merge, Observable, Subject, + switchMap, take, takeUntil, + throwError, + fromEventPattern, } from "rxjs"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserRequestedFallbackAbortReason } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; import { @@ -18,6 +24,7 @@ import { NewCredentialParams, PickCredentialParams, } from "@bitwarden/common/vault/abstractions/fido2/fido2-user-interface.service.abstraction"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { BrowserApi } from "../../platform/browser/browser-api"; import { Popout, PopupUtilsService } from "../../popup/services/popup-utils.service"; @@ -105,7 +112,11 @@ export type BrowserFido2Message = { sessionId: string } & ( ); export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { - constructor(private popupUtilsService: PopupUtilsService) {} + constructor( + private popupUtilsService: PopupUtilsService, + private authService: AuthService, + private syncService: SyncService + ) {} async newSession( fallbackSupported: boolean, @@ -113,6 +124,8 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi ): Promise { return await BrowserFido2UserInterfaceSession.create( this.popupUtilsService, + this.authService, + this.syncService, fallbackSupported, abortController ); @@ -122,11 +135,15 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession { static async create( popupUtilsService: PopupUtilsService, + authService: AuthService, + syncService: SyncService, fallbackSupported: boolean, abortController?: AbortController ): Promise { return new BrowserFido2UserInterfaceSession( popupUtilsService, + authService, + syncService, fallbackSupported, abortController ); @@ -140,12 +157,16 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi private messages$ = (BrowserApi.messageListener$() as Observable).pipe( filter((msg) => msg.sessionId === this.sessionId) ); + private windowClosed$: Observable; + private tabClosed$: Observable; private connected$ = new BehaviorSubject(false); private destroy$ = new Subject(); private popout?: Popout; private constructor( private readonly popupUtilsService: PopupUtilsService, + private readonly authService: AuthService, + private readonly syncService: SyncService, private readonly fallbackSupported: boolean, readonly abortController = new AbortController(), readonly sessionId = Utils.newGuid() @@ -181,12 +202,20 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi .subscribe((msg) => { if (msg.type === "AbortResponse") { this.close(); - this.abortController.abort( - msg.fallbackRequested ? UserRequestedFallbackAbortReason : undefined - ); + this.abort(msg.fallbackRequested); } }); + this.windowClosed$ = fromEventPattern( + (handler: any) => chrome.windows.onRemoved.addListener(handler), + (handler: any) => chrome.windows.onRemoved.removeListener(handler) + ); + + this.tabClosed$ = fromEventPattern( + (handler: any) => chrome.windows.onRemoved.addListener(handler), + (handler: any) => chrome.windows.onRemoved.removeListener(handler) + ); + BrowserFido2UserInterfaceSession.sendMessage({ type: "NewSessionCreatedRequest", sessionId, @@ -269,6 +298,15 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi await this.receive("AbortResponse"); } + async ensureUnlockedVault(): Promise { + await this.connect(); + // await this.syncService.fullSync(false); + } + + async fullSync(): Promise { + await this.syncService.fullSync(false); + } + async informCredentialNotFound(): Promise { const data: BrowserFido2Message = { type: "InformCredentialNotFoundRequest", @@ -280,19 +318,6 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi await this.receive("AbortResponse"); } - async login(userVerification: boolean): Promise<{ userVerified: boolean }> { - const data: BrowserFido2Message = { - type: "LogInRequest", - userVerification, - sessionId: this.sessionId, - }; - - await this.send(data); - const response = await this.receive("LogInResponse"); - - return { userVerified: response.userVerified }; - } - async close() { this.popupUtilsService.closePopOut(this.popout); this.closed = true; @@ -300,6 +325,10 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi this.destroy$.complete(); } + async abort(fallback = false) { + this.abortController.abort(fallback ? UserRequestedFallbackAbortReason : undefined); + } + private async send(msg: BrowserFido2Message): Promise { if (!this.connected$.value) { await this.connect(); @@ -331,16 +360,70 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi throw new Error("Cannot re-open closed session"); } - const queryParams = new URLSearchParams({ sessionId: this.sessionId }).toString(); // create promise first to avoid race condition where the popout opens before we start listening const connectPromise = firstValueFrom( - this.connected$.pipe(filter((connected) => connected === true)) - ); - this.popout = await this.popupUtilsService.popOut( - null, - `popup/index.html?uilocation=popout#/fido2?${queryParams}`, - { center: true } + merge( + this.connected$.pipe(filter((connected) => connected === true)), + fromEvent(this.abortController.signal, "abort").pipe( + switchMap(() => throwError(() => new SessionClosedError())) + ) + ) ); + + const authStatus = await this.authService.getAuthStatus(); + if (authStatus === AuthenticationStatus.LoggedOut) { + const queryParams = new URLSearchParams({ + sessionId: this.sessionId, + redirectPath: "fido2", + }).toString(); + this.popout = await this.popupUtilsService.popOut( + null, + `popup/index.html?uilocation=popout#/home?${queryParams}`, + { center: true } + ); + } else if (authStatus === AuthenticationStatus.Locked) { + const queryParams = new URLSearchParams({ + sessionId: this.sessionId, + redirectPath: "fido2", + }).toString(); + this.popout = await this.popupUtilsService.popOut( + null, + `popup/index.html?uilocation=popout#/lock?${queryParams}`, + { center: true } + ); + } else { + const queryParams = new URLSearchParams({ sessionId: this.sessionId }).toString(); + this.popout = await this.popupUtilsService.popOut( + null, + `popup/index.html?uilocation=popout#/fido2?${queryParams}`, + { center: true } + ); + } + + if (this.popout.type === "window") { + const popoutWindow = this.popout as { type: "window"; window: chrome.windows.Window }; + this.windowClosed$ + .pipe( + filter((windowId) => popoutWindow.window.id === windowId), + takeUntil(this.destroy$) + ) + .subscribe(() => { + this.close(); + this.abort(); + }); + } else if (this.popout.type === "tab") { + const popoutTab = this.popout as { type: "tab"; tab: chrome.tabs.Tab }; + this.tabClosed$ + .pipe( + filter((tabId) => popoutTab.tab.id === tabId), + takeUntil(this.destroy$) + ) + .subscribe(() => { + this.close(); + this.abort(); + }); + } + await connectPromise; } } diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.html b/apps/browser/src/vault/popup/components/fido2/fido2.component.html index 0be33e50c3f..3bd5ad44d11 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.html +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.html @@ -16,14 +16,15 @@ A site is asking for authentication, please choose one of the following credentials to use:
- + > -->
+

Here herer

A site wants to create the following passkey in your vault
diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts index f1edddf17b9..82bc65b3cca 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -1,4 +1,4 @@ -import { Component, HostListener, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, @@ -19,7 +19,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2KeyView } from "@bitwarden/common/vault/models/view/fido2-key.view"; import { BrowserApi } from "../../../../platform/browser/browser-api"; -import { PopupUtilsService } from "../../../../popup/services/popup-utils.service"; import { BrowserFido2Message, BrowserFido2UserInterfaceSession, @@ -49,8 +48,7 @@ export class Fido2Component implements OnInit, OnDestroy { constructor( private activatedRoute: ActivatedRoute, private cipherService: CipherService, - private passwordRepromptService: PasswordRepromptService, - private popupUtils: PopupUtilsService + private passwordRepromptService: PasswordRepromptService ) {} ngOnInit(): void { @@ -63,19 +61,6 @@ export class Fido2Component implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe(([sessionId, message]) => { this.sessionId = sessionId; - if (message.type === "LogInRequest") { - //route to login - const url = chrome.extension.getURL( - `popup/index.html#/home?sessionId=${sessionId}&redirectPath=fido2&uilocation=popout` - ); - // BrowserApi.createNewWindow(url); - BrowserApi.createNewTab(url); - // if (this.popupUtils.inPopup(window)) { - // BrowserApi.closePopup(window); - // } - return; - } - if (message.type === "NewSessionCreatedRequest" && message.sessionId !== sessionId) { return this.abort(false); } @@ -196,7 +181,6 @@ export class Fido2Component implements OnInit, OnDestroy { window.close(); } - @HostListener("window:unload") unload(fallback = false) { this.send({ sessionId: this.sessionId, diff --git a/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts index f804e154310..fad47f5f58b 100644 --- a/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -32,11 +32,12 @@ export abstract class Fido2UserInterfaceSession { params: NewCredentialParams, abortController?: AbortController ) => Promise<{ cipherId: string; userVerified: boolean }>; + ensureUnlockedVault: () => Promise; + fullSync: () => Promise; informExcludedCredential: ( existingCipherIds: string[], abortController?: AbortController ) => Promise; informCredentialNotFound: (abortController?: AbortController) => Promise; - login: (userVerification: boolean) => Promise<{ userVerified: boolean }>; close: () => void; } diff --git a/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts b/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts index df947dbcee2..f506a0747a7 100644 --- a/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts @@ -1,9 +1,9 @@ import { CBOR } from "cbor-redux"; +import { AuthService } from "../../../auth/abstractions/auth.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { Utils } from "../../../platform/misc/utils"; import { CipherService } from "../../abstractions/cipher.service"; -import { AuthService } from "../../../auth/abstractions/auth.service"; import { Fido2AlgorithmIdentifier, Fido2AutenticatorError, @@ -22,7 +22,6 @@ import { Fido2KeyView } from "../../models/view/fido2-key.view"; import { joseToDer } from "./ecdsa-utils"; import { Fido2Utils } from "./fido2-utils"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; // AAGUID: 6e8248d5-b479-40db-a3d8-11116f7e8349 export const AAGUID = new Uint8Array([ @@ -221,14 +220,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr params.fallbackSupported, abortController ); - try { - const authStatus = await this.authService.getAuthStatus(); - - if (authStatus === AuthenticationStatus.LoggedOut) { - const response = await userInterfaceSession.login(params.requireUserVerification); - } - if ( params.requireUserVerification != undefined && typeof params.requireUserVerification !== "boolean" @@ -243,6 +235,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr let cipherOptions: CipherView[]; + //TODO: add here + await userInterfaceSession.ensureUnlockedVault(); + // eslint-disable-next-line no-empty if (params.allowCredentialDescriptorList?.length > 0) { cipherOptions = await this.findCredentialsById(