-
{{ app.applicationName }}
-
{{ app.atRiskPasswordCount }}
+
0">
+
+
+ {{ "application" | i18n }}
+
+
+ {{ "atRiskPasswords" | i18n }}
+
+
+
+
{{ app.applicationName }}
+
{{ app.atRiskPasswordCount }}
+
+
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/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 973a5fb5aa4..a8638efba18 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -265,6 +265,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";
@@ -283,6 +284,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";
@@ -511,6 +513,7 @@ const safeProviders: SafeProvider[] = [
stateProvider: StateProvider,
accountService: AccountServiceAbstraction,
logService: LogService,
+ cipherEncryptionService: CipherEncryptionService,
) =>
new CipherService(
keyService,
@@ -527,6 +530,7 @@ const safeProviders: SafeProvider[] = [
stateProvider,
accountService,
logService,
+ cipherEncryptionService,
),
deps: [
KeyService,
@@ -543,6 +547,7 @@ const safeProviders: SafeProvider[] = [
StateProvider,
AccountServiceAbstraction,
LogService,
+ CipherEncryptionService,
],
}),
safeProvider({
@@ -1530,6 +1535,11 @@ const safeProviders: SafeProvider[] = [
useClass: MasterPasswordApiService,
deps: [ApiServiceAbstraction, LogService],
}),
+ safeProvider({
+ provide: CipherEncryptionService,
+ useClass: DefaultCipherEncryptionService,
+ deps: [SdkService, LogService],
+ }),
safeProvider({
provide: ChangePasswordService,
useClass: DefaultChangePasswordService,
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 c4312c067b1..97f65a00e2e 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -58,6 +58,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",
@@ -112,6 +113,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.CipherKeyEncryption]: FALSE,
[FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE,
[FeatureFlag.EndUserNotifications]: FALSE,
+ [FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
/* Auth */
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: 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/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;
+ }
+}
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/additional-options/additional-options-section.component.html b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html
index e1eedb36e85..63d809cb8ba 100644
--- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html
+++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html
@@ -1,6 +1,7 @@
-
{{ "additionalOptions" | i18n }}
@@ -30,7 +31,7 @@
{{ "addField" | i18n }}
-
+
{
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/components/autofill-options/autofill-options.component.html b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.html
index bdfdc6ff189..5042dce8608 100644
--- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.html
+++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.html
@@ -1,4 +1,4 @@
-
+
{{ "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 @@
-
+
+
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/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 @@
-