From ea9a18dcad6115c6fff2c6fbdd9343bb5a6ec7f7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 29 Oct 2025 12:59:26 +0100 Subject: [PATCH 1/6] Remove folder usage without provided key --- libs/common/src/vault/models/domain/folder.ts | 4 +-- .../bitwarden/bitwarden-json-importer.ts | 26 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/libs/common/src/vault/models/domain/folder.ts b/libs/common/src/vault/models/domain/folder.ts index 50c67eee01f..3ac6a771830 100644 --- a/libs/common/src/vault/models/domain/folder.ts +++ b/libs/common/src/vault/models/domain/folder.ts @@ -39,8 +39,8 @@ export class Folder extends Domain { this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; } - decrypt(): Promise { - return this.decryptObj(this, new FolderView(this), ["name"], null); + decrypt(key: SymmetricCryptoKey): Promise { + return this.decryptObj(this, new FolderView(this), ["name"], null, key); } async decryptWithKey( diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index 76965a364eb..e70d45174e2 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -15,7 +15,7 @@ import { } from "@bitwarden/common/models/export"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { KeyService } from "@bitwarden/key-management"; @@ -45,6 +45,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { } async parse(data: string): Promise { + const account = await firstValueFrom(this.accountService.activeAccount$); this.result = new ImportResult(); const results: BitwardenJsonExport = JSON.parse(data); if (results == null || results.items == null) { @@ -53,9 +54,9 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { } if (results.encrypted) { - await this.parseEncrypted(results as any); + await this.parseEncrypted(results as any, account.id); } else { - await this.parseDecrypted(results as any); + await this.parseDecrypted(results as any, account.id); } return this.result; @@ -63,14 +64,13 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { private async parseEncrypted( results: BitwardenEncryptedIndividualJsonExport | BitwardenEncryptedOrgJsonExport, + userId: UserId, ) { - const account = await firstValueFrom(this.accountService.activeAccount$); - if (results.encKeyValidation_DO_NOT_EDIT != null) { - const orgKeys = await firstValueFrom(this.keyService.orgKeys$(account.id)); + const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId)); let keyForDecryption: SymmetricCryptoKey = orgKeys?.[this.organizationId]; if (keyForDecryption == null) { - keyForDecryption = await firstValueFrom(this.keyService.userKey$(account.id)); + keyForDecryption = await firstValueFrom(this.keyService.userKey$(userId)); } const encKeyValidation = new EncString(results.encKeyValidation_DO_NOT_EDIT); try { @@ -84,7 +84,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { const groupingsMap = this.organization ? await this.parseCollections(results as BitwardenEncryptedOrgJsonExport) - : await this.parseFolders(results as BitwardenEncryptedIndividualJsonExport); + : await this.parseFolders(results as BitwardenEncryptedIndividualJsonExport, userId); for (const c of results.items) { const cipher = CipherWithIdExport.toDomain(c); @@ -114,7 +114,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { }); } - const view = await this.cipherService.decrypt(cipher, account.id); + const view = await this.cipherService.decrypt(cipher, userId); this.cleanupCipher(view); this.result.ciphers.push(view); } @@ -124,10 +124,11 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { private async parseDecrypted( results: BitwardenUnEncryptedIndividualJsonExport | BitwardenUnEncryptedOrgJsonExport, + userId: UserId, ) { const groupingsMap = this.organization ? await this.parseCollections(results as BitwardenUnEncryptedOrgJsonExport) - : await this.parseFolders(results as BitwardenUnEncryptedIndividualJsonExport); + : await this.parseFolders(results as BitwardenUnEncryptedIndividualJsonExport, userId); results.items.forEach((c) => { const cipher = CipherWithIdExport.toView(c); @@ -166,11 +167,14 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { private async parseFolders( data: BitwardenUnEncryptedIndividualJsonExport | BitwardenEncryptedIndividualJsonExport, + userId: UserId, ): Promise> | null { if (data.folders == null) { return null; } + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + const groupingsMap = new Map(); for (const f of data.folders) { @@ -178,7 +182,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { if (data.encrypted) { const folder = FolderWithIdExport.toDomain(f); if (folder != null) { - folderView = await folder.decrypt(); + folderView = await folder.decrypt(userKey); } } else { folderView = FolderWithIdExport.toView(f); From c042341643e6ec075b550b69263b3f1de62728e3 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 29 Oct 2025 14:02:45 +0100 Subject: [PATCH 2/6] Fix folder test --- libs/common/src/vault/models/domain/folder.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/vault/models/domain/folder.spec.ts b/libs/common/src/vault/models/domain/folder.spec.ts index a837fbb2726..4b1a9222b07 100644 --- a/libs/common/src/vault/models/domain/folder.spec.ts +++ b/libs/common/src/vault/models/domain/folder.spec.ts @@ -33,7 +33,7 @@ describe("Folder", () => { folder.name = mockEnc("encName"); folder.revisionDate = new Date("2022-01-31T12:00:00.000Z"); - const view = await folder.decrypt(); + const view = await folder.decrypt(null); expect(view).toEqual({ id: "id", From e718bed43cb20716936fbf82dea6597e888c8e25 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 29 Oct 2025 14:32:18 +0100 Subject: [PATCH 3/6] Fix build --- apps/cli/src/commands/get.command.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index a994ad3117c..2e0c1b0da97 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -417,10 +417,11 @@ export class GetCommand extends DownloadCommand { private async getFolder(id: string) { let decFolder: FolderView = null; const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId)); if (Utils.isGuid(id)) { const folder = await this.folderService.getFromState(id, activeUserId); if (folder != null) { - decFolder = await folder.decrypt(); + decFolder = await folder.decrypt(userKey); } } else if (id.trim() !== "") { let folders = await this.folderService.getAllDecryptedFromState(activeUserId); From 7334d3df8792a62314d4bf374b67c30ed95c717b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 29 Oct 2025 14:35:50 +0100 Subject: [PATCH 4/6] Fix build --- apps/cli/src/vault/create.command.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 03a205e9c48..5400e223eae 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -181,12 +181,12 @@ export class CreateCommand { private async createFolder(req: FolderExport) { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const userKey = await this.keyService.getUserKey(activeUserId); + const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId)); const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey); try { const folderData = await this.folderApiService.save(folder, activeUserId); const newFolder = new Folder(folderData); - const decFolder = await newFolder.decrypt(); + const decFolder = await newFolder.decrypt(userKey); const res = new FolderResponse(decFolder); return Response.success(res); } catch (e) { From 9718b630b31fc5709709a4ac54f3c9838b59c13f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 29 Oct 2025 14:43:46 +0100 Subject: [PATCH 5/6] Fix build --- apps/cli/src/commands/edit.command.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 14a218c7141..d95e8333dca 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -186,15 +186,15 @@ export class EditCommand { return Response.notFound(); } - let folderView = await folder.decrypt(); + const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId)); + let folderView = await folder.decrypt(userKey); folderView = FolderExport.toView(req, folderView); - const userKey = await this.keyService.getUserKey(activeUserId); const encFolder = await this.folderService.encrypt(folderView, userKey); try { const folder = await this.folderApiService.save(encFolder, activeUserId); const updatedFolder = new Folder(folder); - const decFolder = await updatedFolder.decrypt(); + const decFolder = await updatedFolder.decrypt(userKey); const res = new FolderResponse(decFolder); return Response.success(res); } catch (e) { From 7d692087022971418b552de4d161d3719c394e0b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 29 Oct 2025 16:12:28 +0100 Subject: [PATCH 6/6] Fix tests --- .../src/vault/models/domain/cipher.spec.ts | 25 +++++++------------ .../src/vault/services/cipher.service.ts | 13 +++++++--- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 4052c9e5338..1ef1449ef5c 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -39,6 +39,8 @@ import { IdentityView } from "../../models/view/identity.view"; import { LoginView } from "../../models/view/login.view"; import { CipherPermissionsApi } from "../api/cipher-permissions.api"; +const mockSymmetricKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + describe("Cipher DTO", () => { it("Convert from empty CipherData", () => { const data = new CipherData(); @@ -100,6 +102,9 @@ describe("Cipher DTO", () => { const cipherService = mock(); encryptService.unwrapSymmetricKey.mockRejectedValue(new Error("Failed to unwrap key")); + cipherService.getKeyForCipherKeyDecryption.mockResolvedValue( + new SymmetricCryptoKey(makeStaticByteArray(64)), + ); (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); @@ -319,7 +324,6 @@ describe("Cipher DTO", () => { const keyService = mock(); const encryptService = mock(); - const cipherService = mock(); encryptService.unwrapSymmetricKey.mockResolvedValue( new SymmetricCryptoKey(makeStaticByteArray(64)), @@ -327,9 +331,7 @@ describe("Cipher DTO", () => { (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); - const cipherView = await cipher.decrypt( - await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), - ); + const cipherView = await cipher.decrypt(mockSymmetricKey); expect(cipherView).toMatchObject({ id: "id", @@ -447,7 +449,6 @@ describe("Cipher DTO", () => { const keyService = mock(); const encryptService = mock(); - const cipherService = mock(); encryptService.unwrapSymmetricKey.mockResolvedValue( new SymmetricCryptoKey(makeStaticByteArray(64)), @@ -455,9 +456,7 @@ describe("Cipher DTO", () => { (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); - const cipherView = await cipher.decrypt( - await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), - ); + const cipherView = await cipher.decrypt(mockSymmetricKey); expect(cipherView).toMatchObject({ id: "id", @@ -593,7 +592,6 @@ describe("Cipher DTO", () => { const keyService = mock(); const encryptService = mock(); - const cipherService = mock(); encryptService.unwrapSymmetricKey.mockResolvedValue( new SymmetricCryptoKey(makeStaticByteArray(64)), @@ -601,9 +599,7 @@ describe("Cipher DTO", () => { (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); - const cipherView = await cipher.decrypt( - await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), - ); + const cipherView = await cipher.decrypt(mockSymmetricKey); expect(cipherView).toMatchObject({ id: "id", @@ -763,7 +759,6 @@ describe("Cipher DTO", () => { const keyService = mock(); const encryptService = mock(); - const cipherService = mock(); encryptService.unwrapSymmetricKey.mockResolvedValue( new SymmetricCryptoKey(makeStaticByteArray(64)), @@ -771,9 +766,7 @@ describe("Cipher DTO", () => { (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); - const cipherView = await cipher.decrypt( - await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), - ); + const cipherView = await cipher.decrypt(mockSymmetricKey); expect(cipherView).toMatchObject({ id: "id", diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index efe7bc2b89b..d6f60b4f819 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1533,10 +1533,15 @@ export class CipherService implements CipherServiceAbstraction { } async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise { - return ( - (await this.keyService.getOrgKey(cipher.organizationId)) || - ((await this.keyService.getUserKey(userId)) as UserKey) - ); + if (cipher.organizationId == null) { + return await firstValueFrom(this.keyService.userKey$(userId)); + } else { + return await firstValueFrom( + this.keyService + .orgKeys$(userId) + .pipe(map((orgKeys) => orgKeys[cipher.organizationId as OrganizationId] as OrgKey)), + ); + } } async setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId) {