From 1cc06fd3b9ffbb1bfbfff74f286d849212aeccd7 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 13 May 2025 14:29:23 -0500 Subject: [PATCH 01/13] [PM-20999] Styling corrections to Access Intelligence - Part 2 (#14552) --- apps/web/src/locales/en/messages.json | 15 +++++ .../app-table-row-scrollable.component.html | 2 +- .../risk-insights.component.html | 63 +++++++++++-------- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b5725d4eddf..cf2174cc1db 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -140,9 +140,15 @@ "atRiskMembersDescription": { "message": "These members are logging into applications with weak, exposed, or reused passwords." }, + "atRiskMembersDescriptionNone": { + "message": "These are no members logging into applications with weak, exposed, or reused passwords." + }, "atRiskApplicationsDescription": { "message": "These applications have weak, exposed, or reused passwords." }, + "atRiskApplicationsDescriptionNone": { + "message": "These are no applications with weak, exposed, or reused passwords." + }, "atRiskMembersDescriptionWithApp": { "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", "placeholders": { @@ -152,6 +158,15 @@ } } }, + "atRiskMembersDescriptionWithAppNone": { + "message": "There are no at risk members for $APPNAME$.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html index ff38ab5687e..10dbb179519 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html @@ -89,7 +89,7 @@ diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index fea0b32e959..f759e483bd0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -68,19 +68,24 @@ {{ - "atRiskMembersDescription" | i18n + (dataService.atRiskMemberDetails.length > 0 + ? "atRiskMembersDescription" + : "atRiskMembersDescriptionNone" + ) | i18n }} -
-
{{ "email" | i18n }}
-
- {{ "atRiskPasswords" | i18n }} -
-
- -
-
{{ member.email }}
-
{{ member.atRiskPasswordCount }}
+ +
+
{{ "email" | i18n }}
+
+ {{ "atRiskPasswords" | i18n }} +
+ +
+
{{ member.email }}
+
{{ member.atRiskPasswordCount }}
+
+
@@ -94,7 +99,10 @@
{{ - "atRiskMembersDescriptionWithApp" | i18n: dataService.appAtRiskMembers.applicationName + (dataService.appAtRiskMembers.members.length > 0 + ? "atRiskMembersDescriptionWithApp" + : "atRiskMembersDescriptionWithAppNone" + ) | i18n: dataService.appAtRiskMembers.applicationName }}
@@ -113,21 +121,26 @@ {{ - "atRiskApplicationsDescription" | i18n + (dataService.atRiskAppDetails.length > 0 + ? "atRiskApplicationsDescription" + : "atRiskApplicationsDescriptionNone" + ) | i18n }} -
-
- {{ "application" | i18n }} -
-
- {{ "atRiskPasswords" | i18n }} -
-
- -
-
{{ app.applicationName }}
-
{{ app.atRiskPasswordCount }}
+ +
+
+ {{ "application" | i18n }} +
+
+ {{ "atRiskPasswords" | i18n }} +
+ +
+
{{ app.applicationName }}
+
{{ app.atRiskPasswordCount }}
+
+
From bacd1fb999744fdc92b99ae5563333cb2336daef Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 13 May 2025 21:36:26 +0200 Subject: [PATCH 02/13] [PM-17407] [CL-665] Remove jQuery and Popper.js (#14621) Now that the last usages of ModalService is removed from the web portions we can finally remove jQuery and Popper.js. This PR also removes bootstrap js imports since it would drag in jQuery as a peer dependency. Note: Both dependencies still exists in the lockfile as they are peer dependencies of boostrap. --- .github/renovate.json5 | 3 -- apps/web/src/app/app.component.ts | 24 +---------- apps/web/src/app/core/core.module.ts | 7 ---- apps/web/src/app/core/modal.service.ts | 56 -------------------------- apps/web/src/main.ts | 4 -- bitwarden_license/bit-web/src/main.ts | 4 -- package-lock.json | 24 ++--------- package.json | 3 -- 8 files changed, 4 insertions(+), 121 deletions(-) delete mode 100644 apps/web/src/app/core/modal.service.ts diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e5cd47077fb..d0066ddd7ba 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -222,7 +222,6 @@ "@types/chrome", "@types/firefox-webext-browser", "@types/glob", - "@types/jquery", "@types/lowdb", "@types/node", "@types/node-forge", @@ -330,9 +329,7 @@ "autoprefixer", "bootstrap", "chromatic", - "jquery", "ngx-toastr", - "popper.js", "react", "react-dom", "remark-gfm", diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 55e2595e0f7..b94ce004313 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -3,25 +3,20 @@ import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { NavigationEnd, Router } from "@angular/router"; -import * as jq from "jquery"; +import { Router } from "@angular/router"; import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; -import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -29,11 +24,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; -import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KeyService, BiometricStateService } from "@bitwarden/key-management"; import { PolicyListService } from "./admin-console/core/policy-list.service"; @@ -69,8 +62,6 @@ export class AppComponent implements OnDestroy, OnInit { @Inject(DOCUMENT) private document: Document, private broadcasterService: BroadcasterService, private folderService: InternalFolderService, - private syncService: SyncService, - private passwordGenerationService: PasswordGenerationServiceAbstraction, private cipherService: CipherService, private authService: AuthService, private router: Router, @@ -85,17 +76,13 @@ export class AppComponent implements OnDestroy, OnInit { private notificationsService: NotificationsService, private stateService: StateService, private eventUploadService: EventUploadService, - private policyService: InternalPolicyService, protected policyListService: PolicyListService, - private keyConnectorService: KeyConnectorService, protected configService: ConfigService, private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, private organizationService: InternalOrganizationServiceAbstraction, private accountService: AccountService, - private apiService: ApiService, - private appIdService: AppIdService, private processReloadService: ProcessReloadServiceAbstraction, private deviceTrustToastService: DeviceTrustToastService, ) { @@ -247,15 +234,6 @@ export class AppComponent implements OnDestroy, OnInit { }); }); - this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => { - if (event instanceof NavigationEnd) { - const modals = Array.from(document.querySelectorAll(".modal")); - for (const modal of modals) { - (jq(modal) as any).modal("hide"); - } - } - }); - this.policyListService.addPolicies([ new TwoFactorAuthenticationPolicy(), new MasterPasswordPolicy(), diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 06a91895eb8..48e884f252c 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -26,7 +26,6 @@ import { WINDOW, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { RegistrationFinishService as RegistrationFinishServiceAbstraction, LoginComponentService, @@ -136,7 +135,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi import { EventService } from "./event.service"; import { InitService } from "./init.service"; import { ENV_URLS } from "./injection-tokens"; -import { ModalService } from "./modal.service"; import { RouterService } from "./router.service"; import { WebPlatformUtilsService } from "./web-platform-utils.service"; @@ -195,11 +193,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebPlatformUtilsService, useAngularDecorators: true, }), - safeProvider({ - provide: ModalServiceAbstraction, - useClass: ModalService, - useAngularDecorators: true, - }), safeProvider({ provide: FileDownloadService, useClass: WebFileDownloadService, diff --git a/apps/web/src/app/core/modal.service.ts b/apps/web/src/app/core/modal.service.ts deleted file mode 100644 index 14ea6044f36..00000000000 --- a/apps/web/src/app/core/modal.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable, Injector } from "@angular/core"; -import * as jq from "jquery"; -import { first } from "rxjs/operators"; - -import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; -import { ModalService as BaseModalService } from "@bitwarden/angular/services/modal.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -@Injectable() -export class ModalService extends BaseModalService { - el: any = null; - modalOpen = false; - - constructor( - injector: Injector, - private messagingService: MessagingService, - ) { - super(injector); - } - - protected setupHandlers(modalRef: ModalRef) { - modalRef.onCreated.pipe(first()).subscribe(() => { - const modals = Array.from(document.querySelectorAll(".modal")); - if (modals.length > 0) { - this.el = jq(modals[0]); - this.el.modal("show"); - - this.el.on("show.bs.modal", () => { - modalRef.show(); - this.messagingService.send("modalShow"); - }); - this.el.on("shown.bs.modal", () => { - modalRef.shown(); - this.messagingService.send("modalShown"); - if (!Utils.isMobileBrowser) { - this.el.find("*[appAutoFocus]").focus(); - } - }); - this.el.on("hide.bs.modal", () => { - this.messagingService.send("modalClose"); - }); - this.el.on("hidden.bs.modal", () => { - modalRef.closed(); - this.messagingService.send("modalClosed"); - }); - } - }); - - modalRef.onClose.pipe(first()).subscribe(() => { - if (this.el != null) { - this.el.modal("hide"); - } - }); - } -} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index b202a170d26..572d3968f3d 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -1,10 +1,6 @@ import { enableProdMode } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import "bootstrap"; -import "jquery"; -import "popper.js"; - import { AppModule } from "./app/app.module"; if (process.env.NODE_ENV === "production") { diff --git a/bitwarden_license/bit-web/src/main.ts b/bitwarden_license/bit-web/src/main.ts index b202a170d26..572d3968f3d 100644 --- a/bitwarden_license/bit-web/src/main.ts +++ b/bitwarden_license/bit-web/src/main.ts @@ -1,10 +1,6 @@ import { enableProdMode } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import "bootstrap"; -import "jquery"; -import "popper.js"; - import { AppModule } from "./app/app.module"; if (process.env.NODE_ENV === "production") { diff --git a/package-lock.json b/package-lock.json index d1378d63ec3..ddcdd1c9df9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "form-data": "4.0.1", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jquery": "3.7.1", "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", @@ -62,7 +61,6 @@ "open": "8.4.2", "papaparse": "5.5.2", "patch-package": "8.0.0", - "popper.js": "1.16.1", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", "qrious": "4.0.2", @@ -102,7 +100,6 @@ "@types/firefox-webext-browser": "120.0.4", "@types/inquirer": "8.2.10", "@types/jest": "29.5.12", - "@types/jquery": "3.5.32", "@types/jsdom": "21.1.7", "@types/koa": "2.15.0", "@types/koa__multer": "2.0.7", @@ -11723,16 +11720,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/jquery": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", - "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sizzle": "*" - } - }, "node_modules/@types/jsdom": { "version": "21.1.7", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", @@ -12099,13 +12086,6 @@ "@types/send": "*" } }, - "node_modules/@types/sizzle": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", - "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -25233,7 +25213,8 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -31666,6 +31647,7 @@ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" diff --git a/package.json b/package.json index 2993707313f..93361275494 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@types/firefox-webext-browser": "120.0.4", "@types/inquirer": "8.2.10", "@types/jest": "29.5.12", - "@types/jquery": "3.5.32", "@types/jsdom": "21.1.7", "@types/koa": "2.15.0", "@types/koa__multer": "2.0.7", @@ -181,7 +180,6 @@ "form-data": "4.0.1", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jquery": "3.7.1", "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", @@ -198,7 +196,6 @@ "open": "8.4.2", "papaparse": "5.5.2", "patch-package": "8.0.0", - "popper.js": "1.16.1", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", "qrious": "4.0.2", From d50db0d0dded04deb261901a9672b193779d5198 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 13 May 2025 16:08:43 -0400 Subject: [PATCH 03/13] [PM-21441] Defect - Notification bar sometimes gets cut off in fill dev (#14764) * PM-21441 * revert default value --- .../overlay-notifications-content.service.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts index c21aaa37dd4..a2e1d6e49a0 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts @@ -17,7 +17,7 @@ export class OverlayNotificationsContentService private notificationBarIframeElement: HTMLIFrameElement | null = null; private currentNotificationBarType: string | null = null; private removeTabFromNotificationQueueTypes = new Set(["add", "change"]); - private notificationRefreshFlag: boolean; + private notificationRefreshFlag: boolean = false; private notificationBarElementStyles: Partial = { height: "82px", width: "430px", @@ -57,6 +57,7 @@ export class OverlayNotificationsContentService void sendExtensionMessage("checkNotificationQueue"); void sendExtensionMessage("notificationRefreshFlagValue").then((notificationRefreshFlag) => { this.notificationRefreshFlag = !!notificationRefreshFlag; + this.setNotificationRefreshBarHeight(); }); } @@ -223,15 +224,31 @@ export class OverlayNotificationsContentService this.notificationBarElement.id = "bit-notification-bar"; setElementStyles(this.notificationBarElement, this.notificationBarElementStyles, true); - - if (this.notificationRefreshFlag) { - setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true); - } + this.setNotificationRefreshBarHeight(); this.notificationBarElement.appendChild(this.notificationBarIframeElement); } } + /** + * Sets the height of the notification bar based on the value of `notificationRefreshFlag`. + * If the flag is `true`, the bar is expanded to 400px and aligned right. + * If the flag is `false`, `null`, or `undefined`, it defaults to height of 82px. + * Skips if the notification bar element has not yet been created. + * + */ + private setNotificationRefreshBarHeight() { + const isNotificationV3 = !!this.notificationRefreshFlag; + + if (!this.notificationBarElement) { + return; + } + + if (isNotificationV3) { + setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true); + } + } + /** * Sets up the message listener for the initialization of the notification bar. * This will send the initialization data to the notification bar iframe. From 393926beece21a23037f055bb75175dd6ff718df Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 13 May 2025 16:10:33 -0400 Subject: [PATCH 04/13] PM-21605 Remove Login text from error notification (#14767) --- .../content/components/notification/confirmation/body.ts | 1 + .../components/notification/confirmation/message.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts index caea38718fd..8286202b498 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -48,6 +48,7 @@ export function NotificationConfirmationBody({ ? NotificationConfirmationMessage({ buttonAria, buttonText, + error, itemName, message: confirmationMessage, messageDetails, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index 2bf8caecfff..8fdda593382 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -8,6 +8,7 @@ import { spacing, themes, typography } from "../../constants/styles"; export type NotificationConfirmationMessageProps = { buttonAria?: string; buttonText?: string; + error?: string; itemName?: string; message?: string; messageDetails?: string; @@ -18,6 +19,7 @@ export type NotificationConfirmationMessageProps = { export function NotificationConfirmationMessage({ buttonAria, buttonText, + error, itemName, message, messageDetails, @@ -29,7 +31,11 @@ export function NotificationConfirmationMessage({ ${message || buttonText ? html`
- ${itemName} + ${!error && itemName + ? html` + ${itemName} + ` + : nothing} Date: Tue, 13 May 2025 16:49:41 -0400 Subject: [PATCH 05/13] [PM-21395] Vault Nudges Bugs (#14737) * updates to empty vault and has items nudges --- apps/browser/src/_locales/en/messages.json | 10 +++- .../vault-v2/vault-v2.component.html | 6 ++- .../components/vault-v2/vault-v2.component.ts | 11 +++- .../spotlight/spotlight.component.html | 8 ++- .../spotlight/spotlight.component.ts | 2 +- .../empty-vault-nudge.service.ts | 5 +- .../has-items-nudge.service.ts | 50 +++++++++++++------ 7 files changed, 69 insertions(+), 23 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index dcdfe7df4d6..fa300b4253b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5273,8 +5273,14 @@ "hasItemsVaultNudgeTitle": { "message": "Welcome to your vault!" }, - "hasItemsVaultNudgeBody": { - "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + "hasItemsVaultNudgeBodyOne": { + "message": "Autofill items for the current page" + }, + "hasItemsVaultNudgeBodyTwo": { + "message": "Favorite items for easy access" + }, + "hasItemsVaultNudgeBodyThree": { + "message": "Search your vault for something else" }, "newLoginNudgeTitle": { "message": "Save time with autofill" diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 894f27245b2..b46002d645e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -44,9 +44,13 @@
+
    +
  • {{ "hasItemsVaultNudgeBodyOne" | i18n }}
  • +
  • {{ "hasItemsVaultNudgeBodyTwo" | i18n }}
  • +
  • {{ "hasItemsVaultNudgeBodyThree" | i18n }}
  • +

{{ title }}

-

+

+
- + +

{{ "autofillOptions" | i18n }} @@ -38,4 +38,4 @@ - +

diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html index 485f8f79856..7fda078b066 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html @@ -1,4 +1,4 @@ - +

{{ getSectionHeading() }} @@ -71,4 +71,4 @@ > - +

diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index 0dc5e3f6ac0..98cc6489bbd 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -1,4 +1,8 @@ - +

{{ "customFields" | i18n }}

@@ -116,4 +120,4 @@ - +
diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html index 40a8954b05a..5fd3e08f22d 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html @@ -1,4 +1,4 @@ - +

{{ "itemDetails" | i18n }}

diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html index e31be492f93..585f11c2ffe 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html @@ -1,4 +1,4 @@ - +

{{ "loginCredentials" | i18n }} @@ -127,6 +127,6 @@ > - +

diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html index 4e1c0c5cfd9..b919ed69f0d 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html @@ -1,4 +1,4 @@ - +

{{ "typeSshKey" | i18n }} @@ -35,4 +35,4 @@ - +

diff --git a/libs/vault/src/cipher-view/additional-options/additional-options.component.html b/libs/vault/src/cipher-view/additional-options/additional-options.component.html index cc74d4e3a68..aa6d339dcd7 100644 --- a/libs/vault/src/cipher-view/additional-options/additional-options.component.html +++ b/libs/vault/src/cipher-view/additional-options/additional-options.component.html @@ -1,4 +1,4 @@ - +

{{ "additionalOptions" | i18n }}

@@ -18,4 +18,4 @@ > - +
diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html index a794946cb89..67ded3f8358 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html @@ -1,4 +1,4 @@ - +

{{ "attachments" | i18n }}

@@ -21,4 +21,4 @@ - +
diff --git a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html index aa3e05b9aab..22049b2a72e 100644 --- a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html +++ b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html @@ -1,4 +1,4 @@ - +

{{ "autofillOptions" | i18n }}

@@ -41,4 +41,4 @@ - +
diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.html b/libs/vault/src/cipher-view/card-details/card-details-view.component.html index ff61addd7db..9d2fa45ba9e 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.html +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.html @@ -1,4 +1,4 @@ - +

{{ setSectionTitle }}

@@ -93,4 +93,4 @@ > - +
diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html index 2492ed0cd81..7c60d35965f 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html @@ -1,4 +1,4 @@ - +

{{ "customFields" | i18n }}

@@ -115,4 +115,4 @@
- + diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html index 5ba535d0436..32bf1befb66 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html @@ -1,4 +1,4 @@ - +

{{ "itemDetails" | i18n }}

@@ -80,4 +80,4 @@ - +
diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index dc1168b7f01..256aec34b50 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -1,4 +1,4 @@ - +

{{ "loginCredentials" | i18n }}

@@ -164,4 +164,4 @@ > - +
diff --git a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html index 20390c0a285..f7c28ceb3f0 100644 --- a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html +++ b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html @@ -1,4 +1,4 @@ - +

{{ "typeSshKey" | i18n }}

@@ -66,4 +66,4 @@ > - +
diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html index d2154abd098..1b0a1f48f05 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html @@ -1,4 +1,4 @@ - +

{{ "personalDetails" | i18n }}

@@ -64,9 +64,9 @@ > - +
- +

{{ "identification" | i18n }}

@@ -153,9 +153,9 @@ > - +
- +

{{ "contactInfo" | i18n }}

@@ -212,4 +212,4 @@ > - +
From 3e0cc7ca7f9d1576c9122a66bff3e7e20de234ba Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 14 May 2025 09:26:47 -0500 Subject: [PATCH 07/13] [PM-18627] Remove customization settings popover (#14713) * chore: remove customization popover strings, refs PM-18627 * chore: delete new settings callout ts/html component, refs PM-18627 * chore: remove new customization code from vault-v2 component, refs PM-18627: :q * chore: delete vault-page service, refs PM-18627 * chore: add state migration to remove data, refs PM-18627 --- apps/browser/src/_locales/en/messages.json | 9 --- .../new-settings-callout.component.html | 29 ------- .../new-settings-callout.component.ts | 81 ------------------- .../components/vault-v2/vault-page.service.ts | 35 -------- .../vault-v2/vault-v2.component.html | 1 - .../components/vault-v2/vault-v2.component.ts | 5 -- libs/common/src/state-migrations/migrate.ts | 6 +- ...mization-options-callout-dismissed.spec.ts | 50 ++++++++++++ ...customization-options-callout-dismissed.ts | 23 ++++++ 9 files changed, 77 insertions(+), 162 deletions(-) delete mode 100644 apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html delete mode 100644 apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts delete mode 100644 apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts create mode 100644 libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index fa300b4253b..df35facff3c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2208,15 +2208,6 @@ "vaultTimeoutAction1": { "message": "Timeout action" }, - "newCustomizationOptionsCalloutTitle": { - "message": "New customization options" - }, - "newCustomizationOptionsCalloutContent": { - "message": "Customize your vault experience with quick copy actions, compact mode, and more!" - }, - "newCustomizationOptionsCalloutLink": { - "message": "View all Appearance settings" - }, "lock": { "message": "Lock", "description": "Verb form: to make secure or inaccessible by" diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html deleted file mode 100644 index 6cc60eed6d5..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - -
- {{ "newCustomizationOptionsCalloutContent" | i18n }} - - {{ "newCustomizationOptionsCalloutLink" | i18n }} - -
-
-
diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts deleted file mode 100644 index 713dc21c424..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { ButtonModule, PopoverModule } from "@bitwarden/components"; - -import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service"; -import { VaultPageService } from "../vault-page.service"; - -@Component({ - selector: "new-settings-callout", - templateUrl: "new-settings-callout.component.html", - standalone: true, - imports: [PopoverModule, JslibModule, CommonModule, ButtonModule], - providers: [VaultPageService], -}) -export class NewSettingsCalloutComponent implements OnInit, OnDestroy { - protected showNewCustomizationSettingsCallout = false; - protected activeUserId: UserId | null = null; - - constructor( - private accountService: AccountService, - private vaultProfileService: VaultProfileService, - private vaultPageService: VaultPageService, - private router: Router, - private logService: LogService, - private copyButtonService: VaultPopupCopyButtonsService, - private vaultSettingsService: VaultSettingsService, - ) {} - - async ngOnInit() { - this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - - const showQuickCopyActions = await firstValueFrom(this.copyButtonService.showQuickCopyActions$); - const clickItemsToAutofillVaultView = await firstValueFrom( - this.vaultSettingsService.clickItemsToAutofillVaultView$, - ); - - let profileCreatedDate: Date; - - try { - profileCreatedDate = await this.vaultProfileService.getProfileCreationDate(this.activeUserId); - } catch (e) { - this.logService.error("Error getting profile creation date", e); - // Default to before the cutoff date to ensure the callout is shown - profileCreatedDate = new Date("2024-12-24"); - } - - const hasCalloutBeenDismissed = await firstValueFrom( - this.vaultPageService.isCalloutDismissed(this.activeUserId), - ); - - this.showNewCustomizationSettingsCallout = - !showQuickCopyActions && - !clickItemsToAutofillVaultView && - !hasCalloutBeenDismissed && - profileCreatedDate < new Date("2024-12-25"); - } - - async goToAppearance() { - await this.router.navigate(["/appearance"]); - } - - async dismissCallout() { - if (this.activeUserId) { - await this.vaultPageService.dismissCallout(this.activeUserId); - } - } - - async ngOnDestroy() { - await this.dismissCallout(); - } -} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts deleted file mode 100644 index a7c52ed4c51..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { inject, Injectable } from "@angular/core"; -import { map, Observable } from "rxjs"; - -import { - BANNERS_DISMISSED_DISK, - StateProvider, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; - -export const NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY = new UserKeyDefinition( - BANNERS_DISMISSED_DISK, - "newCustomizationOptionsCalloutDismissed", - { - deserializer: (calloutDismissed) => calloutDismissed, - clearOn: [], // Do not clear dismissed callouts - }, -); - -@Injectable() -export class VaultPageService { - private stateProvider = inject(StateProvider); - - isCalloutDismissed(userId: UserId): Observable { - return this.stateProvider - .getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY) - .state$.pipe(map((dismissed) => !!dismissed)); - } - - async dismissCallout(userId: UserId): Promise { - await this.stateProvider - .getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY) - .update(() => true); - } -} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index b46002d645e..43a96fc616e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -107,5 +107,4 @@ >
- diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 58a6ba0000b..ec4f3939204 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -56,9 +56,7 @@ import { NewItemDropdownV2Component, NewItemInitialValues, } from "./new-item-dropdown/new-item-dropdown-v2.component"; -import { NewSettingsCalloutComponent } from "./new-settings-callout/new-settings-callout.component"; import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component"; -import { VaultPageService } from "./vault-page.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "."; @@ -90,12 +88,10 @@ enum VaultState { ScrollingModule, VaultHeaderV2Component, AtRiskPasswordCalloutComponent, - NewSettingsCalloutComponent, SpotlightComponent, RouterModule, TypographyModule, ], - providers: [VaultPageService], }) export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { @ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement; @@ -152,7 +148,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { protected noResultsIcon = Icons.NoResults; protected VaultStateEnum = VaultState; - protected showNewCustomizationSettingsCallout = false; constructor( private vaultPopupItemsService: VaultPopupItemsService, diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index b409f52d936..bea79963b0b 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -69,12 +69,13 @@ import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date"; import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed"; +import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 70; +export const CURRENT_VERSION = 71; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -146,7 +147,8 @@ export function createMigrationBuilder() { .with(RemoveUnassignedItemsBannerDismissed, 66, 67) .with(MoveLastSyncDate, 67, 68) .with(MigrateIncorrectFolderKey, 68, 69) - .with(RemoveAcBannersDismissed, 69, CURRENT_VERSION); + .with(RemoveAcBannersDismissed, 69, 70) + .with(RemoveNewCustomizationOptionsCalloutDismissed, 70, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts new file mode 100644 index 00000000000..f2c83346a62 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts @@ -0,0 +1,50 @@ +import { runMigrator } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { RemoveNewCustomizationOptionsCalloutDismissed } from "./71-remove-new-customization-options-callout-dismissed"; + +describe("RemoveNewCustomizationOptionsCalloutDismissed", () => { + const sut = new RemoveNewCustomizationOptionsCalloutDismissed(70, 71); + + describe("migrate", () => { + it("deletes new customization options callout dismissed from all users", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + user_user1_bannersDismissed_newCustomizationOptionsCalloutDismissed: true, + user_user2_bannersDismissed_newCustomizationOptionsCalloutDismissed: true, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + }); + }); + }); + + describe("rollback", () => { + it("is irreversible", async () => { + await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts new file mode 100644 index 00000000000..7260048daf6 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts @@ -0,0 +1,23 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +export const SHOW_CALLOUT_KEY: KeyDefinitionLike = { + key: "newCustomizationOptionsCalloutDismissed", + stateDefinition: { name: "bannersDismissed" }, +}; + +export class RemoveNewCustomizationOptionsCalloutDismissed extends Migrator<70, 71> { + async migrate(helper: MigrationHelper): Promise { + await Promise.all( + (await helper.getAccounts()).map(async ({ userId }) => { + if (helper.getFromUser(userId, SHOW_CALLOUT_KEY) != null) { + await helper.removeFromUser(userId, SHOW_CALLOUT_KEY); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} From ad3121f5359900c16c172f3a33af31a7681a4647 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Wed, 14 May 2025 10:30:01 -0400 Subject: [PATCH 08/13] [PM-12423] Migrate Cipher Decryption to Use SDK (#14206) * Created mappings for client domain object to SDK * Add abstract decrypt observable * Added todo for future consideration * Added implementation to cipher service * Added adapter and unit tests * Created cipher encryption abstraction and service * Register cipher encryption service * Added tests for the cipher encryption service * changed signature * Updated feature flag name * added new function to be used for decrypting ciphers * Added new encryptedKey field * added new function to be used for decrypting ciphers * Manually set fields * Added encrypted key in attachment view * Fixed test * Updated references to use decrypt with feature flag * Added dependency * updated package.json * lint fix * fixed tests * Fixed small mapping issues * Fixed test * Added function to decrypt fido2 key value * Added function to decrypt fido2 key value and updated test * updated to use sdk function without prociding the key * updated localdata sdk type change * decrypt attachment content using sdk * Fixed dependency issues * updated package.json * Refactored service to handle getting decrypted buffer using the legacy and sdk implementations * updated services and component to use refactored version * Updated decryptCiphersWithSdk to use decryptManyLegacy for batch decryption, ensuring the SDK is only called once per batch * Fixed merge conflicts * Fixed merge conflicts * Fixed merge conflicts * Fixed lint issues * Moved getDecryptedAttachmentBuffer to cipher service * Moved getDecryptedAttachmentBuffer to cipher service * ensure CipherView properties are null instead of undefined * Fixed test * ensure AttachmentView properties are null instead of undefined * Linked ticket in comment * removed unused orgKey --- .../background/notification.background.ts | 4 +- .../autofill/popup/fido2/fido2.component.ts | 8 +- .../browser/src/background/main.background.ts | 9 + .../assign-collections.component.ts | 7 +- .../open-attachments.component.spec.ts | 1 + .../open-attachments.component.ts | 4 +- .../vault-password-history-v2.component.ts | 4 +- .../view-v2/view-v2.component.spec.ts | 1 + .../vault-v2/view-v2/view-v2.component.ts | 4 +- .../admin-console/commands/share.command.ts | 8 +- apps/cli/src/commands/edit.command.ts | 15 +- apps/cli/src/commands/get.command.ts | 4 +- .../service-container/service-container.ts | 9 + apps/cli/src/vault/create.command.ts | 8 +- .../services/desktop-autofill.service.ts | 4 +- .../encrypted-message-handler.service.ts | 4 +- .../vault/app/vault/attachments.component.ts | 3 + .../src/vault/app/vault/view.component.ts | 3 +- .../vault-item-dialog.component.ts | 8 +- .../components/collections.component.ts | 4 +- .../angular/src/components/share.component.ts | 8 +- .../src/services/jslib-services.module.ts | 10 + .../vault/components/add-edit.component.ts | 4 +- .../vault/components/attachments.component.ts | 49 ++- .../components/password-history.component.ts | 4 +- .../src/vault/components/view.component.ts | 23 +- libs/common/spec/utils.ts | 14 + libs/common/src/enums/feature-flag.enum.ts | 2 + .../fido2/fido2-authenticator.service.spec.ts | 8 + .../fido2/fido2-authenticator.service.ts | 4 +- .../abstractions/cipher-encryption.service.ts | 60 ++++ .../src/vault/abstractions/cipher.service.ts | 25 ++ .../models/api/cipher-permissions.api.ts | 17 + .../src/vault/models/data/cipher.data.ts | 2 +- .../vault/models/domain/attachment.spec.ts | 17 + .../src/vault/models/domain/attachment.ts | 18 + .../src/vault/models/domain/card.spec.ts | 17 + libs/common/src/vault/models/domain/card.ts | 18 + .../src/vault/models/domain/cipher.spec.ts | 167 ++++++++- libs/common/src/vault/models/domain/cipher.ts | 70 ++++ .../models/domain/fido2-credential.spec.ts | 39 ++ .../vault/models/domain/fido2-credential.ts | 25 ++ .../src/vault/models/domain/field.spec.ts | 24 +- libs/common/src/vault/models/domain/field.ts | 17 + .../src/vault/models/domain/identity.spec.ts | 28 ++ .../src/vault/models/domain/identity.ts | 30 ++ .../src/vault/models/domain/login-uri.spec.ts | 15 + .../src/vault/models/domain/login-uri.ts | 15 + .../src/vault/models/domain/login.spec.ts | 48 +++ libs/common/src/vault/models/domain/login.ts | 19 + .../src/vault/models/domain/password.spec.ts | 13 + .../src/vault/models/domain/password.ts | 14 + .../vault/models/domain/secure-note.spec.ts | 13 + .../src/vault/models/domain/secure-note.ts | 13 + .../src/vault/models/domain/ssh-key.spec.ts | 13 + .../common/src/vault/models/domain/ssh-key.ts | 15 + .../vault/models/view/attachment.view.spec.ts | 55 +++ .../src/vault/models/view/attachment.view.ts | 40 +++ .../common/src/vault/models/view/card.view.ts | 13 + .../src/vault/models/view/cipher.view.spec.ts | 132 ++++++- .../src/vault/models/view/cipher.view.ts | 68 +++- .../models/view/fido2-credential.view.ts | 27 ++ .../src/vault/models/view/field.view.ts | 19 + .../src/vault/models/view/identity.view.ts | 13 + .../vault/models/view/login-uri-view.spec.ts | 22 ++ .../src/vault/models/view/login-uri.view.ts | 17 + .../src/vault/models/view/login.view.spec.ts | 35 +- .../src/vault/models/view/login.view.ts | 25 ++ .../models/view/password-history.view.spec.ts | 23 ++ .../models/view/password-history.view.ts | 17 + .../src/vault/models/view/secure-note.view.ts | 13 + .../src/vault/models/view/ssh-key.view.ts | 17 + .../src/vault/services/cipher.service.spec.ts | 87 ++++- .../src/vault/services/cipher.service.ts | 89 ++++- .../default-cipher-encryption.service.spec.ts | 334 ++++++++++++++++++ .../default-cipher-encryption.service.ts | 190 ++++++++++ .../bitwarden/bitwarden-json-importer.ts | 4 +- .../individual-vault-export.service.spec.ts | 15 +- .../individual-vault-export.service.ts | 36 +- .../src/services/org-vault-export.service.ts | 9 +- .../cipher-attachments.component.spec.ts | 1 + .../cipher-attachments.component.ts | 8 +- .../services/default-cipher-form.service.ts | 14 +- .../download-attachment.component.spec.ts | 35 +- .../download-attachment.component.ts | 41 +-- 85 files changed, 2171 insertions(+), 218 deletions(-) create mode 100644 libs/common/src/vault/abstractions/cipher-encryption.service.ts create mode 100644 libs/common/src/vault/services/default-cipher-encryption.service.spec.ts create mode 100644 libs/common/src/vault/services/default-cipher-encryption.service.ts diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 52920ec67a8..a73141b7e4d 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -894,9 +894,7 @@ export default class NotificationBackground { private async getDecryptedCipherById(cipherId: string, userId: UserId) { const cipher = await this.cipherService.get(cipherId, userId); if (cipher != null && cipher.type === CipherType.Login) { - return await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId), - ); + return await this.cipherService.decrypt(cipher, userId); } return null; } diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index 0471d460fd5..6b7d9120195 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -216,9 +216,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = await Promise.all( message.cipherIds.map(async (cipherId) => { const cipher = await this.cipherService.get(cipherId, activeUserId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + return this.cipherService.decrypt(cipher, activeUserId); }), ); @@ -237,9 +235,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = await Promise.all( message.existingCipherIds.map(async (cipherId) => { const cipher = await this.cipherService.get(cipherId, activeUserId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + return this.cipherService.decrypt(cipher, activeUserId); }), ); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 85a9cd27c57..a724f857cd1 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -183,6 +183,7 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -199,6 +200,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -408,6 +410,7 @@ export default class MainBackground { endUserNotificationService: EndUserNotificationService; inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; + cipherEncryptionService: CipherEncryptionService; ipcContentScriptManagerService: IpcContentScriptManagerService; ipcService: IpcService; @@ -856,6 +859,11 @@ export default class MainBackground { this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService); + this.cipherEncryptionService = new DefaultCipherEncryptionService( + this.sdkService, + this.logService, + ); + this.cipherService = new CipherService( this.keyService, this.domainSettingsService, @@ -871,6 +879,7 @@ export default class MainBackground { this.stateProvider, this.accountService, this.logService, + this.cipherEncryptionService, ); this.folderService = new FolderService( this.keyService, diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts index 27f3b7e5e18..7052be5ea62 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -11,7 +11,6 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -66,11 +65,7 @@ export class AssignCollections { route.queryParams.pipe( switchMap(async ({ cipherId }) => { const cipherDomain = await this.cipherService.get(cipherId, userId); - const key: UserKey | OrgKey = await this.cipherService.getKeyForCipherKeyDecryption( - cipherDomain, - userId, - ); - return cipherDomain.decrypt(key); + return await this.cipherService.decrypt(cipherDomain, userId); }), ), ), diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index 66d9096cd5c..ec5c93feb9e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -81,6 +81,7 @@ describe("OpenAttachmentsComponent", () => { useValue: { get: getCipher, getKeyForCipherKeyDecryption: () => Promise.resolve(null), + decrypt: jest.fn().mockResolvedValue(cipherView), }, }, { diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index 1bc7e22e6d5..9189ea51313 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -81,9 +81,7 @@ export class OpenAttachmentsComponent implements OnInit { this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); - const cipher = await cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), - ); + const cipher = await this.cipherService.decrypt(cipherDomain, activeUserId); if (!cipher.organizationId) { this.cipherIsAPartOfFreeOrg = false; diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts index 5d315775b10..d0eef20f044 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts @@ -69,8 +69,6 @@ export class PasswordHistoryV2Component implements OnInit { const activeUserId = activeAccount.id as UserId; const cipher = await this.cipherService.get(cipherId, activeUserId); - this.cipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(cipher, activeUserId); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 44874221a59..3222f39a162 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -82,6 +82,7 @@ describe("ViewV2Component", () => { getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}), deleteWithServer: jest.fn().mockResolvedValue(undefined), softDeleteWithServer: jest.fn().mockResolvedValue(undefined), + decrypt: jest.fn().mockResolvedValue(mockCipher), }; beforeEach(async () => { diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index a834314560b..0a71caf5aee 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -203,9 +203,7 @@ export class ViewV2Component { async getCipherData(id: string, userId: UserId) { const cipher = await this.cipherService.get(id, userId); - return await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId), - ); + return await this.cipherService.decrypt(cipher, userId); } async editCipher() { diff --git a/apps/cli/src/admin-console/commands/share.command.ts b/apps/cli/src/admin-console/commands/share.command.ts index 6d9e6c8b6c0..540bc2659c9 100644 --- a/apps/cli/src/admin-console/commands/share.command.ts +++ b/apps/cli/src/admin-console/commands/share.command.ts @@ -59,15 +59,11 @@ export class ShareCommand { return Response.badRequest("This item already belongs to an organization."); } - const cipherView = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const cipherView = await this.cipherService.decrypt(cipher, activeUserId); try { await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId); const updatedCipher = await this.cipherService.get(cipher.id, activeUserId); - const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 2d4a854135d..4dcf805661d 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -90,9 +90,7 @@ export class EditCommand { return Response.notFound(); } - let cipherView = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + let cipherView = await this.cipherService.decrypt(cipher, activeUserId); if (cipherView.isDeleted) { return Response.badRequest("You may not edit a deleted item. Use the restore command first."); } @@ -100,9 +98,7 @@ export class EditCommand { const encCipher = await this.cipherService.encrypt(cipherView, activeUserId); try { const updatedCipher = await this.cipherService.updateWithServer(encCipher); - const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { @@ -132,12 +128,7 @@ export class EditCommand { cipher, activeUserId, ); - const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption( - updatedCipher, - await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)), - ), - ); + const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 1bdbd051585..c3ba6044f8a 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -116,9 +116,7 @@ export class GetCommand extends DownloadCommand { if (Utils.isGuid(id)) { const cipher = await this.cipherService.get(id, activeUserId); if (cipher != null) { - decCipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + decCipher = await this.cipherService.decrypt(cipher, activeUserId); } } else if (id.trim() !== "") { let ciphers = await this.cipherService.getAllDecrypted(activeUserId); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index fe2f506f229..cdf6c4bbfda 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -139,12 +139,14 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherAuthorizationService, DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -284,6 +286,7 @@ export class ServiceContainer { ssoUrlService: SsoUrlService; masterPasswordApiService: MasterPasswordApiServiceAbstraction; bulkEncryptService: FallbackBulkEncryptService; + cipherEncryptionService: CipherEncryptionService; constructor() { let p = null; @@ -679,6 +682,11 @@ export class ServiceContainer { this.accountService, ); + this.cipherEncryptionService = new DefaultCipherEncryptionService( + this.sdkService, + this.logService, + ); + this.cipherService = new CipherService( this.keyService, this.domainSettingsService, @@ -694,6 +702,7 @@ export class ServiceContainer { this.stateProvider, this.accountService, this.logService, + this.cipherEncryptionService, ); this.folderService = new FolderService( diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 5b34d2cb507..b1536e23748 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -93,9 +93,7 @@ export class CreateCommand { const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId); try { const newCipher = await this.cipherService.createWithServer(cipher); - const decCipher = await newCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(newCipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(newCipher, activeUserId); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { @@ -162,9 +160,7 @@ export class CreateCommand { new Uint8Array(fileBuf).buffer, activeUserId, ); - const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); return Response.success(new CipherResponse(decCipher)); } catch (e) { return Response.error(e); diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index e88e16c2ffc..d6dddf3b23f 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -199,9 +199,7 @@ export class DesktopAutofillService implements OnDestroy { return; } - const decrypted = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const decrypted = await this.cipherService.decrypt(cipher, activeUserId); const fido2Credential = decrypted.login.fido2Credentials?.[0]; if (!fido2Credential) { diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index 591ff6fa8cf..37a8114c1d1 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -207,9 +207,7 @@ export class EncryptedMessageHandlerService { return { status: "failure" }; } - const cipherView = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const cipherView = await this.cipherService.decrypt(cipher, activeUserId); cipherView.name = credentialUpdatePayload.name; cipherView.login.password = credentialUpdatePayload.password; cipherView.login.username = credentialUpdatePayload.userName; diff --git a/apps/desktop/src/vault/app/vault/attachments.component.ts b/apps/desktop/src/vault/app/vault/attachments.component.ts index ea4f49b8431..a2cea5f2722 100644 --- a/apps/desktop/src/vault/app/vault/attachments.component.ts +++ b/apps/desktop/src/vault/app/vault/attachments.component.ts @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -33,6 +34,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, toastService: ToastService, + configService: ConfigService, ) { super( cipherService, @@ -49,6 +51,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { billingAccountProfileStateService, accountService, toastService, + configService, ); } } diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index e5f677cbca6..e74b07445da 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -72,7 +72,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro accountService: AccountService, toastService: ToastService, cipherAuthorizationService: CipherAuthorizationService, - private configService: ConfigService, + configService: ConfigService, ) { super( cipherService, @@ -100,6 +100,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro billingAccountProfileStateService, toastService, cipherAuthorizationService, + configService, ); } diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 10c35f861b9..aa457e97093 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -481,9 +481,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { activeUserId, ); - updatedCipherView = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), - ); + updatedCipherView = await this.cipherService.decrypt(updatedCipher, activeUserId); } this.cipherFormComponent.patchCipher((currentCipher) => { @@ -520,9 +518,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return; } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - return await config.originalCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId), - ); + return await this.cipherService.decrypt(config.originalCipher, activeUserId); } private updateTitle() { diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 5f39966468f..8ae90705f92 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -50,9 +50,7 @@ export class CollectionsComponent implements OnInit { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.cipherDomain = await this.loadCipher(activeUserId); this.collectionIds = this.loadCipherCollections(); - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId); this.collections = await this.loadCollections(); this.collections.forEach((c) => ((c as any).checked = false)); diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index e785441b8e4..198cc7dc3a5 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -76,9 +76,7 @@ export class ShareComponent implements OnInit, OnDestroy { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); - this.cipher = await cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(cipherDomain, activeUserId); } filterCollections() { @@ -105,9 +103,7 @@ export class ShareComponent implements OnInit, OnDestroy { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); - const cipherView = await cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), - ); + const cipherView = await this.cipherService.decrypt(cipherDomain, activeUserId); const orgs = await firstValueFrom(this.organizations$); const orgName = orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3ffca776034..920d35a1017 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -263,6 +263,7 @@ import { InternalSendService, SendService as SendServiceAbstraction, } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -281,6 +282,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -509,6 +511,7 @@ const safeProviders: SafeProvider[] = [ stateProvider: StateProvider, accountService: AccountServiceAbstraction, logService: LogService, + cipherEncryptionService: CipherEncryptionService, ) => new CipherService( keyService, @@ -525,6 +528,7 @@ const safeProviders: SafeProvider[] = [ stateProvider, accountService, logService, + cipherEncryptionService, ), deps: [ KeyService, @@ -541,6 +545,7 @@ const safeProviders: SafeProvider[] = [ StateProvider, AccountServiceAbstraction, LogService, + CipherEncryptionService, ], }), safeProvider({ @@ -1528,6 +1533,11 @@ const safeProviders: SafeProvider[] = [ useClass: MasterPasswordApiService, deps: [ApiServiceAbstraction, LogService], }), + safeProvider({ + provide: CipherEncryptionService, + useClass: DefaultCipherEncryptionService, + deps: [SdkService, LogService], + }), ]; @NgModule({ diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index b9defa8383d..b04adc1fdfb 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -269,9 +269,7 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.cipher == null) { if (this.editMode) { const cipher = await this.loadCipher(activeUserId); - this.cipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(cipher, activeUserId); // Adjust Cipher Name if Cloning if (this.cloneMode) { diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index 9e9450c587e..e4b01d3aac1 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -9,13 +9,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { UserId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -56,6 +56,7 @@ export class AttachmentsComponent implements OnInit { protected billingAccountProfileStateService: BillingAccountProfileStateService, protected accountService: AccountService, protected toastService: ToastService, + protected configService: ConfigService, ) {} async ngOnInit() { @@ -88,9 +89,7 @@ export class AttachmentsComponent implements OnInit { const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.formPromise = this.saveCipherAttachment(files[0], activeUserId); this.cipherDomain = await this.formPromise; - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId); this.toastService.showToast({ variant: "success", title: null, @@ -130,9 +129,7 @@ export class AttachmentsComponent implements OnInit { const updatedCipher = await this.deletePromises[attachment.id]; const cipher = new Cipher(updatedCipher); - this.cipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(cipher, activeUserId); this.toastService.showToast({ variant: "success", @@ -197,12 +194,14 @@ export class AttachmentsComponent implements OnInit { } try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = - attachment.key != null - ? attachment.key - : await this.keyService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.encryptService.decryptFileData(encBuf, key); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + this.cipherDomain.id as CipherId, + attachment, + response, + activeUserId, + ); + this.fileDownloadService.download({ fileName: attachment.fileName, blobData: decBuf, @@ -228,9 +227,7 @@ export class AttachmentsComponent implements OnInit { protected async init() { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.cipherDomain = await this.loadCipher(activeUserId); - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId); const canAccessPremium = await firstValueFrom( this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), @@ -276,15 +273,17 @@ export class AttachmentsComponent implements OnInit { try { // 2. Resave - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = - attachment.key != null - ? attachment.key - : await this.keyService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.encryptService.decryptFileData(encBuf, key); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getUserId), ); + + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + this.cipherDomain.id as CipherId, + attachment, + response, + activeUserId, + ); + this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer( this.cipherDomain, attachment.fileName, @@ -292,9 +291,7 @@ export class AttachmentsComponent implements OnInit { activeUserId, admin, ); - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId); // 3. Delete old this.deletePromises[attachment.id] = this.deleteCipherAttachment( diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 4df9f4bd24d..acb89b82191 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -42,9 +42,7 @@ export class PasswordHistoryComponent implements OnInit { protected async init() { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.cipherService.get(this.cipherId, activeUserId); - const decCipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(cipher, activeUserId); this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; } } diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 9d5a8fe9e62..8915cb6b671 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -34,13 +34,13 @@ import { EventType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -137,6 +137,7 @@ export class ViewComponent implements OnDestroy, OnInit { private billingAccountProfileStateService: BillingAccountProfileStateService, protected toastService: ToastService, private cipherAuthorizationService: CipherAuthorizationService, + protected configService: ConfigService, ) {} ngOnInit() { @@ -458,19 +459,19 @@ export class ViewComponent implements OnDestroy, OnInit { } try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = - attachment.key != null - ? attachment.key - : await this.keyService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.encryptService.decryptFileData(encBuf, key); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + this.cipher.id as CipherId, + attachment, + response, + activeUserId, + ); + this.fileDownloadService.download({ fileName: attachment.fileName, blobData: decBuf, }); - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + } catch { this.toastService.showToast({ variant: "error", title: null, diff --git a/libs/common/spec/utils.ts b/libs/common/spec/utils.ts index 51db65d0ce0..2b9b2567895 100644 --- a/libs/common/spec/utils.ts +++ b/libs/common/spec/utils.ts @@ -64,6 +64,20 @@ export function makeSymmetricCryptoKey( */ export const mockFromJson = (stub: any) => (stub + "_fromJSON") as any; +/** + * Use to mock a return value of a static fromSdk method. + */ +export const mockFromSdk = (stub: any) => { + if (typeof stub === "object") { + return { + ...stub, + __fromSdk: true, + }; + } + + return `${stub}_fromSdk`; +}; + /** * Tracks the emissions of the given observable. * diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index ddc75eb0d66..d349703bddf 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -57,6 +57,7 @@ export enum FeatureFlag { PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge", PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", SecurityTasks = "security-tasks", + PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk", CipherKeyEncryption = "cipher-key-encryption", PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", EndUserNotifications = "pm-10609-end-user-notifications", @@ -111,6 +112,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, [FeatureFlag.EndUserNotifications]: FALSE, + [FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE, /* Auth */ [FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE, diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts index 3ea86a1f504..5c377e1a980 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts @@ -152,6 +152,7 @@ describe("FidoAuthenticatorService", () => { id === excludedCipher.id ? ({ decrypt: () => excludedCipher } as any) : undefined, ); cipherService.getAllDecrypted.mockResolvedValue([excludedCipher]); + cipherService.decrypt.mockResolvedValue(excludedCipher); }); /** @@ -220,6 +221,7 @@ describe("FidoAuthenticatorService", () => { id === existingCipher.id ? ({ decrypt: () => existingCipher } as any) : undefined, ); cipherService.getAllDecrypted.mockResolvedValue([existingCipher]); + cipherService.decrypt.mockResolvedValue(existingCipher); }); /** @@ -306,6 +308,11 @@ describe("FidoAuthenticatorService", () => { const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password }; cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher); + cipherService.decrypt.mockResolvedValue({ + ...existingCipher, + reprompt: CipherRepromptType.Password, + } as unknown as CipherView); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); @@ -347,6 +354,7 @@ describe("FidoAuthenticatorService", () => { cipherId === cipher.id ? ({ decrypt: () => cipher } as any) : undefined, ); cipherService.getAllDecrypted.mockResolvedValue([await cipher]); + cipherService.decrypt.mockResolvedValue(cipher); cipherService.encrypt.mockImplementation(async (cipher) => { cipher.login.fido2Credentials[0].credentialId = credentialId; // Replace id for testability return {} as any; diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 76bd19b2876..a605e466338 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -151,9 +151,7 @@ export class Fido2AuthenticatorService ); const encrypted = await this.cipherService.get(cipherId, activeUserId); - cipher = await encrypted.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId), - ); + cipher = await this.cipherService.decrypt(encrypted, activeUserId); if ( !userVerified && diff --git a/libs/common/src/vault/abstractions/cipher-encryption.service.ts b/libs/common/src/vault/abstractions/cipher-encryption.service.ts new file mode 100644 index 00000000000..6b2a8e8943e --- /dev/null +++ b/libs/common/src/vault/abstractions/cipher-encryption.service.ts @@ -0,0 +1,60 @@ +import { CipherListView } from "@bitwarden/sdk-internal"; + +import { UserId } from "../../types/guid"; +import { Cipher } from "../models/domain/cipher"; +import { AttachmentView } from "../models/view/attachment.view"; +import { CipherView } from "../models/view/cipher.view"; + +/** + * Service responsible for encrypting and decrypting ciphers. + */ +export abstract class CipherEncryptionService { + /** + * Decrypts a cipher using the SDK for the given userId. + * + * @param cipher The encrypted cipher object + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to the decrypted cipher view + */ + abstract decrypt(cipher: Cipher, userId: UserId): Promise; + /** + * Decrypts many ciphers using the SDK for the given userId. + * + * For bulk decryption, prefer using `decryptMany`, which returns a more efficient + * `CipherListView` object. + * + * @param ciphers The encrypted cipher objects + * @param userId The user ID whose key will be used for decryption + * + * @deprecated Use `decryptMany` for bulk decryption instead. + * + * @returns A promise that resolves to an array of decrypted cipher views + */ + abstract decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise; + /** + * Decrypts many ciphers using the SDK for the given userId. + * + * @param ciphers The encrypted cipher objects + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to an array of decrypted cipher list views + */ + abstract decryptMany(ciphers: Cipher[], userId: UserId): Promise; + /** + * Decrypts an attachment's content from a response object. + * + * @param cipher The encrypted cipher object that owns the attachment + * @param attachment The attachment view object + * @param encryptedContent The encrypted content of the attachment + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to the decrypted content + */ + abstract decryptAttachmentContent( + cipher: Cipher, + attachment: AttachmentView, + encryptedContent: Uint8Array, + userId: UserId, + ): Promise; +} diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index b12488a5e03..a67dfcef8b9 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -14,6 +14,7 @@ import { LocalData } from "../models/data/local.data"; import { Cipher } from "../models/domain/cipher"; import { Field } from "../models/domain/field"; import { CipherWithIdRequest } from "../models/request/cipher-with-id.request"; +import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; import { FieldView } from "../models/view/field.view"; import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; @@ -215,4 +216,28 @@ export abstract class CipherService implements UserKeyRotationDataProvider; abstract getNextCardCipher(userId: UserId): Promise; abstract getNextIdentityCipher(userId: UserId): Promise; + + /** + * Decrypts a cipher using either the SDK or the legacy method based on the feature flag. + * @param cipher The cipher to decrypt. + * @param userId The user ID to use for decryption. + * @returns A promise that resolves to the decrypted cipher view. + */ + abstract decrypt(cipher: Cipher, userId: UserId): Promise; + /** + * Decrypts an attachment's content from a response object. + * + * @param cipherId The ID of the cipher that owns the attachment + * @param attachment The attachment view object + * @param response The response object containing the encrypted content + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to the decrypted content + */ + abstract getDecryptedAttachmentBuffer( + cipherId: CipherId, + attachment: AttachmentView, + response: Response, + userId: UserId, + ): Promise; } diff --git a/libs/common/src/vault/models/api/cipher-permissions.api.ts b/libs/common/src/vault/models/api/cipher-permissions.api.ts index 4df7f891e26..b7341d39b1d 100644 --- a/libs/common/src/vault/models/api/cipher-permissions.api.ts +++ b/libs/common/src/vault/models/api/cipher-permissions.api.ts @@ -1,5 +1,7 @@ import { Jsonify } from "type-fest"; +import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-internal"; + import { BaseResponse } from "../../../models/response/base.response"; export class CipherPermissionsApi extends BaseResponse { @@ -18,4 +20,19 @@ export class CipherPermissionsApi extends BaseResponse { static fromJSON(obj: Jsonify) { return Object.assign(new CipherPermissionsApi(), obj); } + + /** + * Converts the SDK CipherPermissionsApi to a CipherPermissionsApi. + */ + static fromSdkCipherPermissions(obj: SdkCipherPermissions): CipherPermissionsApi | undefined { + if (!obj) { + return undefined; + } + + const permissions = new CipherPermissionsApi(); + permissions.delete = obj.delete; + permissions.restore = obj.restore; + + return permissions; + } } diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index ee5e5b3e72b..1be70283fb3 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -39,7 +39,7 @@ export class CipherData { passwordHistory?: PasswordHistoryData[]; collectionIds?: string[]; creationDate: string; - deletedDate: string; + deletedDate: string | null; reprompt: CipherRepromptType; key: string; diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 40d7ea7f05c..eab67320679 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -153,4 +153,21 @@ describe("Attachment", () => { expect(Attachment.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkAttachment", () => { + it("should map to SDK Attachment", () => { + const attachment = new Attachment(data); + + const sdkAttachment = attachment.toSdkAttachment(); + + expect(sdkAttachment).toEqual({ + id: "id", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "fileName", + key: "key", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 15ce06e0881..4339f31a2e1 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal"; + import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; @@ -113,4 +115,20 @@ export class Attachment extends Domain { fileName, }); } + + /** + * Maps to SDK Attachment + * + * @returns {SdkAttachment} - The SDK Attachment object + */ + toSdkAttachment(): SdkAttachment { + return { + id: this.id, + url: this.url, + size: this.size, + sizeName: this.sizeName, + fileName: this.fileName?.toJSON(), + key: this.key?.toJSON(), + }; + } } diff --git a/libs/common/src/vault/models/domain/card.spec.ts b/libs/common/src/vault/models/domain/card.spec.ts index a7011966d94..19546ddcb05 100644 --- a/libs/common/src/vault/models/domain/card.spec.ts +++ b/libs/common/src/vault/models/domain/card.spec.ts @@ -99,4 +99,21 @@ describe("Card", () => { expect(Card.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkCard", () => { + it("should map to SDK Card", () => { + const card = new Card(data); + + const sdkCard = card.toSdkCard(); + + expect(sdkCard).toEqual({ + cardholderName: "encHolder", + brand: "encBrand", + number: "encNumber", + expMonth: "encMonth", + expYear: "encYear", + code: "encCode", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index 3d73a8f527c..43068012ef6 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Card as SdkCard } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -85,4 +87,20 @@ export class Card extends Domain { code, }); } + + /** + * Maps Card to SDK format. + * + * @returns {SdkCard} The SDK card object. + */ + toSdkCard(): SdkCard { + return { + cardholderName: this.cardholderName?.toJSON(), + brand: this.brand?.toJSON(), + number: this.number?.toJSON(), + expMonth: this.expMonth?.toJSON(), + expYear: this.expYear?.toJSON(), + code: this.code?.toJSON(), + }; + } } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 0ef2233120a..a889f0b969c 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -3,6 +3,12 @@ import { Jsonify } from "type-fest"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { KeyService } from "@bitwarden/key-management"; +import { + CipherType as SdkCipherType, + UriMatchType, + CipherRepromptType as SdkCipherRepromptType, + LoginLinkedIdType, +} from "@bitwarden/sdk-internal"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; @@ -12,7 +18,7 @@ import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { UserId } from "../../../types/guid"; import { CipherService } from "../../abstractions/cipher.service"; -import { FieldType, SecureNoteType } from "../../enums"; +import { FieldType, LoginLinkedId, SecureNoteType } from "../../enums"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherData } from "../../models/data/cipher.data"; @@ -770,6 +776,165 @@ describe("Cipher DTO", () => { expect(Cipher.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkCipher", () => { + it("should map to SDK Cipher", () => { + const lastUsedDate = new Date("2025-04-15T12:00:00.000Z").getTime(); + const lastLaunched = new Date("2025-04-15T12:00:00.000Z").getTime(); + + const cipherData: CipherData = { + id: "id", + organizationId: "orgId", + folderId: "folderId", + edit: true, + permissions: new CipherPermissionsApi(), + viewPassword: true, + organizationUseTotp: true, + favorite: false, + revisionDate: "2022-01-31T12:00:00.000Z", + type: CipherType.Login, + name: "EncryptedString", + notes: "EncryptedString", + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: null, + reprompt: CipherRepromptType.None, + key: "EncryptedString", + login: { + uris: [ + { + uri: "EncryptedString", + uriChecksum: "EncryptedString", + match: UriMatchStrategy.Domain, + }, + ], + username: "EncryptedString", + password: "EncryptedString", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + totp: "EncryptedString", + autofillOnPageLoad: false, + }, + passwordHistory: [ + { password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }, + ], + attachments: [ + { + id: "a1", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + { + id: "a2", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + ], + fields: [ + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedId.Username, + }, + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedId.Password, + }, + ], + }; + + const cipher = new Cipher(cipherData, { lastUsedDate, lastLaunched }); + const sdkCipher = cipher.toSdkCipher(); + + expect(sdkCipher).toEqual({ + id: "id", + organizationId: "orgId", + folderId: "folderId", + collectionIds: [], + key: "EncryptedString", + name: "EncryptedString", + notes: "EncryptedString", + type: SdkCipherType.Login, + login: { + username: "EncryptedString", + password: "EncryptedString", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + uris: [ + { + uri: "EncryptedString", + uriChecksum: "EncryptedString", + match: UriMatchType.Domain, + }, + ], + totp: "EncryptedString", + autofillOnPageLoad: false, + fido2Credentials: undefined, + }, + identity: undefined, + card: undefined, + secureNote: undefined, + sshKey: undefined, + favorite: false, + reprompt: SdkCipherRepromptType.None, + organizationUseTotp: true, + edit: true, + permissions: new CipherPermissionsApi(), + viewPassword: true, + localData: { + lastUsedDate: "2025-04-15T12:00:00.000Z", + lastLaunched: "2025-04-15T12:00:00.000Z", + }, + attachments: [ + { + id: "a1", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + { + id: "a2", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + ], + fields: [ + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedIdType.Username, + }, + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedIdType.Password, + }, + ], + passwordHistory: [ + { + password: "EncryptedString", + lastUsedDate: "2022-01-31T12:00:00.000Z", + }, + ], + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: undefined, + revisionDate: "2022-01-31T12:00:00.000Z", + }); + }); + }); }); const mockUserId = "TestUserId" as UserId; diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 780690217a8..f647adf198e 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Cipher as SdkCipher } from "@bitwarden/sdk-internal"; + import { Decryptable } from "../../../platform/interfaces/decryptable.interface"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; @@ -330,4 +332,72 @@ export class Cipher extends Domain implements Decryptable { return domain; } + + /** + * Maps Cipher to SDK format. + * + * @returns {SdkCipher} The SDK cipher object. + */ + toSdkCipher(): SdkCipher { + const sdkCipher: SdkCipher = { + id: this.id, + organizationId: this.organizationId, + folderId: this.folderId, + collectionIds: this.collectionIds || [], + key: this.key?.toJSON(), + name: this.name.toJSON(), + notes: this.notes?.toJSON(), + type: this.type, + favorite: this.favorite, + organizationUseTotp: this.organizationUseTotp, + edit: this.edit, + permissions: this.permissions, + viewPassword: this.viewPassword, + localData: this.localData + ? { + lastUsedDate: this.localData.lastUsedDate + ? new Date(this.localData.lastUsedDate).toISOString() + : undefined, + lastLaunched: this.localData.lastLaunched + ? new Date(this.localData.lastLaunched).toISOString() + : undefined, + } + : undefined, + attachments: this.attachments?.map((a) => a.toSdkAttachment()), + fields: this.fields?.map((f) => f.toSdkField()), + passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()), + revisionDate: this.revisionDate?.toISOString(), + creationDate: this.creationDate?.toISOString(), + deletedDate: this.deletedDate?.toISOString(), + reprompt: this.reprompt, + // Initialize all cipher-type-specific properties as undefined + login: undefined, + identity: undefined, + card: undefined, + secureNote: undefined, + sshKey: undefined, + }; + + switch (this.type) { + case CipherType.Login: + sdkCipher.login = this.login.toSdkLogin(); + break; + case CipherType.SecureNote: + sdkCipher.secureNote = this.secureNote.toSdkSecureNote(); + break; + case CipherType.Card: + sdkCipher.card = this.card.toSdkCard(); + break; + case CipherType.Identity: + sdkCipher.identity = this.identity.toSdkIdentity(); + break; + case CipherType.SshKey: + sdkCipher.sshKey = this.sshKey.toSdkSshKey(); + break; + default: + break; + } + + return sdkCipher; + } } diff --git a/libs/common/src/vault/models/domain/fido2-credential.spec.ts b/libs/common/src/vault/models/domain/fido2-credential.spec.ts index e2cddcea3f3..bde29d0e99c 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.spec.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.spec.ts @@ -167,6 +167,45 @@ describe("Fido2Credential", () => { expect(Fido2Credential.fromJSON(null)).toBeNull(); }); }); + + describe("SDK Fido2Credential Mapping", () => { + it("should map to SDK Fido2Credential", () => { + const data: Fido2CredentialData = { + credentialId: "credentialId", + keyType: "public-key", + keyAlgorithm: "ECDSA", + keyCurve: "P-256", + keyValue: "keyValue", + rpId: "rpId", + userHandle: "userHandle", + userName: "userName", + counter: "2", + rpName: "rpName", + userDisplayName: "userDisplayName", + discoverable: "discoverable", + creationDate: mockDate.toISOString(), + }; + + const credential = new Fido2Credential(data); + const sdkCredential = credential.toSdkFido2Credential(); + + expect(sdkCredential).toEqual({ + credentialId: "credentialId", + keyType: "public-key", + keyAlgorithm: "ECDSA", + keyCurve: "P-256", + keyValue: "keyValue", + rpId: "rpId", + userHandle: "userHandle", + userName: "userName", + counter: "2", + rpName: "rpName", + userDisplayName: "userDisplayName", + discoverable: "discoverable", + creationDate: mockDate.toISOString(), + }); + }); + }); }); function createEncryptedEncString(s: string): EncString { diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index 8b0082892e4..7002a58150d 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -148,4 +150,27 @@ export class Fido2Credential extends Domain { creationDate, }); } + + /** + * Maps Fido2Credential to SDK format. + * + * @returns {SdkFido2Credential} The SDK Fido2Credential object. + */ + toSdkFido2Credential(): SdkFido2Credential { + return { + credentialId: this.credentialId?.toJSON(), + keyType: this.keyType.toJSON(), + keyAlgorithm: this.keyAlgorithm.toJSON(), + keyCurve: this.keyCurve.toJSON(), + keyValue: this.keyValue.toJSON(), + rpId: this.rpId.toJSON(), + userHandle: this.userHandle.toJSON(), + userName: this.userName.toJSON(), + counter: this.counter.toJSON(), + rpName: this.rpName?.toJSON(), + userDisplayName: this.userDisplayName?.toJSON(), + discoverable: this.discoverable?.toJSON(), + creationDate: this.creationDate.toISOString(), + }; + } } diff --git a/libs/common/src/vault/models/domain/field.spec.ts b/libs/common/src/vault/models/domain/field.spec.ts index 7dc5556e6cf..c0f9713f7ab 100644 --- a/libs/common/src/vault/models/domain/field.spec.ts +++ b/libs/common/src/vault/models/domain/field.spec.ts @@ -1,6 +1,6 @@ import { mockEnc, mockFromJson } from "../../../../spec"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; -import { FieldType } from "../../enums"; +import { CardLinkedId, FieldType, IdentityLinkedId, LoginLinkedId } from "../../enums"; import { FieldData } from "../../models/data/field.data"; import { Field } from "../../models/domain/field"; @@ -82,4 +82,26 @@ describe("Field", () => { expect(Field.fromJSON(null)).toBeNull(); }); }); + + describe("SDK Field Mapping", () => { + it("should map to SDK Field", () => { + // Test Login LinkedId + const loginField = new Field(data); + loginField.type = FieldType.Linked; + loginField.linkedId = LoginLinkedId.Username; + expect(loginField.toSdkField().linkedId).toBe(100); + + // Test Card LinkedId + const cardField = new Field(data); + cardField.type = FieldType.Linked; + cardField.linkedId = CardLinkedId.Number; + expect(cardField.toSdkField().linkedId).toBe(305); + + // Test Identity LinkedId + const identityField = new Field(data); + identityField.type = FieldType.Linked; + identityField.linkedId = IdentityLinkedId.LicenseNumber; + expect(identityField.toSdkField().linkedId).toBe(415); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts index c0f08a38bcc..223c9b39163 100644 --- a/libs/common/src/vault/models/domain/field.ts +++ b/libs/common/src/vault/models/domain/field.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Field as SdkField, LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -73,4 +75,19 @@ export class Field extends Domain { value, }); } + + /** + * Maps Field to SDK format. + * + * @returns {SdkField} The SDK field object. + */ + toSdkField(): SdkField { + return { + name: this.name?.toJSON(), + value: this.value?.toJSON(), + type: this.type, + // Safe type cast: client and SDK LinkedIdType enums have identical values + linkedId: this.linkedId as unknown as SdkLinkedIdType, + }; + } } diff --git a/libs/common/src/vault/models/domain/identity.spec.ts b/libs/common/src/vault/models/domain/identity.spec.ts index 3a95138998b..cf296a6ff08 100644 --- a/libs/common/src/vault/models/domain/identity.spec.ts +++ b/libs/common/src/vault/models/domain/identity.spec.ts @@ -184,4 +184,32 @@ describe("Identity", () => { expect(Identity.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkIdentity", () => { + it("returns the correct SDK Identity object", () => { + const identity = new Identity(data); + const sdkIdentity = identity.toSdkIdentity(); + + expect(sdkIdentity).toEqual({ + title: "enctitle", + firstName: "encfirstName", + middleName: "encmiddleName", + lastName: "enclastName", + address1: "encaddress1", + address2: "encaddress2", + address3: "encaddress3", + city: "enccity", + state: "encstate", + postalCode: "encpostalCode", + country: "enccountry", + company: "enccompany", + email: "encemail", + phone: "encphone", + ssn: "encssn", + username: "encusername", + passportNumber: "encpassportNumber", + licenseNumber: "enclicenseNumber", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index 5d8c20ef2b3..c7756733a66 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Identity as SdkIdentity } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -165,4 +167,32 @@ export class Identity extends Domain { licenseNumber, }); } + + /** + * Maps Identity to SDK format. + * + * @returns {SdkIdentity} The SDK identity object. + */ + toSdkIdentity(): SdkIdentity { + return { + title: this.title?.toJSON(), + firstName: this.firstName?.toJSON(), + middleName: this.middleName?.toJSON(), + lastName: this.lastName?.toJSON(), + address1: this.address1?.toJSON(), + address2: this.address2?.toJSON(), + address3: this.address3?.toJSON(), + city: this.city?.toJSON(), + state: this.state?.toJSON(), + postalCode: this.postalCode?.toJSON(), + country: this.country?.toJSON(), + company: this.company?.toJSON(), + email: this.email?.toJSON(), + phone: this.phone?.toJSON(), + ssn: this.ssn?.toJSON(), + username: this.username?.toJSON(), + passportNumber: this.passportNumber?.toJSON(), + licenseNumber: this.licenseNumber?.toJSON(), + }; + } } diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index 6346f38f0de..a0e6b6d7dc9 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; +import { UriMatchType } from "@bitwarden/sdk-internal"; + import { mockEnc, mockFromJson } from "../../../../spec"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; @@ -118,4 +120,17 @@ describe("LoginUri", () => { expect(LoginUri.fromJSON(null)).toBeNull(); }); }); + + describe("SDK Login Uri Mapping", () => { + it("should map to SDK login uri", () => { + const loginUri = new LoginUri(data); + const sdkLoginUri = loginUri.toSdkLoginUri(); + + expect(sdkLoginUri).toEqual({ + uri: "encUri", + uriChecksum: "encUriChecksum", + match: UriMatchType.Domain, + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 883f8c9a616..b3e6fad70dd 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { LoginUri as SdkLoginUri } from "@bitwarden/sdk-internal"; + import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; @@ -87,4 +89,17 @@ export class LoginUri extends Domain { uriChecksum, }); } + + /** + * Maps LoginUri to SDK format. + * + * @returns {SdkLoginUri} The SDK login uri object. + */ + toSdkLoginUri(): SdkLoginUri { + return { + uri: this.uri.toJSON(), + uriChecksum: this.uriChecksum.toJSON(), + match: this.match, + }; + } } diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts index 4f9e4546220..84d12e8131f 100644 --- a/libs/common/src/vault/models/domain/login.spec.ts +++ b/libs/common/src/vault/models/domain/login.spec.ts @@ -202,6 +202,54 @@ describe("Login DTO", () => { expect(Login.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkLogin", () => { + it("should map to SDK login", () => { + const data: LoginData = { + uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }], + username: "username", + password: "password", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + totp: "123", + autofillOnPageLoad: false, + fido2Credentials: [initializeFido2Credential(new Fido2CredentialData())], + }; + const login = new Login(data); + const sdkLogin = login.toSdkLogin(); + + expect(sdkLogin).toEqual({ + username: "username", + password: "password", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + uris: [ + { + match: 0, + uri: "uri", + uriChecksum: "checksum", + }, + ], + totp: "123", + autofillOnPageLoad: false, + fido2Credentials: [ + { + credentialId: "credentialId", + keyType: "public-key", + keyAlgorithm: "ECDSA", + keyCurve: "P-256", + keyValue: "keyValue", + rpId: "rpId", + userHandle: "userHandle", + userName: "userName", + counter: "counter", + rpName: "rpName", + userDisplayName: "userDisplayName", + discoverable: "discoverable", + creationDate: "2023-01-01T12:00:00.000Z", + }, + ], + }); + }); + }); }); type Fido2CredentialLike = Fido2CredentialData | Fido2CredentialView | Fido2CredentialApi; diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index b29b42bf3de..1893212bdaa 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Login as SdkLogin } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -144,4 +146,21 @@ export class Login extends Domain { fido2Credentials, }); } + + /** + * Maps Login to SDK format. + * + * @returns {SdkLogin} The SDK login object. + */ + toSdkLogin(): SdkLogin { + return { + uris: this.uris?.map((u) => u.toSdkLoginUri()), + username: this.username?.toJSON(), + password: this.password?.toJSON(), + passwordRevisionDate: this.passwordRevisionDate?.toISOString(), + totp: this.totp?.toJSON(), + autofillOnPageLoad: this.autofillOnPageLoad, + fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()), + }; + } } diff --git a/libs/common/src/vault/models/domain/password.spec.ts b/libs/common/src/vault/models/domain/password.spec.ts index 614b9639e52..24163cccf36 100644 --- a/libs/common/src/vault/models/domain/password.spec.ts +++ b/libs/common/src/vault/models/domain/password.spec.ts @@ -70,4 +70,17 @@ describe("Password", () => { expect(Password.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkPasswordHistory", () => { + it("returns the correct SDK PasswordHistory object", () => { + const password = new Password(data); + + const sdkPasswordHistory = password.toSdkPasswordHistory(); + + expect(sdkPasswordHistory).toEqual({ + password: "encPassword", + lastUsedDate: new Date("2022-01-31T12:00:00.000Z").toISOString(), + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index 8573c224416..b69a61a95a9 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { PasswordHistory } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -57,4 +59,16 @@ export class Password extends Domain { lastUsedDate, }); } + + /** + * Maps Password to SDK format. + * + * @returns {PasswordHistory} The SDK password history object. + */ + toSdkPasswordHistory(): PasswordHistory { + return { + password: this.password.toJSON(), + lastUsedDate: this.lastUsedDate.toISOString(), + }; + } } diff --git a/libs/common/src/vault/models/domain/secure-note.spec.ts b/libs/common/src/vault/models/domain/secure-note.spec.ts index 719cf59f136..ff71e53238d 100644 --- a/libs/common/src/vault/models/domain/secure-note.spec.ts +++ b/libs/common/src/vault/models/domain/secure-note.spec.ts @@ -50,4 +50,17 @@ describe("SecureNote", () => { expect(SecureNote.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkSecureNote", () => { + it("returns the correct SDK SecureNote object", () => { + const secureNote = new SecureNote(); + secureNote.type = SecureNoteType.Generic; + + const sdkSecureNote = secureNote.toSdkSecureNote(); + + expect(sdkSecureNote).toEqual({ + type: 0, + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts index 693ae38d9fb..ac7977b0e46 100644 --- a/libs/common/src/vault/models/domain/secure-note.ts +++ b/libs/common/src/vault/models/domain/secure-note.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { SecureNote as SdkSecureNote } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SecureNoteType } from "../../enums"; @@ -41,4 +43,15 @@ export class SecureNote extends Domain { return Object.assign(new SecureNote(), obj); } + + /** + * Maps Secure note to SDK format. + * + * @returns {SdkSecureNote} The SDK secure note object. + */ + toSdkSecureNote(): SdkSecureNote { + return { + type: this.type, + }; + } } diff --git a/libs/common/src/vault/models/domain/ssh-key.spec.ts b/libs/common/src/vault/models/domain/ssh-key.spec.ts index f56d738fde8..6576d1a41e9 100644 --- a/libs/common/src/vault/models/domain/ssh-key.spec.ts +++ b/libs/common/src/vault/models/domain/ssh-key.spec.ts @@ -64,4 +64,17 @@ describe("Sshkey", () => { expect(SshKey.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkSshKey", () => { + it("returns the correct SDK SshKey object", () => { + const sshKey = new SshKey(data); + const sdkSshKey = sshKey.toSdkSshKey(); + + expect(sdkSshKey).toEqual({ + privateKey: "privateKey", + publicKey: "publicKey", + fingerprint: "keyFingerprint", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index f32a1a913ca..96a1c9e58de 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { SshKey as SdkSshKey } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -70,4 +72,17 @@ export class SshKey extends Domain { keyFingerprint, }); } + + /** + * Maps SSH key to SDK format. + * + * @returns {SdkSshKey} The SDK SSH key object. + */ + toSdkSshKey(): SdkSshKey { + return { + privateKey: this.privateKey.toJSON(), + publicKey: this.publicKey.toJSON(), + fingerprint: this.keyFingerprint.toJSON(), + }; + } } diff --git a/libs/common/src/vault/models/view/attachment.view.spec.ts b/libs/common/src/vault/models/view/attachment.view.spec.ts index 7cb291f2714..8ae836e1265 100644 --- a/libs/common/src/vault/models/view/attachment.view.spec.ts +++ b/libs/common/src/vault/models/view/attachment.view.spec.ts @@ -1,4 +1,7 @@ +import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; + import { mockFromJson } from "../../../../spec"; +import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { AttachmentView } from "./attachment.view"; @@ -15,4 +18,56 @@ describe("AttachmentView", () => { expect(actual.key).toEqual("encKeyB64_fromJSON"); }); + + describe("fromSdkAttachmentView", () => { + it("should return undefined when the input is null", () => { + const result = AttachmentView.fromSdkAttachmentView(null as unknown as any); + expect(result).toBeUndefined(); + }); + + it("should return an AttachmentView from an SdkAttachmentView", () => { + const sdkAttachmentView = { + id: "id", + url: "url", + size: "size", + sizeName: "sizeName", + fileName: "fileName", + key: "encKeyB64_fromString", + } as SdkAttachmentView; + + const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView); + + expect(result).toMatchObject({ + id: "id", + url: "url", + size: "size", + sizeName: "sizeName", + fileName: "fileName", + key: null, + encryptedKey: new EncString(sdkAttachmentView.key as string), + }); + }); + }); + + describe("toSdkAttachmentView", () => { + it("should convert AttachmentView to SdkAttachmentView", () => { + const attachmentView = new AttachmentView(); + attachmentView.id = "id"; + attachmentView.url = "url"; + attachmentView.size = "size"; + attachmentView.sizeName = "sizeName"; + attachmentView.fileName = "fileName"; + attachmentView.encryptedKey = new EncString("encKeyB64"); + + const result = attachmentView.toSdkAttachmentView(); + expect(result).toEqual({ + id: "id", + url: "url", + size: "size", + sizeName: "sizeName", + fileName: "fileName", + key: "encKeyB64", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index 09839ed2fef..2ef4280d97a 100644 --- a/libs/common/src/vault/models/view/attachment.view.ts +++ b/libs/common/src/vault/models/view/attachment.view.ts @@ -2,7 +2,10 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; + import { View } from "../../../models/view/view"; +import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { Attachment } from "../domain/attachment"; @@ -13,6 +16,10 @@ export class AttachmentView implements View { sizeName: string = null; fileName: string = null; key: SymmetricCryptoKey = null; + /** + * The SDK returns an encrypted key for the attachment. + */ + encryptedKey: EncString | undefined; constructor(a?: Attachment) { if (!a) { @@ -40,4 +47,37 @@ export class AttachmentView implements View { const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key); return Object.assign(new AttachmentView(), obj, { key: key }); } + + /** + * Converts the AttachmentView to a SDK AttachmentView. + */ + toSdkAttachmentView(): SdkAttachmentView { + return { + id: this.id, + url: this.url, + size: this.size, + sizeName: this.sizeName, + fileName: this.fileName, + key: this.encryptedKey?.toJSON(), + }; + } + + /** + * Converts the SDK AttachmentView to a AttachmentView. + */ + static fromSdkAttachmentView(obj: SdkAttachmentView): AttachmentView | undefined { + if (!obj) { + return undefined; + } + + const view = new AttachmentView(); + view.id = obj.id ?? null; + view.url = obj.url ?? null; + view.size = obj.size ?? null; + view.sizeName = obj.sizeName ?? null; + view.fileName = obj.fileName ?? null; + view.encryptedKey = new EncString(obj.key); + + return view; + } } diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts index 9eeb4dabf4d..2adfbb39e89 100644 --- a/libs/common/src/vault/models/view/card.view.ts +++ b/libs/common/src/vault/models/view/card.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { CardView as SdkCardView } from "@bitwarden/sdk-internal"; + import { normalizeExpiryYearFormat } from "../../../autofill/utils"; import { CardLinkedId as LinkedId } from "../../enums"; import { linkedFieldOption } from "../../linked-field-option.decorator"; @@ -146,4 +148,15 @@ export class CardView extends ItemView { return null; } + + /** + * Converts an SDK CardView to a CardView. + */ + static fromSdkCardView(obj: SdkCardView): CardView | undefined { + if (obj == null) { + return undefined; + } + + return Object.assign(new CardView(), obj); + } } diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index 3ab2706d356..b9d3e42aa62 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -1,4 +1,16 @@ -import { mockFromJson } from "../../../../spec"; +import { + CipherView as SdkCipherView, + CipherType as SdkCipherType, + CipherRepromptType as SdkCipherRepromptType, + AttachmentView as SdkAttachmentView, + LoginUriView as SdkLoginUriView, + LoginView as SdkLoginView, + FieldView as SdkFieldView, + FieldType as SdkFieldType, +} from "@bitwarden/sdk-internal"; + +import { mockFromJson, mockFromSdk } from "../../../../spec"; +import { CipherRepromptType } from "../../enums"; import { CipherType } from "../../enums/cipher-type"; import { AttachmentView } from "./attachment.view"; @@ -9,6 +21,7 @@ import { IdentityView } from "./identity.view"; import { LoginView } from "./login.view"; import { PasswordHistoryView } from "./password-history.view"; import { SecureNoteView } from "./secure-note.view"; +import { SshKeyView } from "./ssh-key.view"; jest.mock("../../models/view/login.view"); jest.mock("../../models/view/attachment.view"); @@ -73,4 +86,121 @@ describe("CipherView", () => { expect(actual).toMatchObject(expected); }); }); + + describe("fromSdkCipherView", () => { + let sdkCipherView: SdkCipherView; + + beforeEach(() => { + jest.spyOn(CardView, "fromSdkCardView").mockImplementation(mockFromSdk); + jest.spyOn(IdentityView, "fromSdkIdentityView").mockImplementation(mockFromSdk); + jest.spyOn(LoginView, "fromSdkLoginView").mockImplementation(mockFromSdk); + jest.spyOn(SecureNoteView, "fromSdkSecureNoteView").mockImplementation(mockFromSdk); + jest.spyOn(SshKeyView, "fromSdkSshKeyView").mockImplementation(mockFromSdk); + jest.spyOn(AttachmentView, "fromSdkAttachmentView").mockImplementation(mockFromSdk); + jest.spyOn(FieldView, "fromSdkFieldView").mockImplementation(mockFromSdk); + + sdkCipherView = { + id: "id", + organizationId: "orgId", + folderId: "folderId", + collectionIds: ["collectionId"], + key: undefined, + name: "name", + notes: undefined, + type: SdkCipherType.Login, + favorite: true, + edit: true, + reprompt: SdkCipherRepromptType.None, + organizationUseTotp: false, + viewPassword: true, + localData: undefined, + permissions: undefined, + attachments: [{ id: "attachmentId", url: "attachmentUrl" } as SdkAttachmentView], + login: { + username: "username", + password: "password", + uris: [{ uri: "bitwarden.com" } as SdkLoginUriView], + totp: "totp", + autofillOnPageLoad: true, + } as SdkLoginView, + identity: undefined, + card: undefined, + secureNote: undefined, + sshKey: undefined, + fields: [ + { + name: "fieldName", + value: "fieldValue", + type: SdkFieldType.Linked, + linkedId: 100, + } as SdkFieldView, + ], + passwordHistory: undefined, + creationDate: "2022-01-01T12:00:00.000Z", + revisionDate: "2022-01-02T12:00:00.000Z", + deletedDate: undefined, + }; + }); + + it("returns undefined when input is null", () => { + expect(CipherView.fromSdkCipherView(null as unknown as SdkCipherView)).toBeUndefined(); + }); + + it("maps properties correctly", () => { + const result = CipherView.fromSdkCipherView(sdkCipherView); + + expect(result).toMatchObject({ + id: "id", + organizationId: "orgId", + folderId: "folderId", + collectionIds: ["collectionId"], + name: "name", + notes: null, + type: CipherType.Login, + favorite: true, + edit: true, + reprompt: CipherRepromptType.None, + organizationUseTotp: false, + viewPassword: true, + localData: undefined, + permissions: undefined, + attachments: [ + { + id: "attachmentId", + url: "attachmentUrl", + __fromSdk: true, + }, + ], + login: { + username: "username", + password: "password", + uris: [ + { + uri: "bitwarden.com", + }, + ], + totp: "totp", + autofillOnPageLoad: true, + __fromSdk: true, + }, + identity: new IdentityView(), + card: new CardView(), + secureNote: new SecureNoteView(), + sshKey: new SshKeyView(), + fields: [ + { + name: "fieldName", + value: "fieldValue", + type: SdkFieldType.Linked, + linkedId: 100, + __fromSdk: true, + }, + ], + passwordHistory: null, + creationDate: new Date("2022-01-01T12:00:00.000Z"), + revisionDate: new Date("2022-01-02T12:00:00.000Z"), + deletedDate: null, + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 7ddba9e2ed5..1f73903a5bc 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; + import { View } from "../../../models/view/view"; import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; @@ -110,7 +112,7 @@ export class CipherView implements View, InitializerMetadata { get hasOldAttachments(): boolean { if (this.hasAttachments) { for (let i = 0; i < this.attachments.length; i++) { - if (this.attachments[i].key == null) { + if (this.attachments[i].key == null && this.attachments[i].encryptedKey == null) { return true; } } @@ -222,4 +224,68 @@ export class CipherView implements View, InitializerMetadata { return view; } + + /** + * Creates a CipherView from the SDK CipherView. + */ + static fromSdkCipherView(obj: SdkCipherView): CipherView | undefined { + if (obj == null) { + return undefined; + } + + const cipherView = new CipherView(); + cipherView.id = obj.id ?? null; + cipherView.organizationId = obj.organizationId ?? null; + cipherView.folderId = obj.folderId ?? null; + cipherView.name = obj.name; + cipherView.notes = obj.notes ?? null; + cipherView.type = obj.type; + cipherView.favorite = obj.favorite; + cipherView.organizationUseTotp = obj.organizationUseTotp; + cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions); + cipherView.edit = obj.edit; + cipherView.viewPassword = obj.viewPassword; + cipherView.localData = obj.localData + ? { + lastUsedDate: obj.localData.lastUsedDate + ? new Date(obj.localData.lastUsedDate).getTime() + : undefined, + lastLaunched: obj.localData.lastLaunched + ? new Date(obj.localData.lastLaunched).getTime() + : undefined, + } + : undefined; + cipherView.attachments = + obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? null; + cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? null; + cipherView.passwordHistory = + obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? null; + cipherView.collectionIds = obj.collectionIds ?? null; + cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); + cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate); + cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate); + cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None; + + switch (obj.type) { + case CipherType.Card: + cipherView.card = CardView.fromSdkCardView(obj.card); + break; + case CipherType.Identity: + cipherView.identity = IdentityView.fromSdkIdentityView(obj.identity); + break; + case CipherType.Login: + cipherView.login = LoginView.fromSdkLoginView(obj.login); + break; + case CipherType.SecureNote: + cipherView.secureNote = SecureNoteView.fromSdkSecureNoteView(obj.secureNote); + break; + case CipherType.SshKey: + cipherView.sshKey = SshKeyView.fromSdkSshKeyView(obj.sshKey); + break; + default: + break; + } + + return cipherView; + } } diff --git a/libs/common/src/vault/models/view/fido2-credential.view.ts b/libs/common/src/vault/models/view/fido2-credential.view.ts index b364d63b8ea..bf1d324d22d 100644 --- a/libs/common/src/vault/models/view/fido2-credential.view.ts +++ b/libs/common/src/vault/models/view/fido2-credential.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Fido2CredentialView as SdkFido2CredentialView } from "@bitwarden/sdk-internal"; + import { ItemView } from "./item.view"; export class Fido2CredentialView extends ItemView { @@ -29,4 +31,29 @@ export class Fido2CredentialView extends ItemView { creationDate, }); } + + /** + * Converts the SDK Fido2CredentialView to a Fido2CredentialView. + */ + static fromSdkFido2CredentialView(obj: SdkFido2CredentialView): Fido2CredentialView | undefined { + if (!obj) { + return undefined; + } + + const view = new Fido2CredentialView(); + view.credentialId = obj.credentialId; + view.keyType = obj.keyType as "public-key"; + view.keyAlgorithm = obj.keyAlgorithm as "ECDSA"; + view.keyCurve = obj.keyCurve as "P-256"; + view.rpId = obj.rpId; + view.userHandle = obj.userHandle; + view.userName = obj.userName; + view.counter = parseInt(obj.counter); + view.rpName = obj.rpName; + view.userDisplayName = obj.userDisplayName; + view.discoverable = obj.discoverable?.toLowerCase() === "true" ? true : false; + view.creationDate = obj.creationDate ? new Date(obj.creationDate) : null; + + return view; + } } diff --git a/libs/common/src/vault/models/view/field.view.ts b/libs/common/src/vault/models/view/field.view.ts index ef8c5113fd0..770150f8a63 100644 --- a/libs/common/src/vault/models/view/field.view.ts +++ b/libs/common/src/vault/models/view/field.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { FieldView as SdkFieldView } from "@bitwarden/sdk-internal"; + import { View } from "../../../models/view/view"; import { FieldType, LinkedIdType } from "../../enums"; import { Field } from "../domain/field"; @@ -31,4 +33,21 @@ export class FieldView implements View { static fromJSON(obj: Partial>): FieldView { return Object.assign(new FieldView(), obj); } + + /** + * Converts the SDK FieldView to a FieldView. + */ + static fromSdkFieldView(obj: SdkFieldView): FieldView | undefined { + if (!obj) { + return undefined; + } + + const view = new FieldView(); + view.name = obj.name; + view.value = obj.value; + view.type = obj.type; + view.linkedId = obj.linkedId as unknown as LinkedIdType; + + return view; + } } diff --git a/libs/common/src/vault/models/view/identity.view.ts b/libs/common/src/vault/models/view/identity.view.ts index 247e5cfec86..a75d11efd95 100644 --- a/libs/common/src/vault/models/view/identity.view.ts +++ b/libs/common/src/vault/models/view/identity.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { IdentityView as SdkIdentityView } from "@bitwarden/sdk-internal"; + import { Utils } from "../../../platform/misc/utils"; import { IdentityLinkedId as LinkedId } from "../../enums"; import { linkedFieldOption } from "../../linked-field-option.decorator"; @@ -158,4 +160,15 @@ export class IdentityView extends ItemView { static fromJSON(obj: Partial>): IdentityView { return Object.assign(new IdentityView(), obj); } + + /** + * Converts the SDK IdentityView to an IdentityView. + */ + static fromSdkIdentityView(obj: SdkIdentityView): IdentityView | undefined { + if (obj == null) { + return undefined; + } + + return Object.assign(new IdentityView(), obj); + } } diff --git a/libs/common/src/vault/models/view/login-uri-view.spec.ts b/libs/common/src/vault/models/view/login-uri-view.spec.ts index efc75096295..155d3d59f7c 100644 --- a/libs/common/src/vault/models/view/login-uri-view.spec.ts +++ b/libs/common/src/vault/models/view/login-uri-view.spec.ts @@ -1,3 +1,5 @@ +import { LoginUriView as SdkLoginUriView, UriMatchType } from "@bitwarden/sdk-internal"; + import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; @@ -184,6 +186,26 @@ describe("LoginUriView", () => { }); }); }); + + describe("fromSdkLoginUriView", () => { + it("should return undefined when the input is null", () => { + const result = LoginUriView.fromSdkLoginUriView(null as unknown as SdkLoginUriView); + expect(result).toBeUndefined(); + }); + + it("should create a LoginUriView from a SdkLoginUriView", () => { + const sdkLoginUriView = { + uri: "https://example.com", + match: UriMatchType.Host, + } as SdkLoginUriView; + + const loginUriView = LoginUriView.fromSdkLoginUriView(sdkLoginUriView); + + expect(loginUriView).toBeInstanceOf(LoginUriView); + expect(loginUriView!.uri).toBe(sdkLoginUriView.uri); + expect(loginUriView!.match).toBe(sdkLoginUriView.match); + }); + }); }); function uriFactory(match: UriMatchStrategySetting, uri: string) { diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index 315adb87c75..43d47aa4a3c 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { LoginUriView as SdkLoginUriView } from "@bitwarden/sdk-internal"; + import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { View } from "../../../models/view/view"; import { SafeUrls } from "../../../platform/misc/safe-urls"; @@ -112,6 +114,21 @@ export class LoginUriView implements View { return Object.assign(new LoginUriView(), obj); } + /** + * Converts a LoginUriView object from the SDK to a LoginUriView object. + */ + static fromSdkLoginUriView(obj: SdkLoginUriView): LoginUriView | undefined { + if (obj == null) { + return undefined; + } + + const view = new LoginUriView(); + view.uri = obj.uri; + view.match = obj.match; + + return view; + } + matchesUri( targetUri: string, equivalentDomains: Set, diff --git a/libs/common/src/vault/models/view/login.view.spec.ts b/libs/common/src/vault/models/view/login.view.spec.ts index 728a62deb9d..57e82faf7f1 100644 --- a/libs/common/src/vault/models/view/login.view.spec.ts +++ b/libs/common/src/vault/models/view/login.view.spec.ts @@ -1,4 +1,6 @@ -import { mockFromJson } from "../../../../spec"; +import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal"; + +import { mockFromJson, mockFromSdk } from "../../../../spec"; import { LoginUriView } from "./login-uri.view"; import { LoginView } from "./login.view"; @@ -25,4 +27,35 @@ describe("LoginView", () => { uris: ["uri1_fromJSON", "uri2_fromJSON", "uri3_fromJSON"], }); }); + + describe("fromSdkLoginView", () => { + it("should return undefined when the input is null", () => { + const result = LoginView.fromSdkLoginView(null as unknown as SdkLoginView); + expect(result).toBeUndefined(); + }); + + it("should return a LoginView from an SdkLoginView", () => { + jest.spyOn(LoginUriView, "fromSdkLoginUriView").mockImplementation(mockFromSdk); + + const sdkLoginView = { + username: "username", + password: "password", + passwordRevisionDate: "2025-01-01T01:06:40.441Z", + uris: [{ uri: "bitwarden.com" } as any], + totp: "totp", + autofillOnPageLoad: true, + } as SdkLoginView; + + const result = LoginView.fromSdkLoginView(sdkLoginView); + + expect(result).toMatchObject({ + username: "username", + password: "password", + passwordRevisionDate: new Date("2025-01-01T01:06:40.441Z"), + uris: [expect.objectContaining({ uri: "bitwarden.com", __fromSdk: true })], + totp: "totp", + autofillOnPageLoad: true, + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 228f3a60c34..41568f643d5 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal"; + import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; import { DeepJsonify } from "../../../types/deep-jsonify"; @@ -100,4 +102,27 @@ export class LoginView extends ItemView { fido2Credentials, }); } + + /** + * Converts the SDK LoginView to a LoginView. + * + * Note: FIDO2 credentials remain encrypted at this stage. + * Unlike other fields that are decrypted as part of the LoginView, the SDK maintains + * the FIDO2 credentials in encrypted form. We can decrypt them later using a separate + * call to client.vault().ciphers().decrypt_fido2_credentials(). + */ + static fromSdkLoginView(obj: SdkLoginView): LoginView | undefined { + if (obj == null) { + return undefined; + } + + const passwordRevisionDate = + obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); + const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)); + + return Object.assign(new LoginView(), obj, { + passwordRevisionDate, + uris, + }); + } } diff --git a/libs/common/src/vault/models/view/password-history.view.spec.ts b/libs/common/src/vault/models/view/password-history.view.spec.ts index 7349e44454d..81894ec7493 100644 --- a/libs/common/src/vault/models/view/password-history.view.spec.ts +++ b/libs/common/src/vault/models/view/password-history.view.spec.ts @@ -1,3 +1,5 @@ +import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal"; + import { PasswordHistoryView } from "./password-history.view"; describe("PasswordHistoryView", () => { @@ -10,4 +12,25 @@ describe("PasswordHistoryView", () => { expect(actual.lastUsedDate).toEqual(lastUsedDate); }); + + describe("fromSdkPasswordHistoryView", () => { + it("should return undefined when the input is null", () => { + const result = PasswordHistoryView.fromSdkPasswordHistoryView(null as unknown as any); + expect(result).toBeUndefined(); + }); + + it("should return a PasswordHistoryView from an SdkPasswordHistoryView", () => { + const sdkPasswordHistoryView = { + password: "password", + lastUsedDate: "2023-10-01T00:00:00Z", + } as SdkPasswordHistoryView; + + const result = PasswordHistoryView.fromSdkPasswordHistoryView(sdkPasswordHistoryView); + + expect(result).toMatchObject({ + password: "password", + lastUsedDate: new Date("2023-10-01T00:00:00Z"), + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/password-history.view.ts b/libs/common/src/vault/models/view/password-history.view.ts index 3ab360d5e09..31f05f4cc71 100644 --- a/libs/common/src/vault/models/view/password-history.view.ts +++ b/libs/common/src/vault/models/view/password-history.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal"; + import { View } from "../../../models/view/view"; import { Password } from "../domain/password"; @@ -24,4 +26,19 @@ export class PasswordHistoryView implements View { lastUsedDate: lastUsedDate, }); } + + /** + * Converts the SDK PasswordHistoryView to a PasswordHistoryView. + */ + static fromSdkPasswordHistoryView(obj: SdkPasswordHistoryView): PasswordHistoryView | undefined { + if (!obj) { + return undefined; + } + + const view = new PasswordHistoryView(); + view.password = obj.password; + view.lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate); + + return view; + } } diff --git a/libs/common/src/vault/models/view/secure-note.view.ts b/libs/common/src/vault/models/view/secure-note.view.ts index c7dd4f8932d..075e4dfc520 100644 --- a/libs/common/src/vault/models/view/secure-note.view.ts +++ b/libs/common/src/vault/models/view/secure-note.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { SecureNoteView as SdkSecureNoteView } from "@bitwarden/sdk-internal"; + import { SecureNoteType } from "../../enums"; import { SecureNote } from "../domain/secure-note"; @@ -26,4 +28,15 @@ export class SecureNoteView extends ItemView { static fromJSON(obj: Partial>): SecureNoteView { return Object.assign(new SecureNoteView(), obj); } + + /** + * Converts the SDK SecureNoteView to a SecureNoteView. + */ + static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView | undefined { + if (!obj) { + return undefined; + } + + return Object.assign(new SecureNoteView(), obj); + } } diff --git a/libs/common/src/vault/models/view/ssh-key.view.ts b/libs/common/src/vault/models/view/ssh-key.view.ts index 8f1a9c5a65a..a3d091e4c07 100644 --- a/libs/common/src/vault/models/view/ssh-key.view.ts +++ b/libs/common/src/vault/models/view/ssh-key.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { SshKeyView as SdkSshKeyView } from "@bitwarden/sdk-internal"; + import { SshKey } from "../domain/ssh-key"; import { ItemView } from "./item.view"; @@ -44,4 +46,19 @@ export class SshKeyView extends ItemView { static fromJSON(obj: Partial>): SshKeyView { return Object.assign(new SshKeyView(), obj); } + + /** + * Converts the SDK SshKeyView to a SshKeyView. + */ + static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView | undefined { + if (!obj) { + return undefined; + } + + const keyFingerprint = obj.fingerprint; + + return Object.assign(new SshKeyView(), obj, { + keyFingerprint, + }); + } } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index a8b37e8adc6..b15bc4a9112 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -6,7 +6,7 @@ import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; -import { makeStaticByteArray } from "../../../spec/utils"; +import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; @@ -24,6 +24,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { ContainerService } from "../../platform/services/container.service"; import { CipherId, UserId } from "../../types/guid"; import { CipherKey, OrgKey, UserKey } from "../../types/key"; +import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; import { CipherRepromptType } from "../enums/cipher-reprompt-type"; @@ -34,6 +35,7 @@ import { Cipher } from "../models/domain/cipher"; import { CipherCreateRequest } from "../models/request/cipher-create.request"; import { CipherPartialRequest } from "../models/request/cipher-partial.request"; import { CipherRequest } from "../models/request/cipher.request"; +import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; import { LoginUriView } from "../models/view/login-uri.view"; @@ -124,6 +126,7 @@ describe("Cipher Service", () => { accountService = mockAccountServiceWith(mockUserId); const logService = mock(); const stateProvider = new FakeStateProvider(accountService); + const cipherEncryptionService = mock(); const userId = "TestUserId" as UserId; @@ -151,6 +154,7 @@ describe("Cipher Service", () => { stateProvider, accountService, logService, + cipherEncryptionService, ); cipherObj = new Cipher(cipherData); @@ -478,4 +482,85 @@ describe("Cipher Service", () => { ).rejects.toThrow("Cannot rotate ciphers when decryption failures are present"); }); }); + + describe("decrypt", () => { + it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => { + configService.getFeatureFlag.mockResolvedValue(true); + cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(cipherObj)); + + const result = await cipherService.decrypt(cipherObj, userId); + + expect(result).toEqual(new CipherView(cipherObj)); + expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(cipherObj, userId); + }); + + it("should call legacy decrypt when feature flag is false", async () => { + const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; + configService.getFeatureFlag.mockResolvedValue(false); + cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey); + encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); + jest.spyOn(cipherObj, "decrypt").mockResolvedValue(new CipherView(cipherObj)); + + const result = await cipherService.decrypt(cipherObj, userId); + + expect(result).toEqual(new CipherView(cipherObj)); + expect(cipherObj.decrypt).toHaveBeenCalledWith(mockUserKey); + }); + }); + + describe("getDecryptedAttachmentBuffer", () => { + const mockEncryptedContent = new Uint8Array([1, 2, 3]); + const mockDecryptedContent = new Uint8Array([4, 5, 6]); + + it("should use SDK when feature flag is enabled", async () => { + const cipher = new Cipher(cipherData); + const attachment = new AttachmentView(cipher.attachments![0]); + configService.getFeatureFlag.mockResolvedValue(true); + + jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData })); + cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent); + const mockResponse = { + arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer), + } as unknown as Response; + + const result = await cipherService.getDecryptedAttachmentBuffer( + cipher.id as CipherId, + attachment, + mockResponse, + userId, + ); + + expect(result).toEqual(mockDecryptedContent); + expect(cipherEncryptionService.decryptAttachmentContent).toHaveBeenCalledWith( + cipher, + attachment, + mockEncryptedContent, + userId, + ); + }); + + it("should use legacy decryption when feature flag is enabled", async () => { + configService.getFeatureFlag.mockResolvedValue(false); + const cipher = new Cipher(cipherData); + const attachment = new AttachmentView(cipher.attachments![0]); + attachment.key = makeSymmetricCryptoKey(64); + + const mockResponse = { + arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer), + } as unknown as Response; + const mockEncBuf = {} as EncArrayBuffer; + EncArrayBuffer.fromResponse = jest.fn().mockResolvedValue(mockEncBuf); + encryptService.decryptFileData.mockResolvedValue(mockDecryptedContent); + + const result = await cipherService.getDecryptedAttachmentBuffer( + cipher.id as CipherId, + attachment, + mockResponse, + userId, + ); + + expect(result).toEqual(mockDecryptedContent); + expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key); + }); + }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 169568d44e9..6bea56baa5e 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -29,7 +29,8 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { StateProvider } from "../../platform/state"; import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid"; import { OrgKey, UserKey } from "../../types/key"; -import { perUserCache$ } from "../../vault/utils/observable-utilities"; +import { filterOutNullish, perUserCache$ } from "../../vault/utils/observable-utilities"; +import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; @@ -103,6 +104,7 @@ export class CipherService implements CipherServiceAbstraction { private stateProvider: StateProvider, private accountService: AccountService, private logService: LogService, + private cipherEncryptionService: CipherEncryptionService, ) {} localData$(userId: UserId): Observable> { @@ -424,13 +426,21 @@ export class CipherService implements CipherServiceAbstraction { ciphers: Cipher[], userId: UserId, ): Promise<[CipherView[], CipherView[]] | null> { - const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); + if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) { + const decryptStartTime = new Date().getTime(); + const decrypted = await this.decryptCiphersWithSdk(ciphers, userId); + this.logService.info( + `[CipherService] Decrypting ${decrypted.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`, + ); + // With SDK, failed ciphers are not returned + return [decrypted, []]; + } + const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { // return early if there are no keys to decrypt with return null; } - // Group ciphers by orgId or under 'null' for the user's ciphers const grouped = ciphers.reduce( (agg, c) => { @@ -440,7 +450,6 @@ export class CipherService implements CipherServiceAbstraction { }, {} as Record, ); - const decryptStartTime = new Date().getTime(); const allCipherViews = ( await Promise.all( @@ -464,7 +473,6 @@ export class CipherService implements CipherServiceAbstraction { this.logService.info( `[CipherService] Decrypting ${allCipherViews.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`, ); - // Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt return allCipherViews.reduce( (acc, c) => { @@ -479,6 +487,21 @@ export class CipherService implements CipherServiceAbstraction { ); } + /** + * Decrypts a cipher using either the SDK or the legacy method based on the feature flag. + * @param cipher The cipher to decrypt. + * @param userId The user ID to use for decryption. + * @returns A promise that resolves to the decrypted cipher view. + */ + async decrypt(cipher: Cipher, userId: UserId): Promise { + if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) { + return await this.cipherEncryptionService.decrypt(cipher, userId); + } else { + const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId); + return await cipher.decrypt(encKey); + } + } + private async reindexCiphers(userId: UserId) { const reindexRequired = this.searchService != null && @@ -895,7 +918,7 @@ export class CipherService implements CipherServiceAbstraction { //then we rollback to using the user key as the main key of encryption of the item //in order to keep item and it's attachments with the same encryption level if (cipher.key != null && !cipherKeyEncryptionEnabled) { - const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher, userId)); + const model = await this.decrypt(cipher, userId); cipher = await this.encrypt(model, userId); await this.updateWithServer(cipher); } @@ -1381,6 +1404,43 @@ export class CipherService implements CipherServiceAbstraction { return encryptedCiphers; } + async getDecryptedAttachmentBuffer( + cipherId: CipherId, + attachment: AttachmentView, + response: Response, + userId: UserId, + ): Promise { + const useSdkDecryption = await this.configService.getFeatureFlag( + FeatureFlag.PM19941MigrateCipherDomainToSdk, + ); + + const cipherDomain = await firstValueFrom( + this.ciphers$(userId).pipe(map((ciphersData) => new Cipher(ciphersData[cipherId]))), + ); + + if (useSdkDecryption) { + const encryptedContent = await response.arrayBuffer(); + return this.cipherEncryptionService.decryptAttachmentContent( + cipherDomain, + attachment, + new Uint8Array(encryptedContent), + userId, + ); + } + + const encBuf = await EncArrayBuffer.fromResponse(response); + const key = + attachment.key != null + ? attachment.key + : await firstValueFrom( + this.keyService.orgKeys$(userId).pipe( + filterOutNullish(), + map((orgKeys) => orgKeys[cipherDomain.organizationId as OrganizationId] as OrgKey), + ), + ); + return await this.encryptService.decryptFileData(encBuf, key); + } + /** * @returns a SingleUserState */ @@ -1430,9 +1490,7 @@ export class CipherService implements CipherServiceAbstraction { originalCipher: Cipher, userId: UserId, ): Promise { - const existingCipher = await originalCipher.decrypt( - await this.getKeyForCipherKeyDecryption(originalCipher, userId), - ); + const existingCipher = await this.decrypt(originalCipher, userId); model.passwordHistory = existingCipher.passwordHistory || []; if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { if ( @@ -1852,4 +1910,17 @@ export class CipherService implements CipherServiceAbstraction { ); return featureEnabled && meetsServerVersion; } + + /** + * Decrypts the provided ciphers using the SDK. + * @param ciphers The ciphers to decrypt. + * @param userId The user ID to use for decryption. + * @returns The decrypted ciphers. + * @private + */ + private async decryptCiphersWithSdk(ciphers: Cipher[], userId: UserId): Promise { + const decryptedViews = await this.cipherEncryptionService.decryptManyLegacy(ciphers, userId); + + return decryptedViews.sort(this.getLocaleSortingFunction()); + } } diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts new file mode 100644 index 00000000000..c0b3d3be85f --- /dev/null +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -0,0 +1,334 @@ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { + Fido2Credential, + Cipher as SdkCipher, + CipherType as SdkCipherType, + CipherView as SdkCipherView, + CipherListView, + Attachment as SdkAttachment, +} from "@bitwarden/sdk-internal"; + +import { mockEnc } from "../../../spec"; +import { UriMatchStrategy } from "../../models/domain/domain-service"; +import { LogService } from "../../platform/abstractions/log.service"; +import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { UserId } from "../../types/guid"; +import { CipherRepromptType, CipherType } from "../enums"; +import { CipherPermissionsApi } from "../models/api/cipher-permissions.api"; +import { CipherData } from "../models/data/cipher.data"; +import { Cipher } from "../models/domain/cipher"; +import { AttachmentView } from "../models/view/attachment.view"; +import { CipherView } from "../models/view/cipher.view"; +import { Fido2CredentialView } from "../models/view/fido2-credential.view"; + +import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service"; + +const cipherData: CipherData = { + id: "id", + organizationId: "orgId", + folderId: "folderId", + edit: true, + viewPassword: true, + organizationUseTotp: true, + favorite: false, + revisionDate: "2022-01-31T12:00:00.000Z", + type: CipherType.Login, + name: "EncryptedString", + notes: "EncryptedString", + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: null, + permissions: new CipherPermissionsApi(), + key: "EncKey", + reprompt: CipherRepromptType.None, + login: { + uris: [ + { uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchStrategy.Domain }, + ], + username: "EncryptedString", + password: "EncryptedString", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + totp: "EncryptedString", + autofillOnPageLoad: false, + }, + passwordHistory: [{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }], + attachments: [ + { + id: "a1", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + { + id: "a2", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + ], +}; + +describe("DefaultCipherEncryptionService", () => { + let cipherEncryptionService: DefaultCipherEncryptionService; + const sdkService = mock(); + const logService = mock(); + let sdkCipherView: SdkCipherView; + + const mockSdkClient = { + vault: jest.fn().mockReturnValue({ + ciphers: jest.fn().mockReturnValue({ + decrypt: jest.fn(), + decrypt_list: jest.fn(), + decrypt_fido2_credentials: jest.fn(), + }), + attachments: jest.fn().mockReturnValue({ + decrypt_buffer: jest.fn(), + }), + }), + }; + const mockRef = { + value: mockSdkClient, + [Symbol.dispose]: jest.fn(), + }; + const mockSdk = { + take: jest.fn().mockReturnValue(mockRef), + }; + + const userId = "user-id" as UserId; + + let cipherObj: Cipher; + + beforeEach(() => { + sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any; + cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService); + cipherObj = new Cipher(cipherData); + + jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => { + return { id: cipherData.id } as SdkCipher; + }); + + sdkCipherView = { + id: "test-id", + type: SdkCipherType.Login, + name: "test-name", + login: { + username: "test-username", + password: "test-password", + }, + } as SdkCipherView; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("decrypt", () => { + it("should decrypt a cipher successfully", async () => { + const expectedCipherView: CipherView = { + id: "test-id", + type: CipherType.Login, + name: "test-name", + login: { + username: "test-username", + password: "test-password", + }, + } as CipherView; + + mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView); + jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView); + + const result = await cipherEncryptionService.decrypt(cipherObj, userId); + + expect(result).toEqual(expectedCipherView); + expect(cipherObj.toSdkCipher).toHaveBeenCalledTimes(1); + expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledWith({ id: cipherData.id }); + expect(CipherView.fromSdkCipherView).toHaveBeenCalledWith(sdkCipherView); + expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).not.toHaveBeenCalled(); + }); + + it("should decrypt FIDO2 credentials if present", async () => { + const fido2Credentials = [ + { + credentialId: mockEnc("credentialId"), + keyType: mockEnc("keyType"), + keyAlgorithm: mockEnc("keyAlgorithm"), + keyCurve: mockEnc("keyCurve"), + keyValue: mockEnc("keyValue"), + rpId: mockEnc("rpId"), + userHandle: mockEnc("userHandle"), + userName: mockEnc("userName"), + counter: mockEnc("2"), + rpName: mockEnc("rpName"), + userDisplayName: mockEnc("userDisplayName"), + discoverable: mockEnc("true"), + creationDate: new Date("2023-01-01T12:00:00.000Z"), + }, + ] as unknown as Fido2Credential[]; + + sdkCipherView.login!.fido2Credentials = fido2Credentials; + + const expectedCipherView: CipherView = { + id: "test-id", + type: CipherType.Login, + name: "test-name", + login: { + username: "test-username", + password: "test-password", + fido2Credentials: [], + }, + } as unknown as CipherView; + + const fido2CredentialView: Fido2CredentialView = { + credentialId: "credentialId", + keyType: "keyType", + keyAlgorithm: "keyAlgorithm", + keyCurve: "keyCurve", + keyValue: "decrypted-key-value", + rpId: "rpId", + userHandle: "userHandle", + userName: "userName", + counter: 2, + rpName: "rpName", + userDisplayName: "userDisplayName", + discoverable: true, + creationDate: new Date("2023-01-01T12:00:00.000Z"), + } as unknown as Fido2CredentialView; + + mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView); + mockSdkClient.vault().ciphers().decrypt_fido2_credentials.mockReturnValue(fido2Credentials); + mockSdkClient.vault().ciphers().decrypt_fido2_private_key = jest + .fn() + .mockReturnValue("decrypted-key-value"); + + jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView); + jest + .spyOn(Fido2CredentialView, "fromSdkFido2CredentialView") + .mockReturnValueOnce(fido2CredentialView); + + const result = await cipherEncryptionService.decrypt(cipherObj, userId); + + expect(result).toBe(expectedCipherView); + expect(result.login?.fido2Credentials).toEqual([fido2CredentialView]); + expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).toHaveBeenCalledWith( + sdkCipherView, + ); + expect(mockSdkClient.vault().ciphers().decrypt_fido2_private_key).toHaveBeenCalledWith( + sdkCipherView, + ); + expect(Fido2CredentialView.fromSdkFido2CredentialView).toHaveBeenCalledTimes(1); + }); + }); + + describe("decryptManyLegacy", () => { + it("should decrypt multiple ciphers successfully", async () => { + const ciphers = [new Cipher(cipherData), new Cipher(cipherData)]; + + const expectedViews = [ + { + id: "test-id-1", + name: "test-name-1", + } as CipherView, + { + id: "test-id-2", + name: "test-name-2", + } as CipherView, + ]; + + mockSdkClient + .vault() + .ciphers() + .decrypt.mockReturnValueOnce({ id: "test-id-1", name: "test-name-1" } as SdkCipherView) + .mockReturnValueOnce({ id: "test-id-2", name: "test-name-2" } as SdkCipherView); + + jest + .spyOn(CipherView, "fromSdkCipherView") + .mockReturnValueOnce(expectedViews[0]) + .mockReturnValueOnce(expectedViews[1]); + + const result = await cipherEncryptionService.decryptManyLegacy(ciphers, userId); + + expect(result).toEqual(expectedViews); + expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledTimes(2); + expect(CipherView.fromSdkCipherView).toHaveBeenCalledTimes(2); + }); + + it("should throw EmptyError when SDK is not available", async () => { + sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any; + + await expect( + cipherEncryptionService.decryptManyLegacy([cipherObj], userId), + ).rejects.toThrow(); + + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to decrypt ciphers"), + ); + }); + }); + + describe("decryptMany", () => { + it("should decrypt multiple ciphers to list views", async () => { + const ciphers = [new Cipher(cipherData), new Cipher(cipherData)]; + + const expectedListViews = [ + { id: "list1", name: "List 1" } as CipherListView, + { id: "list2", name: "List 2" } as CipherListView, + ]; + + mockSdkClient.vault().ciphers().decrypt_list.mockReturnValue(expectedListViews); + + const result = await cipherEncryptionService.decryptMany(ciphers, userId); + + expect(result).toEqual(expectedListViews); + expect(mockSdkClient.vault().ciphers().decrypt_list).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ id: cipherData.id }), + expect.objectContaining({ id: cipherData.id }), + ]), + ); + }); + + it("should throw EmptyError when SDK is not available", async () => { + sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any; + + await expect(cipherEncryptionService.decryptMany([cipherObj], userId)).rejects.toThrow(); + + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to decrypt cipher list"), + ); + }); + }); + + describe("decryptAttachmentContent", () => { + it("should decrypt attachment content successfully", async () => { + const cipher = new Cipher(cipherData); + const attachment = new AttachmentView(cipher.attachments![0]); + const encryptedContent = new Uint8Array([1, 2, 3, 4]); + const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]); + + jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher); + jest.spyOn(attachment, "toSdkAttachmentView").mockReturnValue({ id: "a1" } as SdkAttachment); + mockSdkClient.vault().attachments().decrypt_buffer.mockReturnValue(expectedDecryptedContent); + + const result = await cipherEncryptionService.decryptAttachmentContent( + cipher, + attachment, + encryptedContent, + userId, + ); + + expect(result).toEqual(expectedDecryptedContent); + expect(cipher.toSdkCipher).toHaveBeenCalled(); + expect(attachment.toSdkAttachmentView).toHaveBeenCalled(); + expect(mockSdkClient.vault().attachments().decrypt_buffer).toHaveBeenCalledWith( + { id: "id" }, + { id: "a1" }, + encryptedContent, + ); + }); + }); +}); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts new file mode 100644 index 00000000000..2c57df6f5bb --- /dev/null +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -0,0 +1,190 @@ +import { EMPTY, catchError, firstValueFrom, map } from "rxjs"; + +import { CipherListView } from "@bitwarden/sdk-internal"; + +import { LogService } from "../../platform/abstractions/log.service"; +import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { UserId } from "../../types/guid"; +import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; +import { CipherType } from "../enums"; +import { Cipher } from "../models/domain/cipher"; +import { AttachmentView } from "../models/view/attachment.view"; +import { CipherView } from "../models/view/cipher.view"; +import { Fido2CredentialView } from "../models/view/fido2-credential.view"; + +export class DefaultCipherEncryptionService implements CipherEncryptionService { + constructor( + private sdkService: SdkService, + private logService: LogService, + ) {} + + async decrypt(cipher: Cipher, userId: UserId): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher()); + + const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!; + + // Decrypt Fido2 credentials if available + if ( + clientCipherView.type === CipherType.Login && + sdkCipherView.login?.fido2Credentials?.length + ) { + const fido2CredentialViews = ref.value + .vault() + .ciphers() + .decrypt_fido2_credentials(sdkCipherView); + + // TEMPORARY: Manually decrypt the keyValue for Fido2 credentials + // since we don't currently use the SDK for Fido2 Authentication. + const decryptedKeyValue = ref.value + .vault() + .ciphers() + .decrypt_fido2_private_key(sdkCipherView); + + clientCipherView.login.fido2Credentials = fido2CredentialViews + .map((f) => { + const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!; + + return { + ...view, + keyValue: decryptedKeyValue, + }; + }) + .filter((view): view is Fido2CredentialView => view !== undefined); + } + + return clientCipherView; + }), + catchError((error: unknown) => { + this.logService.error(`Failed to decrypt cipher ${error}`); + return EMPTY; + }), + ), + ); + } + + decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + + return ciphers.map((cipher) => { + const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher()); + const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!; + + // Handle FIDO2 credentials if present + if ( + clientCipherView.type === CipherType.Login && + sdkCipherView.login?.fido2Credentials?.length + ) { + const fido2CredentialViews = ref.value + .vault() + .ciphers() + .decrypt_fido2_credentials(sdkCipherView); + + // TODO (PM-21259): Remove manual keyValue decryption for FIDO2 credentials. + // This is a temporary workaround until we can use the SDK for FIDO2 authentication. + const decryptedKeyValue = ref.value + .vault() + .ciphers() + .decrypt_fido2_private_key(sdkCipherView); + + clientCipherView.login.fido2Credentials = fido2CredentialViews + .map((f) => { + const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!; + return { + ...view, + keyValue: decryptedKeyValue, + }; + }) + .filter((view): view is Fido2CredentialView => view !== undefined); + } + + return clientCipherView; + }); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to decrypt ciphers: ${error}`); + return EMPTY; + }), + ), + ); + } + + async decryptMany(ciphers: Cipher[], userId: UserId): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK is undefined"); + } + + using ref = sdk.take(); + + return ref.value + .vault() + .ciphers() + .decrypt_list(ciphers.map((cipher) => cipher.toSdkCipher())); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to decrypt cipher list: ${error}`); + return EMPTY; + }), + ), + ); + } + + /** + * Decrypts an attachment's content from a response object. + * + * @param cipher The encrypted cipher object that owns the attachment + * @param attachment The encrypted attachment object + * @param encryptedContent The encrypted content as a Uint8Array + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to the decrypted content + */ + async decryptAttachmentContent( + cipher: Cipher, + attachment: AttachmentView, + encryptedContent: Uint8Array, + userId: UserId, + ): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK is undefined"); + } + + using ref = sdk.take(); + + return ref.value + .vault() + .attachments() + .decrypt_buffer( + cipher.toSdkCipher(), + attachment.toSdkAttachmentView(), + encryptedContent, + ); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to decrypt cipher buffer: ${error}`); + return EMPTY; + }), + ), + ); + } +} diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index 9284718a063..af29d8263c6 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -118,9 +118,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - const view = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const view = await this.cipherService.decrypt(cipher, activeUserId); this.cleanupCipher(view); this.result.ciphers.push(view); } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index ae408af421b..6ed4caa3f8d 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -10,7 +10,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { UserId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -172,6 +172,8 @@ describe("VaultExportService", () => { let apiService: MockProxy; let fetchMock: jest.Mock; + const userId = "" as UserId; + beforeEach(() => { cryptoFunctionService = mock(); cipherService = mock(); @@ -185,7 +187,6 @@ describe("VaultExportService", () => { keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); - const userId = "" as UserId; const accountInfo: AccountInfo = { email: "", emailVerified: true, @@ -338,7 +339,9 @@ describe("VaultExportService", () => { cipherService.getAllDecrypted.mockResolvedValue([cipherView]); folderService.getAllDecryptedFromState.mockResolvedValue([]); - encryptService.decryptFileData.mockResolvedValue(new Uint8Array(255)); + cipherService.getDecryptedAttachmentBuffer.mockRejectedValue( + new Error("Error decrypting attachment"), + ); global.fetch = jest.fn(() => Promise.resolve({ @@ -356,13 +359,17 @@ describe("VaultExportService", () => { it("contains attachments with folders", async () => { const cipherData = new CipherData(); cipherData.id = "mock-id"; + const cipherRecord: Record = { + ["mock-id" as CipherId]: cipherData, + }; const cipherView = new CipherView(new Cipher(cipherData)); const attachmentView = new AttachmentView(new Attachment(new AttachmentData())); attachmentView.fileName = "mock-file-name"; cipherView.attachments = [attachmentView]; + cipherService.ciphers$.mockReturnValue(of(cipherRecord)); cipherService.getAllDecrypted.mockResolvedValue([cipherView]); folderService.getAllDecryptedFromState.mockResolvedValue([]); - encryptService.decryptFileData.mockResolvedValue(new Uint8Array(255)); + cipherService.getDecryptedAttachmentBuffer.mockResolvedValue(new Uint8Array(255)); global.fetch = jest.fn(() => Promise.resolve({ status: 200, diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 8b66580d4cd..537585aac7e 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -12,14 +12,12 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { UserId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Folder } from "@bitwarden/common/vault/models/domain/folder"; -import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; @@ -118,8 +116,19 @@ export class IndividualVaultExportService const cipherFolder = attachmentsFolder.folder(cipher.id); for (const attachment of cipher.attachments) { const response = await this.downloadAttachment(cipher.id, attachment.id); - const decBuf = await this.decryptAttachment(cipher, attachment, response); - cipherFolder.file(attachment.fileName, decBuf); + + try { + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + cipher.id as CipherId, + attachment, + response, + activeUserId, + ); + + cipherFolder.file(attachment.fileName, decBuf); + } catch { + throw new Error("Error decrypting attachment"); + } } } @@ -146,23 +155,6 @@ export class IndividualVaultExportService return response; } - private async decryptAttachment( - cipher: CipherView, - attachment: AttachmentView, - response: Response, - ) { - try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = - attachment.key != null - ? attachment.key - : await this.keyService.getOrgKey(cipher.organizationId); - return await this.encryptService.decryptFileData(encBuf, key); - } catch { - throw new Error("Error decrypting attachment"); - } - } - private async getDecryptedExport( activeUserId: UserId, format: "json" | "csv", diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index fc46915c15d..4f30f299062 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -155,12 +155,9 @@ export class OrganizationVaultExportService .forEach(async (c) => { const cipher = new Cipher(new CipherData(c)); exportPromises.push( - this.cipherService - .getKeyForCipherKeyDecryption(cipher, activeUserId) - .then((key) => cipher.decrypt(key)) - .then((decCipher) => { - decCiphers.push(decCipher); - }), + this.cipherService.decrypt(cipher, activeUserId).then((decCipher) => { + decCiphers.push(decCipher); + }), ); }); } diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts index 0fe358cd89b..da827addf67 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts @@ -80,6 +80,7 @@ describe("CipherAttachmentsComponent", () => { get: cipherServiceGet, saveAttachmentWithServer, getKeyForCipherKeyDecryption: () => Promise.resolve(null), + decrypt: jest.fn().mockResolvedValue(cipherView), }, }, { diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts index 29a80c826c6..aa9769ec392 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts @@ -137,9 +137,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { this.organization = await this.getOrganization(); this.cipherDomain = await this.getCipher(this.cipherId); - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, this.activeUserId); // Update the initial state of the submit button if (this.submitBtn) { @@ -210,9 +208,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { ); // re-decrypt the cipher to update the attachments - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, this.activeUserId); // Reset reactive form and input element this.fileInput.nativeElement.value = ""; diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 98286e4bbb2..68eac4f0da2 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -3,7 +3,6 @@ import { inject, Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -21,13 +20,10 @@ function isSetEqual(a: Set, b: Set) { export class DefaultCipherFormService implements CipherFormService { private cipherService: CipherService = inject(CipherService); private accountService: AccountService = inject(AccountService); - private apiService: ApiService = inject(ApiService); async decryptCipher(cipher: Cipher): Promise { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - return await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + return await this.cipherService.decrypt(cipher, activeUserId); } async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise { @@ -46,9 +42,7 @@ export class DefaultCipherFormService implements CipherFormService { // Creating a new cipher if (cipher.id == null) { savedCipher = await this.cipherService.createWithServer(encryptedCipher, config.admin); - return await savedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId), - ); + return await this.cipherService.decrypt(savedCipher, activeUserId); } if (config.originalCipher == null) { @@ -100,8 +94,6 @@ export class DefaultCipherFormService implements CipherFormService { return null; } - return await savedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId), - ); + return await this.cipherService.decrypt(savedCipher, activeUserId); } } diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts index f621ca63101..8a4e962707d 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts @@ -6,15 +6,16 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { StateProvider } from "@bitwarden/common/platform/state"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "../../services/password-reprompt.service"; @@ -51,6 +52,21 @@ describe("DownloadAttachmentComponent", () => { }, } as CipherView; + const ciphers$ = new BehaviorSubject({ + "5555-444-3333": { + id: "5555-444-3333", + attachments: [ + { + id: "222-3333-4444", + fileName: "encrypted-filename", + key: "encrypted-key", + }, + ], + }, + }); + + const getFeatureFlag = jest.fn().mockResolvedValue(false); + beforeEach(async () => { showToast.mockClear(); getAttachmentData.mockClear(); @@ -60,13 +76,22 @@ describe("DownloadAttachmentComponent", () => { imports: [DownloadAttachmentComponent], providers: [ { provide: EncryptService, useValue: mock() }, - { provide: KeyService, useValue: mock() }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: StateProvider, useValue: { activeUserId$ } }, { provide: ToastService, useValue: { showToast } }, { provide: ApiService, useValue: { getAttachmentData } }, { provide: FileDownloadService, useValue: { download } }, { provide: PasswordRepromptService, useValue: mock() }, + { + provide: ConfigService, + useValue: { + getFeatureFlag, + }, + }, + { + provide: CipherService, + useValue: { ciphers$: () => ciphers$, getDecryptedAttachmentBuffer: jest.fn() }, + }, ], }).compileComponents(); }); @@ -128,10 +153,12 @@ describe("DownloadAttachmentComponent", () => { }); }); - it("shows an error toast when EncArrayBuffer fails", async () => { + it("shows an error toast when getDecryptedAttachmentBuffer fails", async () => { getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" }); fetchMock.mockResolvedValue({ status: 200 }); - EncArrayBuffer.fromResponse = jest.fn().mockRejectedValue({}); + + const cipherService = TestBed.inject(CipherService) as jest.Mocked; + cipherService.getDecryptedAttachmentBuffer.mockRejectedValue(new Error()); await component.download(); diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index e64777ebb8e..f06d6db582a 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -2,23 +2,19 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { NEVER, switchMap } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { StateProvider } from "@bitwarden/common/platform/state"; -import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid"; -import { OrgKey } from "@bitwarden/common/types/key"; +import { CipherId, EmergencyAccessId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; @Component({ standalone: true, @@ -42,29 +38,14 @@ export class DownloadAttachmentComponent { /** When owners/admins can mange all items and when accessing from the admin console, use the admin endpoint */ @Input() admin?: boolean = false; - /** The organization key if the cipher is associated with one */ - private orgKey: OrgKey | null = null; - constructor( private i18nService: I18nService, private apiService: ApiService, private fileDownloadService: FileDownloadService, private toastService: ToastService, - private encryptService: EncryptService, private stateProvider: StateProvider, - private keyService: KeyService, - ) { - this.stateProvider.activeUserId$ - .pipe( - switchMap((userId) => (userId !== null ? this.keyService.orgKeys$(userId) : NEVER)), - takeUntilDestroyed(), - ) - .subscribe((data: Record | null) => { - if (data) { - this.orgKey = data[this.cipher.organizationId as OrganizationId]; - } - }); - } + private cipherService: CipherService, + ) {} /** Download the attachment */ download = async () => { @@ -100,9 +81,15 @@ export class DownloadAttachmentComponent { } try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = this.attachment.key != null ? this.attachment.key : this.orgKey; - const decBuf = await this.encryptService.decryptFileData(encBuf, key); + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + this.cipher.id as CipherId, + this.attachment, + response, + userId, + ); + this.fileDownloadService.download({ fileName: this.attachment.fileName, blobData: decBuf, From 1a1481bbd643169a554e72437c3f8b78264cfbbc Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 14 May 2025 16:01:03 +0100 Subject: [PATCH 09/13] Resolve the tiny line issue (#14758) --- .../billing/members/free-bitwarden-families.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.html b/apps/web/src/app/billing/members/free-bitwarden-families.component.html index ee21909beec..243cf612c73 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.html +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.html @@ -67,7 +67,9 @@ {{ "resendInvitation" | i18n }} -
+ +
+