diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts index 53d3d10076..9db6574aad 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts @@ -96,7 +96,7 @@ describe("CipherContextMenuHandler", () => { expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com"); - expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(1); + expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2); expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith( "Test Cipher (Test Username)", diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts index d4f72e5663..fe6479aae5 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts @@ -4,7 +4,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -177,11 +176,7 @@ export class CipherContextMenuHandler { } private async updateForCipher(url: string, cipher: CipherView) { - if ( - cipher == null || - cipher.type !== CipherType.Login || - cipher.reprompt !== CipherRepromptType.None - ) { + if (cipher == null || cipher.type !== CipherType.Login) { return; } diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index a5cf1c7065..38e605abe7 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -203,17 +203,45 @@ export class ContextMenuClickedHandler { if (tab == null) { return; } - await this.autofillAction(tab, cipher); + + if (cipher.reprompt !== CipherRepromptType.None) { + await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { + cipherId: cipher.id, + action: AUTOFILL_ID, + }); + } else { + await this.autofillAction(tab, cipher); + } + break; case COPY_USERNAME_ID: this.copyToClipboard({ text: cipher.login.username, tab: tab }); break; case COPY_PASSWORD_ID: - this.copyToClipboard({ text: cipher.login.password, tab: tab }); - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id); + if (cipher.reprompt !== CipherRepromptType.None) { + await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { + cipherId: cipher.id, + action: COPY_PASSWORD_ID, + }); + } else { + this.copyToClipboard({ text: cipher.login.password, tab: tab }); + this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id); + } + break; case COPY_VERIFICATIONCODE_ID: - this.copyToClipboard({ text: await this.totpService.getCode(cipher.login.totp), tab: tab }); + if (cipher.reprompt !== CipherRepromptType.None) { + await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { + cipherId: cipher.id, + action: COPY_VERIFICATIONCODE_ID, + }); + } else { + this.copyToClipboard({ + text: await this.totpService.getCode(cipher.login.totp), + tab: tab, + }); + } + break; } } diff --git a/apps/browser/src/autofill/content/message_handler.ts b/apps/browser/src/autofill/content/message_handler.ts index 5302a5042b..5ef0abdb7c 100644 --- a/apps/browser/src/autofill/content/message_handler.ts +++ b/apps/browser/src/autofill/content/message_handler.ts @@ -28,6 +28,7 @@ window.addEventListener( const forwardCommands = [ "promptForLogin", + "passwordReprompt", "addToLockedVaultPendingNotifications", "unlockCompleted", "addedCipher", diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 9ced0104b8..8b7ec91653 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -234,7 +234,16 @@ export default class AutofillService implements AutofillServiceInterface { } } - if (cipher == null || cipher.reprompt !== CipherRepromptType.None) { + if (cipher == null) { + return null; + } + + if (cipher.reprompt !== CipherRepromptType.None) { + await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { + cipherId: cipher.id, + action: "autofill", + }); + return null; } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 2e9fc93488..ee15c0a3b9 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -61,6 +61,8 @@ export default class RuntimeBackground { } async processMessage(msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) { + const cipherId = msg.data?.cipherId; + switch (msg.command) { case "loggedIn": case "unlocked": { @@ -68,7 +70,7 @@ export default class RuntimeBackground { if (this.lockedVaultPendingNotifications?.length > 0) { item = this.lockedVaultPendingNotifications.pop(); - await this.browserPopoutWindowService.closeLoginPrompt(); + await this.browserPopoutWindowService.closeUnlockPrompt(); } await this.main.refreshBadge(); @@ -108,13 +110,22 @@ export default class RuntimeBackground { break; case "promptForLogin": case "bgReopenPromptForLogin": - await this.browserPopoutWindowService.openLoginPrompt(sender.tab?.windowId); + await this.browserPopoutWindowService.openUnlockPrompt(sender.tab?.windowId); + break; + case "passwordReprompt": + if (cipherId) { + await this.browserPopoutWindowService.openPasswordRepromptPrompt(sender.tab?.windowId, { + cipherId: cipherId, + senderTabId: sender.tab.id, + action: msg.data?.action, + }); + } break; case "openAddEditCipher": { const addEditCipherUrl = - msg.data?.cipherId == null + cipherId == null ? "popup/index.html#/edit-cipher" - : "popup/index.html#/edit-cipher?cipherId=" + msg.data.cipherId; + : "popup/index.html#/edit-cipher?cipherId=" + cipherId; BrowserApi.openBitwardenExtensionTab(addEditCipherUrl, true); break; diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 243971dbfc..675fd0b119 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -42,11 +42,20 @@ export class BrowserApi { }); } - static async getTab(tabId: number) { - if (tabId == null) { + static async getTab(tabId: number): Promise | null { + if (!tabId) { return null; } - return await chrome.tabs.get(tabId); + + if (BrowserApi.manifestVersion === 3) { + return await chrome.tabs.get(tabId); + } + + return new Promise((resolve) => + chrome.tabs.get(tabId, (tab) => { + resolve(tab); + }) + ); } static async getTabFromCurrentWindow(): Promise | null { diff --git a/apps/browser/src/platform/popup/abstractions/browser-popout-window.service.ts b/apps/browser/src/platform/popup/abstractions/browser-popout-window.service.ts index ca22e369d8..0b3f55ee99 100644 --- a/apps/browser/src/platform/popup/abstractions/browser-popout-window.service.ts +++ b/apps/browser/src/platform/popup/abstractions/browser-popout-window.service.ts @@ -1,6 +1,15 @@ interface BrowserPopoutWindowService { - openLoginPrompt(senderWindowId: number): Promise; - closeLoginPrompt(): Promise; + openUnlockPrompt(senderWindowId: number): Promise; + closeUnlockPrompt(): Promise; + openPasswordRepromptPrompt( + senderWindowId: number, + promptData: { + action: string; + cipherId: string; + senderTabId: number; + } + ): Promise; + closePasswordRepromptPrompt(): Promise; } export { BrowserPopoutWindowService }; diff --git a/apps/browser/src/platform/popup/browser-popout-window.service.ts b/apps/browser/src/platform/popup/browser-popout-window.service.ts index bfec3e690b..95be15cc20 100644 --- a/apps/browser/src/platform/popup/browser-popout-window.service.ts +++ b/apps/browser/src/platform/popup/browser-popout-window.service.ts @@ -11,20 +11,48 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface { height: 800, }; - async openLoginPrompt(senderWindowId: number) { - await this.closeLoginPrompt(); - await this.openPopoutWindow( + async openUnlockPrompt(senderWindowId: number) { + await this.closeUnlockPrompt(); + await this.openSingleActionPopout( senderWindowId, "popup/index.html?uilocation=popout", - "loginPrompt" + "unlockPrompt" ); } - async closeLoginPrompt() { - await this.closeSingleActionPopout("loginPrompt"); + async closeUnlockPrompt() { + await this.closeSingleActionPopout("unlockPrompt"); } - private async openPopoutWindow( + async openPasswordRepromptPrompt( + senderWindowId: number, + { + cipherId, + senderTabId, + action, + }: { + cipherId: string; + senderTabId: number; + action: string; + } + ) { + await this.closePasswordRepromptPrompt(); + + const promptWindowPath = + "popup/index.html#/view-cipher" + + "?uilocation=popout" + + `&cipherId=${cipherId}` + + `&senderTabId=${senderTabId}` + + `&action=${action}`; + + await this.openSingleActionPopout(senderWindowId, promptWindowPath, "passwordReprompt"); + } + + async closePasswordRepromptPrompt() { + await this.closeSingleActionPopout("passwordReprompt"); + } + + private async openSingleActionPopout( senderWindowId: number, popupWindowURL: string, singleActionPopoutKey: string diff --git a/apps/browser/src/vault/popup/components/vault/view.component.html b/apps/browser/src/vault/popup/components/vault/view.component.html index 7b313ab117..e8dceaff09 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.html +++ b/apps/browser/src/vault/popup/components/vault/view.component.html @@ -555,7 +555,11 @@ class="box-content-row" appStopClick (click)="fillCipher()" - *ngIf="cipher.type !== cipherType.SecureNote && !cipher.isDeleted && !inPopout" + *ngIf=" + cipher.type !== cipherType.SecureNote && + !cipher.isDeleted && + (!this.inPopout || this.loadAction) + " >