From 537fa67b09329930e2865cca68e01a4f0f1cfbc4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 31 Jul 2024 16:03:13 +0200 Subject: [PATCH 01/13] [PM-9465] Move shared ipc keys to main process (#9944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove old biometrics masterkey logic * Move shared ipc keys to main process * Update apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts Co-authored-by: Daniel García * Extract ephemeral store functions to it's own object --------- Co-authored-by: Daniel García --- apps/desktop/src/main.ts | 3 +++ apps/desktop/src/platform/preload.ts | 9 ++++++++ .../ephemeral-value-storage.main.service.ts | 21 +++++++++++++++++++ .../src/services/native-messaging.service.ts | 13 ++++++------ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 762c755485a..816d317f411 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -39,6 +39,7 @@ import { MainCryptoFunctionService } from "./platform/main/main-crypto-function. import { DesktopSettingsService } from "./platform/services/desktop-settings.service"; import { ElectronLogMainService } from "./platform/services/electron-log.main.service"; import { ElectronStorageService } from "./platform/services/electron-storage.service"; +import { EphemeralValueStorageService } from "./platform/services/ephemeral-value-storage.main.service"; import { I18nMainService } from "./platform/services/i18n.main.service"; import { ElectronMainMessagingService } from "./services/electron-main-messaging.service"; import { isMacAppStore } from "./utils"; @@ -224,6 +225,8 @@ export class Main { this.clipboardMain = new ClipboardMain(); this.clipboardMain.init(); + + new EphemeralValueStorageService(); } bootstrap() { diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index b3007753eed..5dced6227bc 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -99,6 +99,14 @@ const crypto = { ipcRenderer.invoke("crypto.argon2", { password, salt, iterations, memory, parallelism }), }; +const ephemeralStore = { + setEphemeralValue: (key: string, value: string): Promise => + ipcRenderer.invoke("setEphemeralValue", { key, value }), + getEphemeralValue: (key: string): Promise => ipcRenderer.invoke("getEphemeralValue", key), + removeEphemeralValue: (key: string): Promise => + ipcRenderer.invoke("deleteEphemeralValue", key), +}; + export default { versions: { app: (): Promise => ipcRenderer.invoke("appVersion"), @@ -156,6 +164,7 @@ export default { powermonitor, nativeMessaging, crypto, + ephemeralStore, }; function deviceType(): DeviceType { diff --git a/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts b/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts new file mode 100644 index 00000000000..b59b48be1e1 --- /dev/null +++ b/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts @@ -0,0 +1,21 @@ +import { ipcMain } from "electron"; + +/** + * The ephemeral value store holds values that should be accessible to the renderer past a process reload. + * In the current state, this store must not contain any keys that can decrypt a vault by themselves. + */ +export class EphemeralValueStorageService { + private ephemeralValues = new Map(); + + constructor() { + ipcMain.handle("setEphemeralValue", async (event, { key, value }) => { + this.ephemeralValues.set(key, value); + }); + ipcMain.handle("getEphemeralValue", async (event, key: string) => { + return this.ephemeralValues.get(key); + }); + ipcMain.handle("deleteEphemeralValue", async (event, key: string) => { + this.ephemeralValues.delete(key); + }); + } +} diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 2a5c341ee7b..ce97b9b7ac4 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -30,8 +30,6 @@ const HashAlgorithmForAsymmetricEncryption = "sha1"; @Injectable() export class NativeMessagingService { - private sharedSecrets = new Map(); - constructor( private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, @@ -104,7 +102,7 @@ export class NativeMessagingService { return; } - if (this.sharedSecrets.get(appId) == null) { + if ((await ipc.platform.ephemeralStore.getEphemeralValue(appId)) == null) { ipc.platform.nativeMessaging.sendMessage({ command: "invalidateEncryption", appId: appId, @@ -115,7 +113,7 @@ export class NativeMessagingService { const message: LegacyMessage = JSON.parse( await this.cryptoService.decryptToUtf8( rawMessage as EncString, - this.sharedSecrets.get(appId), + SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)), ), ); @@ -205,7 +203,7 @@ export class NativeMessagingService { const encrypted = await this.cryptoService.encrypt( JSON.stringify(message), - this.sharedSecrets.get(appId), + SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)), ); ipc.platform.nativeMessaging.sendMessage({ appId: appId, message: encrypted }); @@ -213,7 +211,10 @@ export class NativeMessagingService { private async secureCommunication(remotePublicKey: Uint8Array, appId: string) { const secret = await this.cryptoFunctionService.randomBytes(64); - this.sharedSecrets.set(appId, new SymmetricCryptoKey(secret)); + await ipc.platform.ephemeralStore.setEphemeralValue( + appId, + new SymmetricCryptoKey(secret).keyB64, + ); const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt( secret, From 766c2f4b9c183704c03717ac49c1cfe682a260cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:27:20 +0100 Subject: [PATCH 02/13] [PM-8290] Refresh vault after saving item collections (#10053) --- .../app/vault/individual-vault/vault.component.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 71c025f276a..6aca5662e53 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -86,7 +86,10 @@ import { BulkShareDialogResult, openBulkShareDialog, } from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component"; -import { openIndividualVaultCollectionsDialog } from "./collections.component"; +import { + CollectionsDialogResult, + openIndividualVaultCollectionsDialog, +} from "./collections.component"; import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component"; import { ShareComponent } from "./share.component"; import { VaultBannersComponent } from "./vault-banners/vault-banners.component"; @@ -573,7 +576,14 @@ export class VaultComponent implements OnInit, OnDestroy { } async editCipherCollections(cipher: CipherView) { - openIndividualVaultCollectionsDialog(this.dialogService, { data: { cipherId: cipher.id } }); + const dialog = openIndividualVaultCollectionsDialog(this.dialogService, { + data: { cipherId: cipher.id }, + }); + const result = await lastValueFrom(dialog.closed); + + if (result === CollectionsDialogResult.Saved) { + this.refresh(); + } } async addCipher() { From 0c84f448060a6334b6998abf4f5f5e10e24f50db Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Wed, 31 Jul 2024 09:59:14 -0500 Subject: [PATCH 03/13] [PM-10124] Browser refresh update for autofill settings component returning invalid data type (#10289) --- .../src/autofill/popup/settings/autofill.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 5a7623f21c3..dcbd0fa5770 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -159,7 +159,7 @@ > @@ -206,7 +206,7 @@ @@ -222,7 +222,7 @@ (change)="saveDefaultUriMatch()" [(ngModel)]="defaultUriMatch" > - + {{ "defaultUriMatchDetectionDesc" | i18n }} From c9f4cffc75015003a6a62cc43a17d41243b6ef78 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 31 Jul 2024 12:35:17 -0400 Subject: [PATCH 04/13] remove margin from last field in each card of autofill settings (#10344) --- .../src/autofill/popup/settings/autofill.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index dcbd0fa5770..45deac00a27 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -87,7 +87,7 @@ /> {{ "showCardsInVaultView" | i18n }} - + {{ "enableAutoFillOnPageLoad" | i18n }} - + {{ "defaultAutoFillOnPageLoad" | i18n }} Date: Wed, 31 Jul 2024 12:35:47 -0400 Subject: [PATCH 05/13] section headers should use heading 6 styling (#10345) --- .../src/autofill/popup/settings/autofill.component.html | 8 ++++---- .../popup/settings/excluded-domains.component.html | 2 +- .../autofill/popup/settings/notifications.component.html | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 45deac00a27..3b419cf8f03 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -8,7 +8,7 @@
-

{{ "autofillSuggestionsSectionTitle" | i18n }}

+

{{ "autofillSuggestionsSectionTitle" | i18n }}

@@ -103,7 +103,7 @@
-

{{ "autofillKeyboardShortcutSectionTitle" | i18n }}

+

{{ "autofillKeyboardShortcutSectionTitle" | i18n }}

+ + + `, +}) +class TestCopyClickComponent { + @ViewChild("noToast") noToastButton: ElementRef; + @ViewChild("infoToast") infoToastButton: ElementRef; + @ViewChild("successToast") successToastButton: ElementRef; +} + +describe("CopyClickDirective", () => { + let fixture: ComponentFixture; + const copyToClipboard = jest.fn(); + const showToast = jest.fn(); + + beforeEach(async () => { + copyToClipboard.mockClear(); + showToast.mockClear(); + + await TestBed.configureTestingModule({ + declarations: [CopyClickDirective, TestCopyClickComponent], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: PlatformUtilsService, useValue: { copyToClipboard } }, + { provide: ToastService, useValue: { showToast } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestCopyClickComponent); + fixture.detectChanges(); + }); + + it("copies the the value for all variants of toasts ", () => { + const noToastButton = fixture.componentInstance.noToastButton.nativeElement; + + noToastButton.click(); + expect(copyToClipboard).toHaveBeenCalledWith("no toast shown"); + + const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement; + + infoToastButton.click(); + expect(copyToClipboard).toHaveBeenCalledWith("info toast shown"); + + const successToastButton = fixture.componentInstance.successToastButton.nativeElement; + + successToastButton.click(); + expect(copyToClipboard).toHaveBeenCalledWith("success toast shown"); + }); + + it("does not show a toast when showToast is not present", () => { + const noToastButton = fixture.componentInstance.noToastButton.nativeElement; + + noToastButton.click(); + expect(showToast).not.toHaveBeenCalled(); + }); + + it("shows a success toast when showToast is present", () => { + const successToastButton = fixture.componentInstance.successToastButton.nativeElement; + + successToastButton.click(); + expect(showToast).toHaveBeenCalledWith({ + message: "copySuccessful", + title: null, + variant: "success", + }); + }); + + it("shows the toast variant when set with showToast", () => { + const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement; + + infoToastButton.click(); + expect(showToast).toHaveBeenCalledWith({ + message: "copySuccessful", + title: null, + variant: "info", + }); + }); +}); diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/angular/src/directives/copy-click.directive.ts index cee2bdde4e8..0d764c95edb 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/angular/src/directives/copy-click.directive.ts @@ -1,14 +1,17 @@ -import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Directive, HostListener, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +import { ToastVariant } from "@bitwarden/components/src/toast/toast.component"; @Directive({ selector: "[appCopyClick]", }) export class CopyClickDirective { + private _showToast = false; + private toastVariant: ToastVariant = "success"; + constructor( private platformUtilsService: PlatformUtilsService, private toastService: ToastService, @@ -16,14 +19,36 @@ export class CopyClickDirective { ) {} @Input("appCopyClick") valueToCopy = ""; - @Input({ transform: coerceBooleanProperty }) showToast?: boolean; + + /** + * When set without a value, a success toast will be shown when the value is copied + * @example + * ```html + * + * ``` + * When set with a value, a toast with the specified variant will be shown when the value is copied + * + * @example + * ```html + * + * ``` + */ + @Input() set showToast(value: ToastVariant | "") { + // When the `showToast` is set without a value, an empty string will be passed + if (value === "") { + this._showToast = true; + } else { + this._showToast = true; + this.toastVariant = value; + } + } @HostListener("click") onClick() { this.platformUtilsService.copyToClipboard(this.valueToCopy); - if (this.showToast) { + if (this._showToast) { this.toastService.showToast({ - variant: "info", + variant: this.toastVariant, title: null, message: this.i18nService.t("copySuccessful"), });