diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index ec08fef4e2..2c1573d4e9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -156,7 +156,9 @@ import { BrowserSendService } from "../services/browser-send.service"; import { BrowserSettingsService } from "../services/browser-settings.service"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service"; +import { Fido2Service as Fido2ServiceAbstraction } from "../vault/services/abstractions/fido2.service"; import { BrowserFolderService } from "../vault/services/browser-folder.service"; +import Fido2Service from "../vault/services/fido2.service"; import { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -232,6 +234,7 @@ export default class MainBackground { authRequestCryptoService: AuthRequestCryptoServiceAbstraction; accountService: AccountServiceAbstraction; globalStateProvider: GlobalStateProvider; + fido2Service: Fido2ServiceAbstraction; // Passed to the popup for Safari to workaround issues with theming, downloading, etc. backgroundWindow = window; @@ -597,6 +600,7 @@ export default class MainBackground { this.messagingService, ); + this.fido2Service = new Fido2Service(); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); this.fido2AuthenticatorService = new Fido2AuthenticatorService( this.cipherService, @@ -645,6 +649,7 @@ export default class MainBackground { this.messagingService, this.logService, this.configService, + this.fido2Service, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.cryptoService, @@ -778,6 +783,8 @@ export default class MainBackground { await this.idleBackground.init(); await this.webRequestBackground.init(); + await this.fido2Service.init(); + if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { // Set Private Mode windows to the default icon - they do not share state with the background page const privateWindows = await BrowserApi.getPrivateModeWindows(); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 434fd07834..fcaefc7c5e 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -20,6 +20,7 @@ import { BrowserStateService } from "../platform/services/abstractions/browser-s import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; import { AbortManager } from "../vault/background/abort-manager"; +import { Fido2Service } from "../vault/services/abstractions/fido2.service"; import MainBackground from "./main.background"; @@ -42,6 +43,7 @@ export default class RuntimeBackground { private messagingService: MessagingService, private logService: LogService, private configService: ConfigServiceAbstraction, + private fido2Service: Fido2Service, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -257,6 +259,9 @@ export default class RuntimeBackground { case "getClickedElementResponse": this.platformUtilsService.copyToClipboard(msg.identifier, { window: window }); break; + case "triggerFido2ContentScriptInjection": + await this.fido2Service.injectFido2ContentScripts(sender); + break; case "fido2AbortRequest": this.abortManager.abort(msg.abortedRequestId); break; diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 2bd032495c..aa71d648e8 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -17,7 +17,10 @@ "content_scripts": [ { "all_frames": true, - "js": ["content/trigger-autofill-script-injection.js", "content/fido2/content-script.js"], + "js": [ + "content/trigger-autofill-script-injection.js", + "content/fido2/trigger-fido2-content-script-injection.js" + ], "matches": ["http://*/*", "https://*/*", "file:///*"], "run_at": "document_start" }, diff --git a/apps/browser/src/vault/fido2/content/content-script.ts b/apps/browser/src/vault/fido2/content/content-script.ts index 3fe3e81448..54d18f2c8f 100644 --- a/apps/browser/src/vault/fido2/content/content-script.ts +++ b/apps/browser/src/vault/fido2/content/content-script.ts @@ -61,12 +61,28 @@ async function isLocationBitwardenVault(activeUserSettings: Record) return window.location.origin === activeUserSettings.serverConfig.environment.vault; } -function initializeFido2ContentScript() { - const s = document.createElement("script"); - s.src = chrome.runtime.getURL("content/fido2/page-script.js"); - (document.head || document.documentElement).appendChild(s); +const messenger = Messenger.forDOMCommunication(window); - const messenger = Messenger.forDOMCommunication(window); +function injectPageScript() { + // Locate an existing page-script on the page + const existingPageScript = document.getElementById("bw-fido2-page-script"); + + // Inject the page-script if it doesn't exist + if (!existingPageScript) { + const s = document.createElement("script"); + s.src = chrome.runtime.getURL("content/fido2/page-script.js"); + s.id = "bw-fido2-page-script"; + (document.head || document.documentElement).appendChild(s); + + return; + } + + // If the page-script already exists, send a reconnect message to the page-script + messenger.sendReconnectCommand(); +} + +function initializeFido2ContentScript() { + injectPageScript(); messenger.handler = async (message, abortController) => { const requestId = Date.now().toString(); @@ -78,7 +94,7 @@ function initializeFido2ContentScript() { abortController.signal.addEventListener("abort", abortHandler); if (message.type === MessageType.CredentialCreationRequest) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const data: CreateCredentialParams = { ...message.data, origin: window.location.origin, @@ -92,7 +108,7 @@ function initializeFido2ContentScript() { requestId: requestId, }, (response) => { - if (response.error !== undefined) { + if (response && response.error !== undefined) { return reject(response.error); } @@ -106,7 +122,7 @@ function initializeFido2ContentScript() { } if (message.type === MessageType.CredentialGetRequest) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const data: AssertCredentialParams = { ...message.data, origin: window.location.origin, @@ -120,7 +136,7 @@ function initializeFido2ContentScript() { requestId: requestId, }, (response) => { - if (response.error !== undefined) { + if (response && response.error !== undefined) { return reject(response.error); } @@ -155,6 +171,12 @@ async function run() { } initializeFido2ContentScript(); + + const port = chrome.runtime.connect({ name: "fido2ContentScriptReady" }); + port.onDisconnect.addListener(() => { + // Cleanup the messenger and remove the event listener + messenger.destroy(); + }); } run(); diff --git a/apps/browser/src/vault/fido2/content/messaging/message.ts b/apps/browser/src/vault/fido2/content/messaging/message.ts index e516dd9b37..b803b97f92 100644 --- a/apps/browser/src/vault/fido2/content/messaging/message.ts +++ b/apps/browser/src/vault/fido2/content/messaging/message.ts @@ -11,6 +11,8 @@ export enum MessageType { CredentialGetRequest, CredentialGetResponse, AbortRequest, + DisconnectRequest, + ReconnectRequest, AbortResponse, ErrorResponse, } @@ -60,6 +62,14 @@ export type AbortRequest = { abortedRequestId: string; }; +export type DisconnectRequest = { + type: MessageType.DisconnectRequest; +}; + +export type ReconnectRequest = { + type: MessageType.ReconnectRequest; +}; + export type ErrorResponse = { type: MessageType.ErrorResponse; error: string; @@ -76,5 +86,7 @@ export type Message = | CredentialGetRequest | CredentialGetResponse | AbortRequest + | DisconnectRequest + | ReconnectRequest | AbortResponse | ErrorResponse; diff --git a/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts b/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts index 02e0f94497..4b108a5581 100644 --- a/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts +++ b/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts @@ -12,6 +12,12 @@ describe("Messenger", () => { beforeEach(() => { // jest does not support MessageChannel window.MessageChannel = MockMessageChannel as any; + Object.defineProperty(window, "location", { + value: { + origin: "https://bitwarden.com", + }, + writable: true, + }); const channelPair = new TestChannelPair(); messengerA = new Messenger(channelPair.channelA); @@ -27,7 +33,7 @@ describe("Messenger", () => { const request = createRequest(); messengerA.request(request); - const received = handlerB.recieve(); + const received = handlerB.receive(); expect(received.length).toBe(1); expect(received[0].message).toMatchObject(request); @@ -37,7 +43,7 @@ describe("Messenger", () => { const request = createRequest(); const response = createResponse(); const requestPromise = messengerA.request(request); - const received = handlerB.recieve(); + const received = handlerB.receive(); received[0].respond(response); const returned = await requestPromise; @@ -49,7 +55,7 @@ describe("Messenger", () => { const request = createRequest(); const error = new Error("Test error"); const requestPromise = messengerA.request(request); - const received = handlerB.recieve(); + const received = handlerB.receive(); received[0].reject(error); @@ -61,10 +67,60 @@ describe("Messenger", () => { messengerA.request(createRequest(), abortController); abortController.abort(); - const received = handlerB.recieve(); + const received = handlerB.receive(); expect(received[0].abortController.signal.aborted).toBe(true); }); + + describe("destroy", () => { + beforeEach(() => { + /** + * In Jest's jsdom environment, there is an issue where event listeners are not + * triggered upon dispatching an event. This is a workaround to mock the EventTarget + */ + window.EventTarget = MockEventTarget as any; + }); + + it("should remove the message event listener", async () => { + const channelPair = new TestChannelPair(); + const addEventListenerSpy = jest.spyOn(channelPair.channelA, "addEventListener"); + const removeEventListenerSpy = jest.spyOn(channelPair.channelA, "removeEventListener"); + messengerA = new Messenger(channelPair.channelA); + jest + .spyOn(messengerA as any, "sendDisconnectCommand") + .mockImplementation(() => Promise.resolve()); + + expect(addEventListenerSpy).toHaveBeenCalled(); + + await messengerA.destroy(); + + expect(removeEventListenerSpy).toHaveBeenCalled(); + }); + + it("should dispatch the destroy event on messenger destruction", async () => { + const request = createRequest(); + messengerA.request(request); + + const dispatchEventSpy = jest.spyOn((messengerA as any).onDestroy, "dispatchEvent"); + messengerA.destroy(); + + expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(Event)); + }); + + it("should trigger onDestroyListener when the destroy event is dispatched", async () => { + const request = createRequest(); + messengerA.request(request); + + const onDestroyListener = jest.fn(); + (messengerA as any).onDestroy.addEventListener("destroy", onDestroyListener); + messengerA.destroy(); + + expect(onDestroyListener).toHaveBeenCalled(); + const eventArg = onDestroyListener.mock.calls[0][0]; + expect(eventArg).toBeInstanceOf(Event); + expect(eventArg.type).toBe("destroy"); + }); + }); }); type TestMessage = MessageWithMetadata & { testId: string }; @@ -86,11 +142,13 @@ class TestChannelPair { this.channelA = { addEventListener: (listener) => (broadcastChannel.port1.onmessage = listener), + removeEventListener: () => (broadcastChannel.port1.onmessage = null), postMessage: (message, port) => broadcastChannel.port1.postMessage(message, port), }; this.channelB = { addEventListener: (listener) => (broadcastChannel.port2.onmessage = listener), + removeEventListener: () => (broadcastChannel.port1.onmessage = null), postMessage: (message, port) => broadcastChannel.port2.postMessage(message, port), }; } @@ -102,7 +160,7 @@ class TestMessageHandler { abortController?: AbortController, ) => Promise; - private recievedMessages: { + private receivedMessages: { message: TestMessage; respond: (response: TestMessage) => void; reject: (error: Error) => void; @@ -112,7 +170,7 @@ class TestMessageHandler { constructor() { this.handler = (message, abortController) => new Promise((resolve, reject) => { - this.recievedMessages.push({ + this.receivedMessages.push({ message, abortController, respond: (response) => resolve(response), @@ -121,9 +179,9 @@ class TestMessageHandler { }); } - recieve() { - const received = this.recievedMessages; - this.recievedMessages = []; + receive() { + const received = this.receivedMessages; + this.receivedMessages = []; return received; } } @@ -144,7 +202,11 @@ class MockMessagePort { postMessage(message: T, port?: MessagePort) { this.remotePort.onmessage( - new MessageEvent("message", { data: message, ports: port ? [port] : [] }), + new MessageEvent("message", { + data: message, + ports: port ? [port] : [], + origin: "https://bitwarden.com", + }), ); } @@ -152,3 +214,20 @@ class MockMessagePort { // Do nothing } } + +class MockEventTarget { + listeners: Record = {}; + + addEventListener(type: string, callback: EventListener) { + this.listeners[type] = this.listeners[type] || []; + this.listeners[type].push(callback); + } + + dispatchEvent(event: Event) { + (this.listeners[event.type] || []).forEach((callback) => callback(event)); + } + + removeEventListener(type: string, callback: EventListener) { + this.listeners[type] = (this.listeners[type] || []).filter((listener) => listener !== callback); + } +} diff --git a/apps/browser/src/vault/fido2/content/messaging/messenger.ts b/apps/browser/src/vault/fido2/content/messaging/messenger.ts index b69f6ac076..cc29282227 100644 --- a/apps/browser/src/vault/fido2/content/messaging/messenger.ts +++ b/apps/browser/src/vault/fido2/content/messaging/messenger.ts @@ -1,3 +1,5 @@ +import { FallbackRequestedError } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + import { Message, MessageType } from "./message"; const SENDER = "bitwarden-webauthn"; @@ -6,15 +8,16 @@ type PostMessageFunction = (message: MessageWithMetadata, remotePort: MessagePor export type Channel = { addEventListener: (listener: (message: MessageEvent) => void) => void; + removeEventListener: (listener: (message: MessageEvent) => void) => void; postMessage: PostMessageFunction; }; -export type Metadata = { SENDER: typeof SENDER }; +export type Metadata = { SENDER: typeof SENDER; senderId: string }; export type MessageWithMetadata = Message & Metadata; type Handler = ( message: MessageWithMetadata, abortController?: AbortController, -) => Promise; +) => void | Promise; /** * A class that handles communication between the page and content script. It converts @@ -22,6 +25,9 @@ type Handler = ( * handling aborts and exceptions across separate execution contexts. */ export class Messenger { + private messageEventListener: (event: MessageEvent) => void | null = null; + private onDestroy = new EventTarget(); + /** * Creates a messenger that uses the browser's `window.postMessage` API to initiate * requests in the content script. Every request will then create it's own @@ -35,14 +41,8 @@ export class Messenger { return new Messenger({ postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), - addEventListener: (listener) => - window.addEventListener("message", (event: MessageEvent) => { - if (event.origin !== windowOrigin) { - return; - } - - listener(event as MessageEvent); - }), + addEventListener: (listener) => window.addEventListener("message", listener), + removeEventListener: (listener) => window.removeEventListener("message", listener), }); } @@ -53,38 +53,11 @@ export class Messenger { */ handler?: Handler; + private messengerId = this.generateUniqueId(); + constructor(private broadcastChannel: Channel) { - this.broadcastChannel.addEventListener(async (event) => { - if (this.handler === undefined) { - return; - } - - const message = event.data; - const port = event.ports?.[0]; - if (message?.SENDER !== SENDER || message == null || port == null) { - return; - } - - const abortController = new AbortController(); - port.onmessage = (event: MessageEvent) => { - if (event.data.type === MessageType.AbortRequest) { - abortController.abort(); - } - }; - - try { - const handlerResponse = await this.handler(message, abortController); - port.postMessage({ ...handlerResponse, SENDER }); - } catch (error) { - port.postMessage({ - SENDER, - type: MessageType.ErrorResponse, - error: JSON.stringify(error, Object.getOwnPropertyNames(error)), - }); - } finally { - port.close(); - } - }); + this.messageEventListener = this.createMessageEventListener(); + this.broadcastChannel.addEventListener(this.messageEventListener); } /** @@ -111,7 +84,10 @@ export class Messenger { }); abortController?.signal.addEventListener("abort", abortListener); - this.broadcastChannel.postMessage({ ...request, SENDER }, remotePort); + this.broadcastChannel.postMessage( + { ...request, SENDER, senderId: this.messengerId }, + remotePort, + ); const response = await promise; abortController?.signal.removeEventListener("abort", abortListener); @@ -127,4 +103,79 @@ export class Messenger { localPort.close(); } } + + private createMessageEventListener() { + return async (event: MessageEvent) => { + const windowOrigin = window.location.origin; + if (event.origin !== windowOrigin || !this.handler) { + return; + } + + const message = event.data; + const port = event.ports?.[0]; + if ( + message?.SENDER !== SENDER || + message.senderId == this.messengerId || + message == null || + port == null + ) { + return; + } + + const abortController = new AbortController(); + port.onmessage = (event: MessageEvent) => { + if (event.data.type === MessageType.AbortRequest) { + abortController.abort(); + } + }; + + let onDestroyListener; + const destroyPromise: Promise = new Promise((_, reject) => { + onDestroyListener = () => reject(new FallbackRequestedError()); + this.onDestroy.addEventListener("destroy", onDestroyListener); + }); + + try { + const handlerResponse = await Promise.race([ + this.handler(message, abortController), + destroyPromise, + ]); + port.postMessage({ ...handlerResponse, SENDER }); + } catch (error) { + port.postMessage({ + SENDER, + type: MessageType.ErrorResponse, + error: JSON.stringify(error, Object.getOwnPropertyNames(error)), + }); + } finally { + this.onDestroy.removeEventListener("destroy", onDestroyListener); + port.close(); + } + }; + } + + /** + * Cleans up the messenger by removing the message event listener + */ + async destroy() { + this.onDestroy.dispatchEvent(new Event("destroy")); + + if (this.messageEventListener) { + await this.sendDisconnectCommand(); + this.broadcastChannel.removeEventListener(this.messageEventListener); + this.messageEventListener = null; + } + } + + async sendReconnectCommand() { + await this.request({ type: MessageType.ReconnectRequest }); + } + + private async sendDisconnectCommand() { + await this.request({ type: MessageType.DisconnectRequest }); + } + + private generateUniqueId() { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } } diff --git a/apps/browser/src/vault/fido2/content/page-script.ts b/apps/browser/src/vault/fido2/content/page-script.ts index 9a3a74bed1..b489fd0f3e 100644 --- a/apps/browser/src/vault/fido2/content/page-script.ts +++ b/apps/browser/src/vault/fido2/content/page-script.ts @@ -53,10 +53,21 @@ const browserCredentials = { }; const messenger = ((window as any).messenger = Messenger.forDOMCommunication(window)); -navigator.credentials.create = async ( + +navigator.credentials.create = createWebAuthnCredential; +navigator.credentials.get = getWebAuthnCredential; + +/** + * Creates a new webauthn credential. + * + * @param options Options for creating new credentials. + * @param abortController Abort controller to abort the request if needed. + * @returns Promise that resolves to the new credential object. + */ +async function createWebAuthnCredential( options?: CredentialCreationOptions, abortController?: AbortController, -): Promise => { +): Promise { if (!isWebauthnCall(options)) { return await browserCredentials.create(options); } @@ -88,12 +99,19 @@ navigator.credentials.create = async ( throw error; } -}; +} -navigator.credentials.get = async ( +/** + * Retrieves a webauthn credential. + * + * @param options Options for creating new credentials. + * @param abortController Abort controller to abort the request if needed. + * @returns Promise that resolves to the new credential object. + */ +async function getWebAuthnCredential( options?: CredentialRequestOptions, abortController?: AbortController, -): Promise => { +): Promise { if (!isWebauthnCall(options)) { return await browserCredentials.get(options); } @@ -126,7 +144,7 @@ navigator.credentials.get = async ( throw error; } -}; +} function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) { return options && "publicKey" in options; @@ -174,3 +192,23 @@ async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) { window.clearTimeout(timeoutId); } } + +/** + * Sets up a listener to handle cleanup or reconnection when the extension's + * context changes due to being reloaded or unloaded. + */ +messenger.handler = (message, abortController) => { + const type = message.type; + + // Handle cleanup for disconnect request + if (type === MessageType.DisconnectRequest && browserNativeWebauthnSupport) { + navigator.credentials.create = browserCredentials.create; + navigator.credentials.get = browserCredentials.get; + } + + // Handle reinitialization for reconnect request + if (type === MessageType.ReconnectRequest && browserNativeWebauthnSupport) { + navigator.credentials.create = createWebAuthnCredential; + navigator.credentials.get = getWebAuthnCredential; + } +}; diff --git a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts new file mode 100644 index 0000000000..8f4efe0330 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts @@ -0,0 +1,16 @@ +describe("TriggerFido2ContentScriptInjection", () => { + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + describe("init", () => { + it("sends a message to the extension background", () => { + require("../content/trigger-fido2-content-script-injection"); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "triggerFido2ContentScriptInjection", + }); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts new file mode 100644 index 0000000000..5b90247281 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts @@ -0,0 +1,3 @@ +(function () { + chrome.runtime.sendMessage({ command: "triggerFido2ContentScriptInjection" }); +})(); diff --git a/apps/browser/src/vault/services/abstractions/fido2.service.ts b/apps/browser/src/vault/services/abstractions/fido2.service.ts new file mode 100644 index 0000000000..138b538b15 --- /dev/null +++ b/apps/browser/src/vault/services/abstractions/fido2.service.ts @@ -0,0 +1,4 @@ +export abstract class Fido2Service { + init: () => Promise; + injectFido2ContentScripts: (sender: chrome.runtime.MessageSender) => Promise; +} diff --git a/apps/browser/src/vault/services/fido2.service.spec.ts b/apps/browser/src/vault/services/fido2.service.spec.ts new file mode 100644 index 0000000000..1db2bdfb77 --- /dev/null +++ b/apps/browser/src/vault/services/fido2.service.spec.ts @@ -0,0 +1,35 @@ +import { BrowserApi } from "../../platform/browser/browser-api"; + +import Fido2Service from "./fido2.service"; + +describe("Fido2Service", () => { + let fido2Service: Fido2Service; + let tabMock: chrome.tabs.Tab; + let sender: chrome.runtime.MessageSender; + + beforeEach(() => { + fido2Service = new Fido2Service(); + tabMock = { id: 123, url: "https://bitwarden.com" } as chrome.tabs.Tab; + sender = { tab: tabMock }; + jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + describe("injectFido2ContentScripts", () => { + const fido2ContentScript = "content/fido2/content-script.js"; + const defaultExecuteScriptOptions = { runAt: "document_start" }; + + it("accepts an extension message sender and injects the fido2 scripts into the tab of the sender", async () => { + await fido2Service.injectFido2ContentScripts(sender); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { + file: fido2ContentScript, + ...defaultExecuteScriptOptions, + }); + }); + }); +}); diff --git a/apps/browser/src/vault/services/fido2.service.ts b/apps/browser/src/vault/services/fido2.service.ts new file mode 100644 index 0000000000..a2b21f5651 --- /dev/null +++ b/apps/browser/src/vault/services/fido2.service.ts @@ -0,0 +1,33 @@ +import { BrowserApi } from "../../platform/browser/browser-api"; + +import { Fido2Service as Fido2ServiceInterface } from "./abstractions/fido2.service"; + +export default class Fido2Service implements Fido2ServiceInterface { + async init() { + const tabs = await BrowserApi.tabsQuery({}); + tabs.forEach((tab) => { + if (tab.url?.startsWith("https")) { + this.injectFido2ContentScripts({ tab } as chrome.runtime.MessageSender); + } + }); + + BrowserApi.addListener(chrome.runtime.onConnect, (port) => { + if (port.name === "fido2ContentScriptReady") { + port.postMessage({ command: "fido2ContentScriptInit" }); + } + }); + } + + /** + * Injects the FIDO2 content script into the current tab. + * @param {chrome.runtime.MessageSender} sender + * @returns {Promise} + */ + async injectFido2ContentScripts(sender: chrome.runtime.MessageSender): Promise { + await BrowserApi.executeScriptInTab(sender.tab.id, { + file: "content/fido2/content-script.js", + frameId: sender.frameId, + runAt: "document_start", + }); + } +} diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 23a24b91e6..f5342f7492 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -171,6 +171,8 @@ const mainConfig = { "content/notificationBar": "./src/autofill/content/notification-bar.ts", "content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts", "content/message_handler": "./src/autofill/content/message_handler.ts", + "content/fido2/trigger-fido2-content-script-injection": + "./src/vault/fido2/content/trigger-fido2-content-script-injection.ts", "content/fido2/content-script": "./src/vault/fido2/content/content-script.ts", "content/fido2/page-script": "./src/vault/fido2/content/page-script.ts", "notification/bar": "./src/autofill/notification/bar.ts",