From d5de9cbeb2911319426afed17cb347fc5cbe91c8 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:38:16 +0000 Subject: [PATCH] [AC-1492] Split export service (#7462) * Split export service into vault and org export service * Changed CLI logic to use split export logic * correct unit tests * Created individual export service, export service making the calls for org and ind vault * Improved code readability * Merged PasswordProtectedExport with Export methods to simplify calls * Some small refactor * [AC-1492] Managed collections export (#7556) * Added managed collections export method Added logic to show orgs on export that the user can export from * Merge branch 'tools/AC-1492/split-export-services' into tools/AC-1492/export-flexible-collections # Conflicts: # apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts # apps/web/src/app/tools/vault-export/export.component.ts * Change export to use new organization.flexiblecollection flag * Little refactor changing parameter names and reduzing the size of export.component.ts ngOnInit * Removed unused service from export constructor and removed unnecessary default value from org export service parameter * Simplified organizations selection for vault export to only verify if it has flexiblecollections * removed unecessary services from ExportComponent constructor on popup * Fixed possible race condition on managed export --- .../browser/src/background/main.background.ts | 22 +- apps/cli/src/bw.ts | 22 +- apps/cli/src/tools/export.command.ts | 34 +- .../org-vault-export.component.ts | 16 +- .../tools/vault-export/export.component.html | 26 +- .../tools/vault-export/export.component.ts | 5 +- .../src/services/jslib-services.module.ts | 24 +- .../export/components/export.component.ts | 71 +-- libs/exporter/src/vault-export/index.ts | 4 + .../services/base-vault-export.service.ts | 95 ++++ ...vidual-vault-export.service.abstraction.ts | 6 + ...> individual-vault-export.service.spec.ts} | 10 +- .../individual-vault-export.service.ts | 185 ++++++++ .../org-vault-export.service.abstraction.ts | 14 + .../services/org-vault-export.service.ts | 304 ++++++++++++ .../vault-export.service.abstraction.ts | 10 +- .../services/vault-export.service.ts | 437 ++---------------- 17 files changed, 789 insertions(+), 496 deletions(-) create mode 100644 libs/exporter/src/vault-export/services/base-vault-export.service.ts create mode 100644 libs/exporter/src/vault-export/services/individual-vault-export.service.abstraction.ts rename libs/exporter/src/vault-export/services/{vault-export.service.spec.ts => individual-vault-export.service.spec.ts} (96%) create mode 100644 libs/exporter/src/vault-export/services/individual-vault-export.service.ts create mode 100644 libs/exporter/src/vault-export/services/org-vault-export.service.abstraction.ts create mode 100644 libs/exporter/src/vault-export/services/org-vault-export.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d4d9b2f6e1..ce5ef11933 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -123,6 +123,10 @@ import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync- import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { + IndividualVaultExportService, + IndividualVaultExportServiceAbstraction, + OrganizationVaultExportService, + OrganizationVaultExportServiceAbstraction, VaultExportService, VaultExportServiceAbstraction, } from "@bitwarden/exporter/vault-export"; @@ -253,6 +257,8 @@ export default class MainBackground { derivedStateProvider: DerivedStateProvider; stateProvider: StateProvider; fido2Service: Fido2ServiceAbstraction; + individualVaultExportService: IndividualVaultExportServiceAbstraction; + organizationVaultExportService: OrganizationVaultExportServiceAbstraction; // Passed to the popup for Safari to workaround issues with theming, downloading, etc. backgroundWindow = window; @@ -635,14 +641,28 @@ export default class MainBackground { this.cryptoService, ); - this.exportService = new VaultExportService( + this.individualVaultExportService = new IndividualVaultExportService( this.folderService, + this.cipherService, + this.cryptoService, + this.cryptoFunctionService, + this.stateService, + ); + + this.organizationVaultExportService = new OrganizationVaultExportService( this.cipherService, this.apiService, this.cryptoService, this.cryptoFunctionService, this.stateService, + this.collectionService, ); + + this.exportService = new VaultExportService( + this.individualVaultExportService, + this.organizationVaultExportService, + ); + this.notificationsService = new NotificationsService( this.logService, this.syncService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index cd62c10c00..97f2aef923 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -88,6 +88,10 @@ import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync- import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { + IndividualVaultExportService, + IndividualVaultExportServiceAbstraction, + OrganizationVaultExportService, + OrganizationVaultExportServiceAbstraction, VaultExportService, VaultExportServiceAbstraction, } from "@bitwarden/exporter/vault-export"; @@ -146,6 +150,8 @@ export class Main { importService: ImportServiceAbstraction; importApiService: ImportApiServiceAbstraction; exportService: VaultExportServiceAbstraction; + individualExportService: IndividualVaultExportServiceAbstraction; + organizationExportService: OrganizationVaultExportServiceAbstraction; searchService: SearchService; cryptoFunctionService: NodeCryptoFunctionService; encryptService: EncryptServiceImplementation; @@ -509,13 +515,27 @@ export class Main { this.collectionService, this.cryptoService, ); - this.exportService = new VaultExportService( + + this.individualExportService = new IndividualVaultExportService( this.folderService, + this.cipherService, + this.cryptoService, + this.cryptoFunctionService, + this.stateService, + ); + + this.organizationExportService = new OrganizationVaultExportService( this.cipherService, this.apiService, this.cryptoService, this.cryptoFunctionService, this.stateService, + this.collectionService, + ); + + this.exportService = new VaultExportService( + this.individualExportService, + this.organizationExportService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); diff --git a/apps/cli/src/tools/export.command.ts b/apps/cli/src/tools/export.command.ts index 4d4e81530d..a2ee8dafac 100644 --- a/apps/cli/src/tools/export.command.ts +++ b/apps/cli/src/tools/export.command.ts @@ -32,7 +32,14 @@ export class ExportCommand { ); } - const format = options.format ?? "csv"; + let password = options.password; + + // has password and format is 'json' => should have the same behaviour as 'encrypted_json' + // format is 'undefined' => Defaults to 'csv' + // Any other case => returns the options.format + const format = + password && options.format == "json" ? "encrypted_json" : options.format ?? "csv"; + if (!this.isSupportedExportFormat(format)) { return Response.badRequest( `'${format}' is not a supported export format. Supported formats: ${EXPORT_FORMATS.join( @@ -47,10 +54,18 @@ export class ExportCommand { let exportContent: string = null; try { + if (format === "encrypted_json") { + password = await this.promptPassword(password); + } + exportContent = - format === "encrypted_json" - ? await this.getProtectedExport(options.password, options.organizationid) - : await this.getUnprotectedExport(format, options.organizationid); + options.organizationid == null + ? await this.exportService.getExport(format, password) + : await this.exportService.getOrganizationExport( + options.organizationid, + format, + password, + ); const eventType = options.organizationid ? EventType.Organization_ClientExportedVault @@ -62,17 +77,6 @@ export class ExportCommand { return await this.saveFile(exportContent, options, format); } - private async getProtectedExport(passwordOption: string | boolean, organizationId?: string) { - const password = await this.promptPassword(passwordOption); - return password == null - ? await this.exportService.getExport("encrypted_json", organizationId) - : await this.exportService.getPasswordProtectedExport(password, organizationId); - } - - private async getUnprotectedExport(format: ExportFormat, organizationId?: string) { - return this.exportService.getExport(format, organizationId); - } - private async saveFile( exportContent: string, options: program.OptionValues, diff --git a/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts b/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts index 8674d09366..38c2f99c87 100644 --- a/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts @@ -1,7 +1,6 @@ import { Component } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { map, switchMap } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -62,20 +61,15 @@ export class OrganizationVaultExportComponent extends ExportComponent { this.organizationId = params.organizationId; }); - this.flexibleCollectionsEnabled$ = this.route.parent.parent.params.pipe( - switchMap((params) => this.organizationService.get$(params.organizationId)), - map((organization) => organization.flexibleCollections), - ); - await super.ngOnInit(); } getExportData() { - if (this.isFileEncryptedExport) { - return this.exportService.getPasswordProtectedExport(this.filePassword, this.organizationId); - } else { - return this.exportService.getOrganizationExport(this.organizationId, this.format); - } + return this.exportService.getOrganizationExport( + this.organizationId, + this.format, + this.filePassword, + ); } getFileName() { diff --git a/apps/web/src/app/tools/vault-export/export.component.html b/apps/web/src/app/tools/vault-export/export.component.html index 7e68becfd2..36288b26cb 100644 --- a/apps/web/src/app/tools/vault-export/export.component.html +++ b/apps/web/src/app/tools/vault-export/export.component.html @@ -15,18 +15,20 @@ *ngIf="!disabledByPolicy" > - - {{ "exportFrom" | i18n }} - - - - - + + + {{ "exportFrom" | i18n }} + + + + + + {{ "fileFormat" | i18n }} diff --git a/apps/web/src/app/tools/vault-export/export.component.ts b/apps/web/src/app/tools/vault-export/export.component.ts index 591422b687..2144f73c29 100644 --- a/apps/web/src/app/tools/vault-export/export.component.ts +++ b/apps/web/src/app/tools/vault-export/export.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; -import { Observable, firstValueFrom } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -25,9 +25,6 @@ export class ExportComponent extends BaseExportComponent { encryptedExportType = EncryptedExportType; protected showFilePassword: boolean; - // Used in the OrganizationVaultExport subclass - protected flexibleCollectionsEnabled$ = new Observable(); - constructor( i18nService: I18nService, platformUtilsService: PlatformUtilsService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 57e606964b..f75ae1ea12 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -172,6 +172,10 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultExportService, VaultExportServiceAbstraction, + OrganizationVaultExportService, + OrganizationVaultExportServiceAbstraction, + IndividualVaultExportService, + IndividualVaultExportServiceAbstraction, } from "@bitwarden/exporter/vault-export"; import { ImportApiService, @@ -537,17 +541,33 @@ import { ModalService } from "./modal.service"; ], }, { - provide: VaultExportServiceAbstraction, - useClass: VaultExportService, + provide: IndividualVaultExportServiceAbstraction, + useClass: IndividualVaultExportService, deps: [ FolderServiceAbstraction, + CipherServiceAbstraction, + CryptoServiceAbstraction, + CryptoFunctionServiceAbstraction, + StateServiceAbstraction, + ], + }, + { + provide: OrganizationVaultExportServiceAbstraction, + useClass: OrganizationVaultExportService, + deps: [ CipherServiceAbstraction, ApiServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, StateServiceAbstraction, + CollectionServiceAbstraction, ], }, + { + provide: VaultExportServiceAbstraction, + useClass: VaultExportService, + deps: [IndividualVaultExportServiceAbstraction, OrganizationVaultExportServiceAbstraction], + }, { provide: SearchServiceAbstraction, useClass: SearchService, diff --git a/libs/angular/src/tools/export/components/export.component.ts b/libs/angular/src/tools/export/components/export.component.ts index b24759c821..bd36ae3039 100644 --- a/libs/angular/src/tools/export/components/export.component.ts +++ b/libs/angular/src/tools/export/components/export.component.ts @@ -1,12 +1,9 @@ import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core"; import { UntypedFormBuilder, Validators } from "@angular/forms"; -import { concat, map, merge, Observable, startWith, Subject, takeUntil } from "rxjs"; +import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { - OrganizationService, - canAccessImportExport, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -31,6 +28,7 @@ export class ExportComponent implements OnInit, OnDestroy { filePasswordValue: string = null; formPromise: Promise; private _disabledByPolicy = false; + protected organizationId: string = null; organizations$: Observable; @@ -76,13 +74,6 @@ export class ExportComponent implements OnInit, OnDestroy { ) {} async ngOnInit() { - this.organizations$ = concat( - this.organizationService.memberOrganizations$.pipe( - canAccessImportExport(this.i18nService), - map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))), - ), - ); - this.policyService .policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport) .pipe(takeUntil(this.destroy$)) @@ -93,19 +84,6 @@ export class ExportComponent implements OnInit, OnDestroy { } }); - if (this.organizationId) { - this.exportForm.controls.vaultSelector.patchValue(this.organizationId); - this.exportForm.controls.vaultSelector.disable(); - } else { - this.exportForm.controls.vaultSelector.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe((value) => { - this.organizationId = value != "myVault" ? value : undefined; - }); - - this.exportForm.controls.vaultSelector.setValue("myVault"); - } - merge( this.exportForm.get("format").valueChanges, this.exportForm.get("fileEncryptionType").valueChanges, @@ -113,6 +91,31 @@ export class ExportComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .pipe(startWith(0)) .subscribe(() => this.adjustValidators()); + + if (this.organizationId) { + this.organizations$ = this.organizationService.memberOrganizations$.pipe( + map((orgs) => orgs.filter((org) => org.id == this.organizationId)), + ); + this.exportForm.controls.vaultSelector.patchValue(this.organizationId); + this.exportForm.controls.vaultSelector.disable(); + return; + } + + this.organizations$ = this.organizationService.memberOrganizations$.pipe( + map((orgs) => + orgs + .filter((org) => org.flexibleCollections) + .sort(Utils.getSortFunction(this.i18nService, "name")), + ), + ); + + this.exportForm.controls.vaultSelector.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((value) => { + this.organizationId = value != "myVault" ? value : undefined; + }); + + this.exportForm.controls.vaultSelector.setValue("myVault"); } ngOnDestroy(): void { @@ -188,15 +191,15 @@ export class ExportComponent implements OnInit, OnDestroy { this.onSaved.emit(); } - protected getExportData() { - if ( - this.format === "encrypted_json" && - this.fileEncryptionType === EncryptedExportType.FileEncrypted - ) { - return this.exportService.getPasswordProtectedExport(this.filePassword); - } else { - return this.exportService.getExport(this.format, null); - } + protected async getExportData(): Promise { + return Utils.isNullOrWhitespace(this.organizationId) + ? this.exportService.getExport(this.format, this.filePassword) + : this.exportService.getOrganizationExport( + this.organizationId, + this.format, + this.filePassword, + true, + ); } protected getFileName(prefix?: string) { diff --git a/libs/exporter/src/vault-export/index.ts b/libs/exporter/src/vault-export/index.ts index c7bc4a957a..e7eae409d5 100644 --- a/libs/exporter/src/vault-export/index.ts +++ b/libs/exporter/src/vault-export/index.ts @@ -1,2 +1,6 @@ export * from "./services/vault-export.service.abstraction"; export * from "./services/vault-export.service"; +export * from "./services/org-vault-export.service.abstraction"; +export * from "./services/org-vault-export.service"; +export * from "./services/individual-vault-export.service.abstraction"; +export * from "./services/individual-vault-export.service"; diff --git a/libs/exporter/src/vault-export/services/base-vault-export.service.ts b/libs/exporter/src/vault-export/services/base-vault-export.service.ts new file mode 100644 index 0000000000..53cfd04174 --- /dev/null +++ b/libs/exporter/src/vault-export/services/base-vault-export.service.ts @@ -0,0 +1,95 @@ +import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { BitwardenCsvExportType } from "../bitwarden-csv-export-type"; +import { BitwardenPasswordProtectedFileFormat } from "../bitwarden-json-export-types"; + +export class BaseVaultExportService { + constructor( + protected cryptoService: CryptoService, + private cryptoFunctionService: CryptoFunctionService, + private stateService: StateService, + ) {} + + protected async buildPasswordExport(clearText: string, password: string): Promise { + const kdfType: KdfType = await this.stateService.getKdfType(); + const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + + const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); + const key = await this.cryptoService.makePinKey(password, salt, kdfType, kdfConfig); + + const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key); + const encText = await this.cryptoService.encrypt(clearText, key); + + const jsonDoc: BitwardenPasswordProtectedFileFormat = { + encrypted: true, + passwordProtected: true, + salt: salt, + kdfType: kdfType, + kdfIterations: kdfConfig.iterations, + kdfMemory: kdfConfig.memory, + kdfParallelism: kdfConfig.parallelism, + encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, + data: encText.encryptedString, + }; + + return JSON.stringify(jsonDoc, null, " "); + } + + protected buildCommonCipher( + cipher: BitwardenCsvExportType, + c: CipherView, + ): BitwardenCsvExportType { + cipher.type = null; + cipher.name = c.name; + cipher.notes = c.notes; + cipher.fields = null; + cipher.reprompt = c.reprompt; + // Login props + cipher.login_uri = null; + cipher.login_username = null; + cipher.login_password = null; + cipher.login_totp = null; + + if (c.fields) { + c.fields.forEach((f) => { + if (!cipher.fields) { + cipher.fields = ""; + } else { + cipher.fields += "\n"; + } + + cipher.fields += (f.name || "") + ": " + f.value; + }); + } + + switch (c.type) { + case CipherType.Login: + cipher.type = "login"; + cipher.login_username = c.login.username; + cipher.login_password = c.login.password; + cipher.login_totp = c.login.totp; + + if (c.login.uris) { + cipher.login_uri = []; + c.login.uris.forEach((u) => { + cipher.login_uri.push(u.uri); + }); + } + break; + case CipherType.SecureNote: + cipher.type = "note"; + break; + default: + return; + } + + return cipher; + } +} diff --git a/libs/exporter/src/vault-export/services/individual-vault-export.service.abstraction.ts b/libs/exporter/src/vault-export/services/individual-vault-export.service.abstraction.ts new file mode 100644 index 0000000000..5f296ecd0e --- /dev/null +++ b/libs/exporter/src/vault-export/services/individual-vault-export.service.abstraction.ts @@ -0,0 +1,6 @@ +import { ExportFormat } from "./vault-export.service.abstraction"; + +export abstract class IndividualVaultExportServiceAbstraction { + getExport: (format: ExportFormat) => Promise; + getPasswordProtectedExport: (password: string) => Promise; +} diff --git a/libs/exporter/src/vault-export/services/vault-export.service.spec.ts b/libs/exporter/src/vault-export/services/individual-vault-export.service.spec.ts similarity index 96% rename from libs/exporter/src/vault-export/services/vault-export.service.spec.ts rename to libs/exporter/src/vault-export/services/individual-vault-export.service.spec.ts index b4d0dbb916..f1bd2d130c 100644 --- a/libs/exporter/src/vault-export/services/vault-export.service.spec.ts +++ b/libs/exporter/src/vault-export/services/individual-vault-export.service.spec.ts @@ -1,6 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -21,7 +20,7 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { BuildTestObject, GetUniqueString } from "../../../../common/spec"; -import { VaultExportService } from "./vault-export.service"; +import { IndividualVaultExportService } from "./individual-vault-export.service"; const UserCipherViews = [ generateCipherView(false), @@ -140,8 +139,7 @@ function expectEqualFolders(folders: Folder[], jsonResult: string) { } describe("VaultExportService", () => { - let exportService: VaultExportService; - let apiService: MockProxy; + let exportService: IndividualVaultExportService; let cryptoFunctionService: MockProxy; let cipherService: MockProxy; let folderService: MockProxy; @@ -149,7 +147,6 @@ describe("VaultExportService", () => { let stateService: MockProxy; beforeEach(() => { - apiService = mock(); cryptoFunctionService = mock(); cipherService = mock(); folderService = mock(); @@ -162,10 +159,9 @@ describe("VaultExportService", () => { stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); - exportService = new VaultExportService( + exportService = new IndividualVaultExportService( folderService, cipherService, - apiService, cryptoService, cryptoFunctionService, stateService, diff --git a/libs/exporter/src/vault-export/services/individual-vault-export.service.ts b/libs/exporter/src/vault-export/services/individual-vault-export.service.ts new file mode 100644 index 0000000000..6dcece2634 --- /dev/null +++ b/libs/exporter/src/vault-export/services/individual-vault-export.service.ts @@ -0,0 +1,185 @@ +import * as papa from "papaparse"; + +import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; + +import { BitwardenCsvIndividualExportType } from "../bitwarden-csv-export-type"; +import { + BitwardenEncryptedIndividualJsonExport, + BitwardenUnEncryptedIndividualJsonExport, +} from "../bitwarden-json-export-types"; + +import { BaseVaultExportService } from "./base-vault-export.service"; +import { IndividualVaultExportServiceAbstraction } from "./individual-vault-export.service.abstraction"; +import { ExportFormat } from "./vault-export.service.abstraction"; + +export class IndividualVaultExportService + extends BaseVaultExportService + implements IndividualVaultExportServiceAbstraction +{ + constructor( + private folderService: FolderService, + private cipherService: CipherService, + cryptoService: CryptoService, + cryptoFunctionService: CryptoFunctionService, + stateService: StateService, + ) { + super(cryptoService, cryptoFunctionService, stateService); + } + + async getExport(format: ExportFormat = "csv"): Promise { + if (format === "encrypted_json") { + return this.getEncryptedExport(); + } + return this.getDecryptedExport(format); + } + + async getPasswordProtectedExport(password: string): Promise { + const clearText = await this.getExport("json"); + return this.buildPasswordExport(clearText, password); + } + + private async getDecryptedExport(format: "json" | "csv"): Promise { + let decFolders: FolderView[] = []; + let decCiphers: CipherView[] = []; + const promises = []; + + promises.push( + this.folderService.getAllDecryptedFromState().then((folders) => { + decFolders = folders; + }), + ); + + promises.push( + this.cipherService.getAllDecrypted().then((ciphers) => { + decCiphers = ciphers.filter((f) => f.deletedDate == null); + }), + ); + + await Promise.all(promises); + + if (format === "csv") { + return this.buildCsvExport(decFolders, decCiphers); + } + + return this.buildJsonExport(decFolders, decCiphers); + } + + private async getEncryptedExport(): Promise { + let folders: Folder[] = []; + let ciphers: Cipher[] = []; + const promises = []; + + promises.push( + this.folderService.getAllFromState().then((f) => { + folders = f; + }), + ); + + promises.push( + this.cipherService.getAll().then((c) => { + ciphers = c.filter((f) => f.deletedDate == null); + }), + ); + + await Promise.all(promises); + + const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid()); + + const jsonDoc: BitwardenEncryptedIndividualJsonExport = { + encrypted: true, + encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, + folders: [], + items: [], + }; + + folders.forEach((f) => { + if (f.id == null) { + return; + } + const folder = new FolderWithIdExport(); + folder.build(f); + jsonDoc.folders.push(folder); + }); + + ciphers.forEach((c) => { + if (c.organizationId != null) { + return; + } + const cipher = new CipherWithIdExport(); + cipher.build(c); + cipher.collectionIds = null; + jsonDoc.items.push(cipher); + }); + + return JSON.stringify(jsonDoc, null, " "); + } + + private buildCsvExport(decFolders: FolderView[], decCiphers: CipherView[]): string { + const foldersMap = new Map(); + decFolders.forEach((f) => { + if (f.id != null) { + foldersMap.set(f.id, f); + } + }); + + const exportCiphers: BitwardenCsvIndividualExportType[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + if (c.organizationId != null) { + return; + } + + const cipher = {} as BitwardenCsvIndividualExportType; + cipher.folder = + c.folderId != null && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null; + cipher.favorite = c.favorite ? 1 : null; + this.buildCommonCipher(cipher, c); + exportCiphers.push(cipher); + }); + + return papa.unparse(exportCiphers); + } + + private buildJsonExport(decFolders: FolderView[], decCiphers: CipherView[]): string { + const jsonDoc: BitwardenUnEncryptedIndividualJsonExport = { + encrypted: false, + folders: [], + items: [], + }; + + decFolders.forEach((f) => { + if (f.id == null) { + return; + } + const folder = new FolderWithIdExport(); + folder.build(f); + jsonDoc.folders.push(folder); + }); + + decCiphers.forEach((c) => { + if (c.organizationId != null) { + return; + } + const cipher = new CipherWithIdExport(); + cipher.build(c); + cipher.collectionIds = null; + jsonDoc.items.push(cipher); + }); + + return JSON.stringify(jsonDoc, null, " "); + } +} diff --git a/libs/exporter/src/vault-export/services/org-vault-export.service.abstraction.ts b/libs/exporter/src/vault-export/services/org-vault-export.service.abstraction.ts new file mode 100644 index 0000000000..b5938390d0 --- /dev/null +++ b/libs/exporter/src/vault-export/services/org-vault-export.service.abstraction.ts @@ -0,0 +1,14 @@ +import { ExportFormat } from "./vault-export.service.abstraction"; + +export abstract class OrganizationVaultExportServiceAbstraction { + getPasswordProtectedExport: ( + organizationId: string, + password: string, + onlyManagedCollections: boolean, + ) => Promise; + getOrganizationExport: ( + organizationId: string, + format: ExportFormat, + onlyManagedCollections: boolean, + ) => Promise; +} diff --git a/libs/exporter/src/vault-export/services/org-vault-export.service.ts b/libs/exporter/src/vault-export/services/org-vault-export.service.ts new file mode 100644 index 0000000000..fad4fd2f1a --- /dev/null +++ b/libs/exporter/src/vault-export/services/org-vault-export.service.ts @@ -0,0 +1,304 @@ +import * as papa from "papaparse"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; +import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { Collection } from "@bitwarden/common/vault/models/domain/collection"; +import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; + +import { BitwardenCsvOrgExportType } from "../bitwarden-csv-export-type"; +import { + BitwardenEncryptedOrgJsonExport, + BitwardenUnEncryptedOrgJsonExport, +} from "../bitwarden-json-export-types"; + +import { BaseVaultExportService } from "./base-vault-export.service"; +import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction"; +import { ExportFormat } from "./vault-export.service.abstraction"; + +export class OrganizationVaultExportService + extends BaseVaultExportService + implements OrganizationVaultExportServiceAbstraction +{ + constructor( + private cipherService: CipherService, + private apiService: ApiService, + cryptoService: CryptoService, + cryptoFunctionService: CryptoFunctionService, + stateService: StateService, + private collectionService: CollectionService, + ) { + super(cryptoService, cryptoFunctionService, stateService); + } + + async getPasswordProtectedExport( + organizationId: string, + password: string, + onlyManagedCollections: boolean, + ): Promise { + const clearText = await this.getOrganizationExport( + organizationId, + "json", + onlyManagedCollections, + ); + + return this.buildPasswordExport(clearText, password); + } + + async getOrganizationExport( + organizationId: string, + format: ExportFormat = "csv", + onlyManagedCollections: boolean, + ): Promise { + if (Utils.isNullOrWhitespace(organizationId)) { + throw new Error("OrganizationId must be set"); + } + + if (format === "encrypted_json") { + return onlyManagedCollections + ? this.getEncryptedManagedExport(organizationId) + : this.getOrganizationEncryptedExport(organizationId); + } + + return onlyManagedCollections + ? this.getDecryptedManagedExport(organizationId, format) + : this.getOrganizationDecryptedExport(organizationId, format); + } + + private async getOrganizationDecryptedExport( + organizationId: string, + format: "json" | "csv", + ): Promise { + const decCollections: CollectionView[] = []; + const decCiphers: CipherView[] = []; + const promises = []; + + promises.push( + this.apiService.getOrganizationExport(organizationId).then((exportData) => { + const exportPromises: any = []; + if (exportData != null) { + if (exportData.collections != null && exportData.collections.length > 0) { + exportData.collections.forEach((c) => { + const collection = new Collection(new CollectionData(c as CollectionDetailsResponse)); + exportPromises.push( + collection.decrypt().then((decCol) => { + decCollections.push(decCol); + }), + ); + }); + } + if (exportData.ciphers != null && exportData.ciphers.length > 0) { + exportData.ciphers + .filter((c) => c.deletedDate === null) + .forEach(async (c) => { + const cipher = new Cipher(new CipherData(c)); + exportPromises.push( + this.cipherService + .getKeyForCipherKeyDecryption(cipher) + .then((key) => cipher.decrypt(key)) + .then((decCipher) => { + decCiphers.push(decCipher); + }), + ); + }); + } + } + return Promise.all(exportPromises); + }), + ); + + await Promise.all(promises); + + if (format === "csv") { + return this.buildCsvExport(decCollections, decCiphers); + } + return this.buildJsonExport(decCollections, decCiphers); + } + + private async getOrganizationEncryptedExport(organizationId: string): Promise { + const collections: Collection[] = []; + const ciphers: Cipher[] = []; + const promises = []; + + promises.push( + this.apiService.getCollections(organizationId).then((c) => { + if (c != null && c.data != null && c.data.length > 0) { + c.data.forEach((r) => { + const collection = new Collection(new CollectionData(r as CollectionDetailsResponse)); + collections.push(collection); + }); + } + }), + ); + + promises.push( + this.apiService.getCiphersOrganization(organizationId).then((c) => { + if (c != null && c.data != null && c.data.length > 0) { + c.data + .filter((item) => item.deletedDate === null) + .forEach((item) => { + const cipher = new Cipher(new CipherData(item)); + ciphers.push(cipher); + }); + } + }), + ); + + await Promise.all(promises); + + return this.BuildEncryptedExport(organizationId, collections, ciphers); + } + + private async getDecryptedManagedExport( + organizationId: string, + format: "json" | "csv", + ): Promise { + let decCiphers: CipherView[] = []; + let allDecCiphers: CipherView[] = []; + let decCollections: CollectionView[] = []; + const promises = []; + + promises.push( + this.collectionService.getAllDecrypted().then(async (collections) => { + decCollections = collections.filter((c) => c.organizationId == organizationId && c.manage); + }), + ); + + promises.push( + this.cipherService.getAllDecrypted().then((ciphers) => { + allDecCiphers = ciphers; + }), + ); + await Promise.all(promises); + + decCiphers = allDecCiphers.filter( + (f) => + f.deletedDate == null && + f.organizationId == organizationId && + decCollections.some((dC) => f.collectionIds.some((cId) => dC.id === cId)), + ); + + if (format === "csv") { + return this.buildCsvExport(decCollections, decCiphers); + } + return this.buildJsonExport(decCollections, decCiphers); + } + + private async getEncryptedManagedExport(organizationId: string): Promise { + let encCiphers: Cipher[] = []; + let allCiphers: Cipher[] = []; + let encCollections: Collection[] = []; + const promises = []; + + promises.push( + this.collectionService.getAll().then((collections) => { + encCollections = collections.filter((c) => c.organizationId == organizationId && c.manage); + }), + ); + + promises.push( + this.cipherService.getAll().then((ciphers) => { + allCiphers = ciphers; + }), + ); + + await Promise.all(promises); + + encCiphers = allCiphers.filter( + (f) => + f.deletedDate == null && + f.organizationId == organizationId && + encCollections.some((eC) => f.collectionIds.some((cId) => eC.id === cId)), + ); + + return this.BuildEncryptedExport(organizationId, encCollections, encCiphers); + } + + private async BuildEncryptedExport( + organizationId: string, + collections: Collection[], + ciphers: Cipher[], + ): Promise { + const orgKey = await this.cryptoService.getOrgKey(organizationId); + const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), orgKey); + + const jsonDoc: BitwardenEncryptedOrgJsonExport = { + encrypted: true, + encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, + collections: [], + items: [], + }; + + collections.forEach((c) => { + const collection = new CollectionWithIdExport(); + collection.build(c); + jsonDoc.collections.push(collection); + }); + + ciphers.forEach((c) => { + const cipher = new CipherWithIdExport(); + cipher.build(c); + jsonDoc.items.push(cipher); + }); + return JSON.stringify(jsonDoc, null, " "); + } + + private buildCsvExport(decCollections: CollectionView[], decCiphers: CipherView[]): string { + const collectionsMap = new Map(); + decCollections.forEach((c) => { + collectionsMap.set(c.id, c); + }); + + const exportCiphers: BitwardenCsvOrgExportType[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + + const cipher = {} as BitwardenCsvOrgExportType; + cipher.collections = []; + if (c.collectionIds != null) { + cipher.collections = c.collectionIds + .filter((id) => collectionsMap.has(id)) + .map((id) => collectionsMap.get(id).name); + } + this.buildCommonCipher(cipher, c); + exportCiphers.push(cipher); + }); + + return papa.unparse(exportCiphers); + } + + private buildJsonExport(decCollections: CollectionView[], decCiphers: CipherView[]): string { + const jsonDoc: BitwardenUnEncryptedOrgJsonExport = { + encrypted: false, + collections: [], + items: [], + }; + + decCollections.forEach((c) => { + const collection = new CollectionWithIdExport(); + collection.build(c); + jsonDoc.collections.push(collection); + }); + + decCiphers.forEach((c) => { + const cipher = new CipherWithIdExport(); + cipher.build(c); + jsonDoc.items.push(cipher); + }); + return JSON.stringify(jsonDoc, null, " "); + } +} diff --git a/libs/exporter/src/vault-export/services/vault-export.service.abstraction.ts b/libs/exporter/src/vault-export/services/vault-export.service.abstraction.ts index 68da83bfae..67a14247d9 100644 --- a/libs/exporter/src/vault-export/services/vault-export.service.abstraction.ts +++ b/libs/exporter/src/vault-export/services/vault-export.service.abstraction.ts @@ -2,8 +2,12 @@ export const EXPORT_FORMATS = ["csv", "json", "encrypted_json"] as const; export type ExportFormat = (typeof EXPORT_FORMATS)[number]; export abstract class VaultExportServiceAbstraction { - getExport: (format?: ExportFormat, organizationId?: string) => Promise; - getPasswordProtectedExport: (password: string, organizationId?: string) => Promise; - getOrganizationExport: (organizationId: string, format?: ExportFormat) => Promise; + getExport: (format: ExportFormat, password: string) => Promise; + getOrganizationExport: ( + organizationId: string, + format: ExportFormat, + password: string, + onlyManagedCollections?: boolean, + ) => Promise; getFileName: (prefix?: string, extension?: string) => string; } diff --git a/libs/exporter/src/vault-export/services/vault-export.service.ts b/libs/exporter/src/vault-export/services/vault-export.service.ts index f4fbcf6b01..00e2a61f7c 100644 --- a/libs/exporter/src/vault-export/services/vault-export.service.ts +++ b/libs/exporter/src/vault-export/services/vault-export.service.ts @@ -1,429 +1,54 @@ -import * as papa from "papaparse"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { - CipherWithIdExport, - CollectionWithIdExport, - FolderWithIdExport, -} from "@bitwarden/common/models/export"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -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 { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; -import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { Collection } from "@bitwarden/common/vault/models/domain/collection"; -import { Folder } from "@bitwarden/common/vault/models/domain/folder"; -import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ExportHelper } from "../../export-helper"; -import { - BitwardenCsvExportType, - BitwardenCsvIndividualExportType, - BitwardenCsvOrgExportType, -} from "../bitwarden-csv-export-type"; -import { - BitwardenEncryptedIndividualJsonExport, - BitwardenEncryptedOrgJsonExport, - BitwardenUnEncryptedIndividualJsonExport, - BitwardenUnEncryptedOrgJsonExport, - BitwardenPasswordProtectedFileFormat, -} from "../bitwarden-json-export-types"; +import { IndividualVaultExportServiceAbstraction } from "./individual-vault-export.service.abstraction"; +import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction"; import { ExportFormat, VaultExportServiceAbstraction } from "./vault-export.service.abstraction"; export class VaultExportService implements VaultExportServiceAbstraction { constructor( - private folderService: FolderService, - private cipherService: CipherService, - private apiService: ApiService, - private cryptoService: CryptoService, - private cryptoFunctionService: CryptoFunctionService, - private stateService: StateService, + private individualVaultExportService: IndividualVaultExportServiceAbstraction, + private organizationVaultExportService: OrganizationVaultExportServiceAbstraction, ) {} - async getExport(format: ExportFormat = "csv", organizationId?: string): Promise { - if (organizationId) { - return await this.getOrganizationExport(organizationId, format); + async getExport(format: ExportFormat = "csv", password: string): Promise { + if (!Utils.isNullOrWhitespace(password)) { + if (format == "csv") { + throw new Error("CSV does not support password protected export"); + } + + return this.individualVaultExportService.getPasswordProtectedExport(password); } - - if (format === "encrypted_json") { - return this.getEncryptedExport(); - } else { - return this.getDecryptedExport(format); - } - } - - async getPasswordProtectedExport(password: string, organizationId?: string): Promise { - const clearText = organizationId - ? await this.getOrganizationExport(organizationId, "json") - : await this.getExport("json"); - - const kdfType: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); - - const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); - const key = await this.cryptoService.makePinKey(password, salt, kdfType, kdfConfig); - - const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key); - const encText = await this.cryptoService.encrypt(clearText, key); - - const jsonDoc: BitwardenPasswordProtectedFileFormat = { - encrypted: true, - passwordProtected: true, - salt: salt, - kdfType: kdfType, - kdfIterations: kdfConfig.iterations, - kdfMemory: kdfConfig.memory, - kdfParallelism: kdfConfig.parallelism, - encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, - data: encText.encryptedString, - }; - - return JSON.stringify(jsonDoc, null, " "); + return this.individualVaultExportService.getExport(format); } async getOrganizationExport( organizationId: string, - format: ExportFormat = "csv", + format: ExportFormat, + password: string, + onlyManagedCollections = false, ): Promise { - if (format === "encrypted_json") { - return this.getOrganizationEncryptedExport(organizationId); - } else { - return this.getOrganizationDecryptedExport(organizationId, format); + if (!Utils.isNullOrWhitespace(password)) { + if (format == "csv") { + throw new Error("CSV does not support password protected export"); + } + + return this.organizationVaultExportService.getPasswordProtectedExport( + organizationId, + password, + onlyManagedCollections, + ); } + + return this.organizationVaultExportService.getOrganizationExport( + organizationId, + format, + onlyManagedCollections, + ); } getFileName(prefix: string = null, extension = "csv"): string { return ExportHelper.getFileName(prefix, extension); } - - private async getDecryptedExport(format: "json" | "csv"): Promise { - let decFolders: FolderView[] = []; - let decCiphers: CipherView[] = []; - const promises = []; - - promises.push( - this.folderService.getAllDecryptedFromState().then((folders) => { - decFolders = folders; - }), - ); - - promises.push( - this.cipherService.getAllDecrypted().then((ciphers) => { - decCiphers = ciphers.filter((f) => f.deletedDate == null); - }), - ); - - await Promise.all(promises); - - if (format === "csv") { - const foldersMap = new Map(); - decFolders.forEach((f) => { - if (f.id != null) { - foldersMap.set(f.id, f); - } - }); - - const exportCiphers: BitwardenCsvIndividualExportType[] = []; - decCiphers.forEach((c) => { - // only export logins and secure notes - if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { - return; - } - if (c.organizationId != null) { - return; - } - - const cipher = {} as BitwardenCsvIndividualExportType; - cipher.folder = - c.folderId != null && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null; - cipher.favorite = c.favorite ? 1 : null; - this.buildCommonCipher(cipher, c); - exportCiphers.push(cipher); - }); - - return papa.unparse(exportCiphers); - } else { - const jsonDoc: BitwardenUnEncryptedIndividualJsonExport = { - encrypted: false, - folders: [], - items: [], - }; - - decFolders.forEach((f) => { - if (f.id == null) { - return; - } - const folder = new FolderWithIdExport(); - folder.build(f); - jsonDoc.folders.push(folder); - }); - - decCiphers.forEach((c) => { - if (c.organizationId != null) { - return; - } - const cipher = new CipherWithIdExport(); - cipher.build(c); - cipher.collectionIds = null; - jsonDoc.items.push(cipher); - }); - - return JSON.stringify(jsonDoc, null, " "); - } - } - - private async getEncryptedExport(): Promise { - let folders: Folder[] = []; - let ciphers: Cipher[] = []; - const promises = []; - - promises.push( - this.folderService.getAllFromState().then((f) => { - folders = f; - }), - ); - - promises.push( - this.cipherService.getAll().then((c) => { - ciphers = c.filter((f) => f.deletedDate == null); - }), - ); - - await Promise.all(promises); - - const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid()); - - const jsonDoc: BitwardenEncryptedIndividualJsonExport = { - encrypted: true, - encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, - folders: [], - items: [], - }; - - folders.forEach((f) => { - if (f.id == null) { - return; - } - const folder = new FolderWithIdExport(); - folder.build(f); - jsonDoc.folders.push(folder); - }); - - ciphers.forEach((c) => { - if (c.organizationId != null) { - return; - } - const cipher = new CipherWithIdExport(); - cipher.build(c); - cipher.collectionIds = null; - jsonDoc.items.push(cipher); - }); - - return JSON.stringify(jsonDoc, null, " "); - } - - private async getOrganizationDecryptedExport( - organizationId: string, - format: "json" | "csv", - ): Promise { - const decCollections: CollectionView[] = []; - const decCiphers: CipherView[] = []; - const promises = []; - - promises.push( - this.apiService.getOrganizationExport(organizationId).then((exportData) => { - const exportPromises: any = []; - if (exportData != null) { - if (exportData.collections != null && exportData.collections.length > 0) { - exportData.collections.forEach((c) => { - const collection = new Collection(new CollectionData(c as CollectionDetailsResponse)); - exportPromises.push( - collection.decrypt().then((decCol) => { - decCollections.push(decCol); - }), - ); - }); - } - if (exportData.ciphers != null && exportData.ciphers.length > 0) { - exportData.ciphers - .filter((c) => c.deletedDate === null) - .forEach(async (c) => { - const cipher = new Cipher(new CipherData(c)); - exportPromises.push( - this.cipherService - .getKeyForCipherKeyDecryption(cipher) - .then((key) => cipher.decrypt(key)) - .then((decCipher) => { - decCiphers.push(decCipher); - }), - ); - }); - } - } - return Promise.all(exportPromises); - }), - ); - - await Promise.all(promises); - - if (format === "csv") { - const collectionsMap = new Map(); - decCollections.forEach((c) => { - collectionsMap.set(c.id, c); - }); - - const exportCiphers: BitwardenCsvOrgExportType[] = []; - decCiphers.forEach((c) => { - // only export logins and secure notes - if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { - return; - } - - const cipher = {} as BitwardenCsvOrgExportType; - cipher.collections = []; - if (c.collectionIds != null) { - cipher.collections = c.collectionIds - .filter((id) => collectionsMap.has(id)) - .map((id) => collectionsMap.get(id).name); - } - this.buildCommonCipher(cipher, c); - exportCiphers.push(cipher); - }); - - return papa.unparse(exportCiphers); - } else { - const jsonDoc: BitwardenUnEncryptedOrgJsonExport = { - encrypted: false, - collections: [], - items: [], - }; - - decCollections.forEach((c) => { - const collection = new CollectionWithIdExport(); - collection.build(c); - jsonDoc.collections.push(collection); - }); - - decCiphers.forEach((c) => { - const cipher = new CipherWithIdExport(); - cipher.build(c); - jsonDoc.items.push(cipher); - }); - return JSON.stringify(jsonDoc, null, " "); - } - } - - private async getOrganizationEncryptedExport(organizationId: string): Promise { - const collections: Collection[] = []; - const ciphers: Cipher[] = []; - const promises = []; - - promises.push( - this.apiService.getCollections(organizationId).then((c) => { - if (c != null && c.data != null && c.data.length > 0) { - c.data.forEach((r) => { - const collection = new Collection(new CollectionData(r as CollectionDetailsResponse)); - collections.push(collection); - }); - } - }), - ); - - promises.push( - this.apiService.getCiphersOrganization(organizationId).then((c) => { - if (c != null && c.data != null && c.data.length > 0) { - c.data - .filter((item) => item.deletedDate === null) - .forEach((item) => { - const cipher = new Cipher(new CipherData(item)); - ciphers.push(cipher); - }); - } - }), - ); - - await Promise.all(promises); - - const orgKey = await this.cryptoService.getOrgKey(organizationId); - const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), orgKey); - - const jsonDoc: BitwardenEncryptedOrgJsonExport = { - encrypted: true, - encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, - collections: [], - items: [], - }; - - collections.forEach((c) => { - const collection = new CollectionWithIdExport(); - collection.build(c); - jsonDoc.collections.push(collection); - }); - - ciphers.forEach((c) => { - const cipher = new CipherWithIdExport(); - cipher.build(c); - jsonDoc.items.push(cipher); - }); - return JSON.stringify(jsonDoc, null, " "); - } - - private buildCommonCipher(cipher: BitwardenCsvExportType, c: CipherView): BitwardenCsvExportType { - cipher.type = null; - cipher.name = c.name; - cipher.notes = c.notes; - cipher.fields = null; - cipher.reprompt = c.reprompt; - // Login props - cipher.login_uri = null; - cipher.login_username = null; - cipher.login_password = null; - cipher.login_totp = null; - - if (c.fields) { - c.fields.forEach((f) => { - if (!cipher.fields) { - cipher.fields = ""; - } else { - cipher.fields += "\n"; - } - - cipher.fields += (f.name || "") + ": " + f.value; - }); - } - - switch (c.type) { - case CipherType.Login: - cipher.type = "login"; - cipher.login_username = c.login.username; - cipher.login_password = c.login.password; - cipher.login_totp = c.login.totp; - - if (c.login.uris) { - cipher.login_uri = []; - c.login.uris.forEach((u) => { - cipher.login_uri.push(u.uri); - }); - } - break; - case CipherType.SecureNote: - cipher.type = "note"; - break; - default: - return; - } - - return cipher; - } }