From c7e7ce832bfa1c0c8920da441fb497e58edc2a5a Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 31 Jan 2023 11:10:36 +0100 Subject: [PATCH] [EC-598] feat: ability to abort from page script --- apps/browser/src/browser/browserApi.ts | 10 +++ .../src/popup/fido2/fido2.component.ts | 14 +++- .../browser-fido2-user-interface.service.ts | 69 ++++++++++++++----- ...ido2-user-interface.service.abstraction.ts | 9 ++- .../src/services/fido2/fido2.service.ts | 29 +++++--- 5 files changed, 101 insertions(+), 30 deletions(-) diff --git a/apps/browser/src/browser/browserApi.ts b/apps/browser/src/browser/browserApi.ts index 3dd9bd624a4..74f5de6e35d 100644 --- a/apps/browser/src/browser/browserApi.ts +++ b/apps/browser/src/browser/browserApi.ts @@ -1,3 +1,5 @@ +import { Observable } from "rxjs"; + import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service"; import { TabMessage } from "../types/tab-messages"; @@ -146,6 +148,14 @@ export class BrowserApi { ); } + static messageListener$() { + return new Observable((subscriber) => { + const handler = (message: unknown) => subscriber.next(message); + chrome.runtime.onMessage.addListener(handler); + return () => chrome.runtime.onMessage.removeListener(handler); + }); + } + static sendMessage(subscriber: string, arg: any = {}) { const message = Object.assign({}, { command: subscriber }, arg); return chrome.runtime.sendMessage(message); diff --git a/apps/browser/src/popup/fido2/fido2.component.ts b/apps/browser/src/popup/fido2/fido2.component.ts index deb779913b1..893d0667387 100644 --- a/apps/browser/src/popup/fido2/fido2.component.ts +++ b/apps/browser/src/popup/fido2/fido2.component.ts @@ -1,6 +1,6 @@ import { Component, HostListener, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Subject, takeUntil } from "rxjs"; +import { concatMap, Subject, switchMap, takeUntil } from "rxjs"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/enums/cipherType"; @@ -53,6 +53,16 @@ export class Fido2Component implements OnInit, OnDestroy { takeUntil(this.destroy$) ) .subscribe(); + + this.activatedRoute.queryParamMap + .pipe( + switchMap((queryParamMap) => { + const data = JSON.parse(queryParamMap.get("data")); + return BrowserFido2UserInterfaceService.onAbort$(data.requestId); + }), + takeUntil(this.destroy$) + ) + .subscribe(() => this.cancel(false)); } async pick(cipher: CipherView) { @@ -91,7 +101,7 @@ export class Fido2Component implements OnInit, OnDestroy { const data = this.data; BrowserFido2UserInterfaceService.sendMessage({ requestId: data.requestId, - type: "RequestCancelled", + type: "AbortResponse", fallbackRequested: fallback, }); } 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 fc90a0665c6..470515ea18a 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 @@ -1,4 +1,4 @@ -import { filter, first, lastValueFrom, Subject, takeUntil } from "rxjs"; +import { filter, first, lastValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction, @@ -37,7 +37,10 @@ export type BrowserFido2Message = { requestId: string } & ( type: "ConfirmNewCredentialResponse"; } | { - type: "RequestCancelled"; + type: "AbortRequest"; + } + | { + type: "AbortResponse"; fallbackRequested: boolean; } ); @@ -51,17 +54,31 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi BrowserApi.sendMessage(BrowserFido2MessageName, msg); } - private messages$ = new Subject(); - private destroy$ = new Subject(); - - constructor(private popupUtilsService: PopupUtilsService) { - BrowserApi.messageListener(BrowserFido2MessageName, this.processMessage.bind(this)); + static onAbort$(requestId: string): Observable { + const messages$ = BrowserApi.messageListener$() as Observable; + return messages$.pipe( + filter((message) => message.type === "AbortRequest" && message.requestId === requestId), + first() + ); } - async confirmCredential(cipherId: string): Promise { + private messages$ = BrowserApi.messageListener$() as Observable; + private destroy$ = new Subject(); + + constructor(private popupUtilsService: PopupUtilsService) {} + + async confirmCredential( + cipherId: string, + abortController = new AbortController() + ): Promise { const requestId = Utils.newGuid(); const data: BrowserFido2Message = { type: "ConfirmCredentialRequest", cipherId, requestId }; const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString(); + + const abortHandler = () => + BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId }); + abortController.signal.addEventListener("abort", abortHandler); + this.popupUtilsService.popOut( null, `popup/index.html?uilocation=popout#/fido2?${queryParams}`, @@ -80,17 +97,27 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi return true; } - if (response.type === "RequestCancelled") { + if (response.type === "AbortResponse") { throw new RequestAbortedError(response.fallbackRequested); } + abortController.signal.removeEventListener("abort", abortHandler); + return false; } - async pickCredential(cipherIds: string[]): Promise { + async pickCredential( + cipherIds: string[], + abortController = new AbortController() + ): Promise { const requestId = Utils.newGuid(); const data: BrowserFido2Message = { type: "PickCredentialRequest", cipherIds, requestId }; const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString(); + + const abortHandler = () => + BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId }); + abortController.signal.addEventListener("abort", abortHandler); + this.popupUtilsService.popOut( null, `popup/index.html?uilocation=popout#/fido2?${queryParams}`, @@ -105,7 +132,7 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi ) ); - if (response.type === "RequestCancelled") { + if (response.type === "AbortResponse") { throw new RequestAbortedError(response.fallbackRequested); } @@ -113,10 +140,15 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi throw new RequestAbortedError(); } + abortController.signal.removeEventListener("abort", abortHandler); + return response.cipherId; } - async confirmNewCredential({ credentialName, userName }: NewCredentialParams): Promise { + async confirmNewCredential( + { credentialName, userName }: NewCredentialParams, + abortController = new AbortController() + ): Promise { const requestId = Utils.newGuid(); const data: BrowserFido2Message = { type: "ConfirmNewCredentialRequest", @@ -125,6 +157,11 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi userName, }; const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString(); + + const abortHandler = () => + BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId }); + abortController.signal.addEventListener("abort", abortHandler); + this.popupUtilsService.popOut( null, `popup/index.html?uilocation=popout#/fido2?${queryParams}`, @@ -143,14 +180,12 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi return true; } - if (response.type === "RequestCancelled") { + if (response.type === "AbortResponse") { throw new RequestAbortedError(response.fallbackRequested); } + abortController.signal.removeEventListener("abort", abortHandler); + return false; } - - private processMessage(msg: BrowserFido2Message) { - this.messages$.next(msg); - } } diff --git a/libs/common/src/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 34fa577556f..cd08532fbfa 100644 --- a/libs/common/src/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -4,7 +4,10 @@ export interface NewCredentialParams { } export abstract class Fido2UserInterfaceService { - confirmCredential: (cipherId: string) => Promise; - pickCredential: (cipherIds: string[]) => Promise; - confirmNewCredential: (params: NewCredentialParams) => Promise; + confirmCredential: (cipherId: string, abortController?: AbortController) => Promise; + pickCredential: (cipherIds: string[], abortController?: AbortController) => Promise; + confirmNewCredential: ( + params: NewCredentialParams, + abortController?: AbortController + ) => Promise; } diff --git a/libs/common/src/services/fido2/fido2.service.ts b/libs/common/src/services/fido2/fido2.service.ts index 756fde1ef02..37ea47767b4 100644 --- a/libs/common/src/services/fido2/fido2.service.ts +++ b/libs/common/src/services/fido2/fido2.service.ts @@ -48,10 +48,13 @@ export class Fido2Service implements Fido2ServiceAbstraction { params: CredentialRegistrationParams, abortController?: AbortController ): Promise { - const presence = await this.fido2UserInterfaceService.confirmNewCredential({ - credentialName: params.rp.name, - userName: params.user.displayName, - }); + const presence = await this.fido2UserInterfaceService.confirmNewCredential( + { + credentialName: params.rp.name, + userName: params.user.displayName, + }, + abortController + ); const attestationFormat = STANDARD_ATTESTATION_FORMAT; const encoder = new TextEncoder(); @@ -122,7 +125,10 @@ export class Fido2Service implements Fido2ServiceAbstraction { }; } - async assertCredential(params: CredentialAssertParams): Promise { + async assertCredential( + params: CredentialAssertParams, + abortController?: AbortController + ): Promise { let credential: BitCredential | undefined; if (params.allowedCredentialIds && params.allowedCredentialIds.length > 0) { @@ -138,7 +144,10 @@ export class Fido2Service implements Fido2ServiceAbstraction { // throw new OriginMismatchError(); // } - await this.fido2UserInterfaceService.confirmCredential(credential.credentialId.encoded); + await this.fido2UserInterfaceService.confirmCredential( + credential.credentialId.encoded, + abortController + ); } else { // We're looking for a resident key const credentials = await this.getCredentialsByRp(params.rpId); @@ -148,7 +157,8 @@ export class Fido2Service implements Fido2ServiceAbstraction { } const pickedId = await this.fido2UserInterfaceService.pickCredential( - credentials.map((c) => c.credentialId.encoded) + credentials.map((c) => c.credentialId.encoded), + abortController ); credential = credentials.find((c) => c.credentialId.encoded === pickedId); } @@ -184,7 +194,10 @@ export class Fido2Service implements Fido2ServiceAbstraction { }; } - private async getCredential(allowedCredentialIds: string[]): Promise { + private async getCredential( + allowedCredentialIds: string[], + abortController?: AbortController + ): Promise { let cipher: Cipher | undefined; for (const allowedCredential of allowedCredentialIds) { cipher = await this.cipherService.get(allowedCredential);