diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index 70c783df52a..14a16211deb 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -64,12 +64,13 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { private async parseEncrypted( results: BitwardenEncryptedIndividualJsonExport | BitwardenEncryptedOrgJsonExport, ) { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (results.encKeyValidation_DO_NOT_EDIT != null) { - let keyForDecryption: SymmetricCryptoKey = await this.keyService.getOrgKey( - this.organizationId, - ); + const orgKeys = await firstValueFrom(this.keyService.orgKeys$(account.id)); + let keyForDecryption: SymmetricCryptoKey = orgKeys?.[this.organizationId]; if (keyForDecryption == null) { - keyForDecryption = await this.keyService.getUserKey(); + keyForDecryption = await firstValueFrom(this.keyService.userKey$(account.id)); } const encKeyValidation = new EncString(results.encKeyValidation_DO_NOT_EDIT); try { @@ -113,10 +114,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { }); } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const view = await this.cipherService.decrypt(cipher, activeUserId); + const view = await this.cipherService.decrypt(cipher, account.id); this.cleanupCipher(view); this.result.ciphers.push(view); } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts index 7812cce2c05..dfdcef51735 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts @@ -1,12 +1,17 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { emptyGuid, OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { newGuid } from "@bitwarden/guid"; import { KdfType, KeyService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/user-core"; import { emptyAccountEncrypted } from "../spec-data/bitwarden-json/account-encrypted.json"; import { emptyUnencryptedExport } from "../spec-data/bitwarden-json/unencrypted.json"; @@ -35,6 +40,36 @@ describe("BitwardenPasswordProtectedImporter", () => { pinService = mock(); accountService = mock(); + accountService.activeAccount$ = of({ + id: newGuid() as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + + const mockOrgId = emptyGuid as OrganizationId; + /* + The key values below are never read, empty objects are cast as types for compilation type checking only. + Tests specific to key contents are in key-service.spec.ts + */ + const mockOrgKey = {} as unknown as OrgKey; + const mockUserKey = {} as unknown as UserKey; + + keyService.orgKeys$.mockImplementation(() => + of({ [mockOrgId]: mockOrgKey } as Record), + ); + keyService.userKey$.mockImplementation(() => of(mockUserKey)); + (keyService as any).activeUserOrgKeys$ = of({ + [mockOrgId]: mockOrgKey, + } as Record); + + /* + Crypto isn’t under test here; keys are just placeholders. + Decryption methods are stubbed to always return empty CipherView or string allowing OK import flow. + */ + cipherService.decrypt.mockResolvedValue({} as any); + encryptService.decryptString.mockResolvedValue("ok"); + importer = new BitwardenPasswordProtectedImporter( keyService, encryptService, @@ -62,6 +97,24 @@ describe("BitwardenPasswordProtectedImporter", () => { jest.spyOn(BitwardenJsonImporter.prototype, "parse"); }); + beforeEach(() => { + accountService.activeAccount$ = of({ + id: newGuid() as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + importer = new BitwardenPasswordProtectedImporter( + keyService, + encryptService, + i18nService, + cipherService, + pinService, + accountService, + promptForPassword_callback, + ); + }); + it("Should call BitwardenJsonImporter", async () => { expect((await importer.parse(emptyAccountEncrypted)).success).toEqual(true); expect(BitwardenJsonImporter.prototype.parse).toHaveBeenCalledWith(emptyAccountEncrypted); 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 3884dde4b18..53952938aa8 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 @@ -15,6 +15,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -22,6 +23,7 @@ import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { newGuid } from "@bitwarden/guid"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { @@ -112,7 +114,7 @@ export class OrganizationVaultExportService type: "text/plain", data: onlyManagedCollections ? await this.getEncryptedManagedExport(userId, organizationId) - : await this.getOrganizationEncryptedExport(organizationId), + : await this.getOrganizationEncryptedExport(userId, organizationId), fileName: ExportHelper.getFileName("org", "encrypted_json"), } as ExportedVaultAsString; } @@ -184,7 +186,10 @@ export class OrganizationVaultExportService return this.buildJsonExport(decCollections, decCiphers); } - private async getOrganizationEncryptedExport(organizationId: OrganizationId): Promise { + private async getOrganizationEncryptedExport( + userId: UserId, + organizationId: OrganizationId, + ): Promise { const collections: Collection[] = []; const ciphers: Cipher[] = []; @@ -215,7 +220,7 @@ export class OrganizationVaultExportService } }); } - return this.BuildEncryptedExport(organizationId, collections, ciphers); + return this.BuildEncryptedExport(userId, organizationId, collections, ciphers); } private async getDecryptedManagedExport( @@ -295,16 +300,21 @@ export class OrganizationVaultExportService !this.restrictedItemTypesService.isCipherRestricted(f, restrictions), ); - return this.BuildEncryptedExport(organizationId, encCollections, encCiphers); + return this.BuildEncryptedExport(activeUserId, organizationId, encCollections, encCiphers); } private async BuildEncryptedExport( + activeUserId: UserId, organizationId: OrganizationId, collections: Collection[], ciphers: Cipher[], ): Promise { - const orgKey = await this.keyService.getOrgKey(organizationId); - const encKeyValidation = await this.encryptService.encryptString(Utils.newGuid(), orgKey); + const orgKeys = await firstValueFrom(this.keyService.orgKeys$(activeUserId)); + const keyForEncryption: SymmetricCryptoKey = orgKeys?.[organizationId]; + if (keyForEncryption == null) { + throw new Error("No encryption key found for organization"); + } + const encKeyValidation = await this.encryptService.encryptString(newGuid(), keyForEncryption); const jsonDoc: BitwardenEncryptedOrgJsonExport = { encrypted: true,