From f6a7315d1333a6fdf21120b8294ce1a0265a6634 Mon Sep 17 00:00:00 2001 From: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com> Date: Thu, 16 Jun 2022 16:20:10 -0400 Subject: [PATCH] Rough draft of Export/Import changes w/ password encryption --- .../file-password-prompt.component.html | 64 ++++++++ .../file-password-prompt.component.ts | 8 + .../app/modules/loose-components.module.ts | 3 + .../organizations/tools/export.component.ts | 13 +- .../organizations/tools/import.component.ts | 17 +- apps/web/src/app/services/services.module.ts | 20 +++ apps/web/src/app/tools/export.component.html | 146 ++++++++++++++++-- apps/web/src/app/tools/export.component.ts | 62 +++++++- apps/web/src/app/tools/import.component.ts | 34 +++- apps/web/src/locales/en/messages.json | 27 ++++ apps/web/src/locales/en_GB/messages.json | 27 ++++ apps/web/src/locales/en_IN/messages.json | 27 ++++ .../services/filePasswordPrompt.service.ts | 10 ++ .../src/components/export.component.ts | 107 +++++++++++-- .../file-password-prompt.component.ts | 58 +++++++ .../services/filePasswordPrompt.service.ts | 51 ++++++ .../common/src/abstractions/export.service.ts | 2 +- .../filePasswordPrompt.service.ts | 5 + libs/common/src/services/export.service.ts | 10 +- 19 files changed, 647 insertions(+), 44 deletions(-) create mode 100644 apps/web/src/app/components/file-password-prompt.component.html create mode 100644 apps/web/src/app/components/file-password-prompt.component.ts create mode 100644 apps/web/src/services/filePasswordPrompt.service.ts create mode 100644 libs/angular/src/components/file-password-prompt.component.ts create mode 100644 libs/angular/src/services/filePasswordPrompt.service.ts create mode 100644 libs/common/src/abstractions/filePasswordPrompt.service.ts diff --git a/apps/web/src/app/components/file-password-prompt.component.html b/apps/web/src/app/components/file-password-prompt.component.html new file mode 100644 index 00000000000..4c98ef66833 --- /dev/null +++ b/apps/web/src/app/components/file-password-prompt.component.html @@ -0,0 +1,64 @@ + diff --git a/apps/web/src/app/components/file-password-prompt.component.ts b/apps/web/src/app/components/file-password-prompt.component.ts new file mode 100644 index 00000000000..b055107c80c --- /dev/null +++ b/apps/web/src/app/components/file-password-prompt.component.ts @@ -0,0 +1,8 @@ +import { Component } from "@angular/core"; + +import { FilePasswordPromptComponent as BaseFilePasswordPrompt } from "@bitwarden/angular/components/file-password-prompt.component"; + +@Component({ + templateUrl: "file-password-prompt.component.html", +}) +export class FilePasswordPromptComponent extends BaseFilePasswordPrompt {} diff --git a/apps/web/src/app/modules/loose-components.module.ts b/apps/web/src/app/modules/loose-components.module.ts index 3fa418f6c5a..382c7fdd193 100644 --- a/apps/web/src/app/modules/loose-components.module.ts +++ b/apps/web/src/app/modules/loose-components.module.ts @@ -19,6 +19,7 @@ import { UpdatePasswordComponent } from "../accounts/update-password.component"; import { UpdateTempPasswordComponent } from "../accounts/update-temp-password.component"; import { VerifyEmailTokenComponent } from "../accounts/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "../accounts/verify-recover-delete.component"; +import { FilePasswordPromptComponent } from "../components/file-password-prompt.component"; import { NestedCheckboxComponent } from "../components/nested-checkbox.component"; import { OrganizationSwitcherComponent } from "../components/organization-switcher.component"; import { PasswordRepromptComponent } from "../components/password-reprompt.component"; @@ -269,6 +270,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga PasswordGeneratorHistoryComponent, PasswordGeneratorPolicyComponent, PasswordRepromptComponent, + FilePasswordPromptComponent, PasswordStrengthComponent, PaymentComponent, PaymentMethodComponent, @@ -427,6 +429,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga PasswordGeneratorHistoryComponent, PasswordGeneratorPolicyComponent, PasswordRepromptComponent, + FilePasswordPromptComponent, PasswordStrengthComponent, PaymentComponent, PaymentMethodComponent, diff --git a/apps/web/src/app/organizations/tools/export.component.ts b/apps/web/src/app/organizations/tools/export.component.ts index 8c482f58b7a..e4b89654e9f 100644 --- a/apps/web/src/app/organizations/tools/export.component.ts +++ b/apps/web/src/app/organizations/tools/export.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; @@ -9,6 +11,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification.service"; import { ExportComponent as BaseExportComponent } from "../../tools/export.component"; @@ -28,7 +31,10 @@ export class ExportComponent extends BaseExportComponent { policyService: PolicyService, logService: LogService, userVerificationService: UserVerificationService, - formBuilder: FormBuilder + formBuilder: FormBuilder, + modalService: ModalService, + apiService: ApiService, + stateService: StateService ) { super( cryptoService, @@ -39,7 +45,10 @@ export class ExportComponent extends BaseExportComponent { policyService, logService, userVerificationService, - formBuilder + formBuilder, + modalService, + apiService, + stateService ); } diff --git a/apps/web/src/app/organizations/tools/import.component.ts b/apps/web/src/app/organizations/tools/import.component.ts index 08aea5b1aca..44032545312 100644 --- a/apps/web/src/app/organizations/tools/import.component.ts +++ b/apps/web/src/app/organizations/tools/import.component.ts @@ -1,6 +1,8 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { FilePasswordPromptService } from "@bitwarden/common/abstractions/filePasswordPrompt.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { ImportService } from "@bitwarden/common/abstractions/import.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -25,9 +27,20 @@ export class ImportComponent extends BaseImportComponent { platformUtilsService: PlatformUtilsService, policyService: PolicyService, private organizationService: OrganizationService, - logService: LogService + logService: LogService, + modalService: ModalService, + filePasswordPromptService: FilePasswordPromptService ) { - super(i18nService, importService, router, platformUtilsService, policyService, logService); + super( + i18nService, + importService, + router, + platformUtilsService, + policyService, + logService, + modalService, + filePasswordPromptService + ); } async ngOnInit() { diff --git a/apps/web/src/app/services/services.module.ts b/apps/web/src/app/services/services.module.ts index cf4b290e62b..f616d683fdd 100644 --- a/apps/web/src/app/services/services.module.ts +++ b/apps/web/src/app/services/services.module.ts @@ -14,6 +14,9 @@ import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstracti import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/common/abstractions/collection.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service"; +import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service"; +import { ExportService as ExportServiceAbstraction } from "@bitwarden/common/abstractions/export.service"; +import { FilePasswordPromptService as FilePasswordPromptServiceAbstraction } from "@bitwarden/common/abstractions/filePasswordPrompt.service"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/abstractions/folder.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service"; import { ImportService as ImportServiceAbstraction } from "@bitwarden/common/abstractions/import.service"; @@ -25,12 +28,14 @@ import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/a import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service"; import { StorageService as StorageServiceAbstraction } from "@bitwarden/common/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/factories/stateFactory"; +import { ExportService } from "@bitwarden/common/services/export.service"; import { ImportService } from "@bitwarden/common/services/import.service"; import { StateService as StateServiceAbstraction } from "../../abstractions/state.service"; import { Account } from "../../models/account"; import { GlobalState } from "../../models/globalState"; import { BroadcasterMessagingService } from "../../services/broadcasterMessaging.service"; +import { FilePasswordPromptService } from "../../services/filePasswordPrompt.service"; import { HtmlStorageService } from "../../services/htmlStorage.service"; import { I18nService } from "../../services/i18n.service"; import { MemoryStorageService } from "../../services/memoryStorage.service"; @@ -103,6 +108,17 @@ import { RouterService } from "./router.service"; CryptoServiceAbstraction, ], }, + { + provide: ExportServiceAbstraction, + useClass: ExportService, + deps: [ + FolderServiceAbstraction, + CipherServiceAbstraction, + ApiServiceAbstraction, + CryptoServiceAbstraction, + CryptoFunctionServiceAbstraction, + ], + }, { provide: StateMigrationServiceAbstraction, useClass: StateMigrationService, @@ -128,6 +144,10 @@ import { RouterService } from "./router.service"; provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService, }, + { + provide: FilePasswordPromptServiceAbstraction, + useClass: FilePasswordPromptService, + }, HomeGuard, ], }) diff --git a/apps/web/src/app/tools/export.component.html b/apps/web/src/app/tools/export.component.html index da667f3e44a..a22ec2dd797 100644 --- a/apps/web/src/app/tools/export.component.html +++ b/apps/web/src/app/tools/export.component.html @@ -1,6 +1,6 @@
-
-
- - +
+ + + +
+ + + +
+ + +
+ {{ "accountBackupOptionDescription" | i18n }} +
+ + + + + +
+ {{ "passwordProtectedOptionDescription" | i18n }} +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+ {{ "exportPasswordDescription" | i18n }} +
+
+ +
+ + +
+
+ +
+
+
+
+
+
- diff --git a/apps/web/src/app/tools/export.component.ts b/apps/web/src/app/tools/export.component.ts index d5e6aadccb3..87ad669875b 100644 --- a/apps/web/src/app/tools/export.component.ts +++ b/apps/web/src/app/tools/export.component.ts @@ -1,7 +1,9 @@ -import { Component } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; +import { Component, EventEmitter, Output, ViewChild, ViewContainerRef } from "@angular/core"; +import { FormBuilder, FormControl } from "@angular/forms"; import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/components/export.component"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; @@ -9,14 +11,26 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification.service"; +import { ApiKeyComponent } from "../settings/api-key.component"; + @Component({ selector: "app-export", templateUrl: "export.component.html", }) export class ExportComponent extends BaseExportComponent { organizationId: string; + formatControl: string; + encryptionType: string; + showPassword: boolean; + showConfirmPassword: boolean; + secretValue: string; + secret: FormControl; + + @ViewChild("viewUserApiKeyModalRef", { read: ViewContainerRef, static: true }) + viewUserApiKeyModalRef: ViewContainerRef; constructor( cryptoService: CryptoService, @@ -27,7 +41,10 @@ export class ExportComponent extends BaseExportComponent { policyService: PolicyService, logService: LogService, userVerificationService: UserVerificationService, - formBuilder: FormBuilder + formBuilder: FormBuilder, + modalService: ModalService, + apiService: ApiService, + stateService: StateService ) { super( cryptoService, @@ -39,10 +56,47 @@ export class ExportComponent extends BaseExportComponent { window, logService, userVerificationService, - formBuilder + formBuilder, + modalService, + apiService, + stateService ); } + async promptUserForSecret() { + const entityId = await this.stateService.getUserId(); + try { + // //TODO get help from Thomas on this/ other options I have to get a Secret : + // await this.modalService.openViewRef(ApiKeyComponent, this.viewUserApiKeyModalRef, (comp) => { + // comp.keyType = "user"; + // comp.entityId = entityId; + // comp.postKey = this.apiService.postUserApiKey.bind(this.apiService); + // comp.scope = "api"; + // comp.grantType = "client_credentials"; + // comp.apiKeyTitle = "apiKey"; + // comp.apiKeyWarning = "userApiKeyWarning"; + // comp.apiKeyDescription = "userApiKeyDesc"; + // }); + + // this.platformUtilsService.showToast("error", "This line doesn't get called because of an error ", this.i18nService.t("exportFail")); + + //If verification is successful: + this.submitWithSecretAlreadyVerified(); + } catch { + this.platformUtilsService.showToast("error", "FAIL", this.i18nService.t("exportFail")); + } + } + + togglePassword() { + this.showPassword = !this.showPassword; + document.getElementById("newPassword").focus(); + } + + toggleConfirmPassword() { + this.showConfirmPassword = !this.showConfirmPassword; + document.getElementById("newConfirmPassword").focus(); + } + protected saved() { super.saved(); this.platformUtilsService.showToast("success", null, this.i18nService.t("exportSuccess")); diff --git a/apps/web/src/app/tools/import.component.ts b/apps/web/src/app/tools/import.component.ts index 540872daf99..00977522877 100644 --- a/apps/web/src/app/tools/import.component.ts +++ b/apps/web/src/app/tools/import.component.ts @@ -3,6 +3,8 @@ import { Router } from "@angular/router"; import * as JSZip from "jszip"; import Swal, { SweetAlertIcon } from "sweetalert2"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { FilePasswordPromptService } from "@bitwarden/common/abstractions/filePasswordPrompt.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { ImportService } from "@bitwarden/common/abstractions/import.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -10,6 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti import { PolicyService } from "@bitwarden/common/abstractions/policy.service"; import { ImportOption, ImportType } from "@bitwarden/common/enums/importOptions"; import { PolicyType } from "@bitwarden/common/enums/policyType"; +import { ImportError } from "@bitwarden/common/importers/importError"; @Component({ selector: "app-import", @@ -20,7 +23,7 @@ export class ImportComponent implements OnInit { importOptions: ImportOption[]; format: ImportType = null; fileContents: string; - formPromise: Promise; + formPromise: Promise; loading = false; importBlockedByPolicy = false; @@ -33,7 +36,9 @@ export class ImportComponent implements OnInit { protected router: Router, protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, - private logService: LogService + private logService: LogService, + private modalService: ModalService, + private filePasswordPromptService: FilePasswordPromptService ) {} async ngOnInit() { @@ -55,7 +60,6 @@ export class ImportComponent implements OnInit { } this.loading = true; - const importer = this.importService.getImporter(this.format, this.organizationId); if (importer === null) { this.platformUtilsService.showToast( @@ -108,10 +112,24 @@ export class ImportComponent implements OnInit { this.formPromise = this.importService.import(importer, fileContents, this.organizationId); const error = await this.formPromise; if (error != null) { - this.error(error); - this.loading = false; - return; + //Check if the error is that a password is required + if (error.passwordRequired) { + if (await this.promptPassword(fileContents)) { + //successful + } else { + //failed + this.error(error); //TODO different error + this.loading = false; + return; + } + } else { + this.error(error); + this.loading = false; + return; + } } + + //No errors, display success message this.platformUtilsService.showToast("success", null, this.i18nService.t("importSuccess")); this.router.navigate(this.successNavigate); } catch (e) { @@ -121,6 +139,10 @@ export class ImportComponent implements OnInit { this.loading = false; } + private async promptPassword(fcontents: string) { + return await this.filePasswordPromptService.showPasswordPrompt(fcontents, this.organizationId); + } + getFormatInstructionTitle() { if (this.format == null) { return null; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index a930e76fbf2..60aa6e4e275 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -878,6 +878,30 @@ "fileFormat": { "message": "File Format" }, + "confirmFormat": { + "message": "Confirm Format" + }, + "filePassword": { + "message": "File Password" + }, + "confirmFilePassword": { + "message": "Confirm File Password" + }, + "passwordProtectedOptionDescription": { + "message": "Leverages your bitwarden account encryption not master password, to protect the export. This export can only be imported into the current account. Use this to create a backup that cannot be used elsewhere." + }, + "accountBackupOptionDescription": { + "message": "Create a user-generated password to protect the export. Use this to create an export that can be used in other accounts." + }, + "fileTypeHeading": { + "message": "File Type" + }, + "confirmVaultImport": { + "message": "Confirm Vault Import" + }, + "confirmVaultImportDesc": { + "message": "This file is password protected. Please enter the file password to import data." + }, "exportSuccess": { "message": "Your vault data has been exported." }, @@ -4701,6 +4725,9 @@ "confirmIdentity": { "message": "Confirm your identity to continue." }, + "exportPasswordDescription" : { + "message": "This password will be used to export and import this file" + }, "verificationCodeRequired": { "message": "Verification code is required." }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 109935c44c6..6cf8a48ed04 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -875,6 +875,30 @@ "exportVault": { "message": "Export vault" }, + "confirmFormat": { + "message": "Confirm Format" + }, + "filePassword": { + "message": "File Password" + }, + "confirmFilePassword": { + "message": "Confirm File Password" + }, + "passwordProtectedOptionDescription": { + "message": "Leverages your bitwarden account encryption not master password, to protect the export. This export can only be imported into the current account. Use this to create a backup that cannot be used elsewhere." + }, + "accountBackupOptionDescription": { + "message": "Create a user-generated password to protect the export. Use this to create an export that can be used in other accounts." + }, + "fileTypeHeading": { + "message": "File Type" + }, + "confirmVaultImport": { + "message": "Confirm Vault Import" + }, + "confirmVaultImportDesc": { + "message": "This file is password protected. Please enter the file password to import data." + }, "fileFormat": { "message": "File format" }, @@ -4701,6 +4725,9 @@ "confirmIdentity": { "message": "Confirm your identity to continue." }, + "exportPasswordDescription" : { + "message": "This password will be used to export and import this file" + }, "verificationCodeRequired": { "message": "Verification code is required." }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index c9a76572299..555a229131c 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -878,6 +878,30 @@ "fileFormat": { "message": "File format" }, + "confirmFormat": { + "message": "Confirm Format" + }, + "filePassword": { + "message": "File Password" + }, + "confirmFilePassword": { + "message": "Confirm File Password" + }, + "passwordProtectedOptionDescription": { + "message": "Leverages your bitwarden account encryption not master password, to protect the export. This export can only be imported into the current account. Use this to create a backup that cannot be used elsewhere." + }, + "accountBackupOptionDescription": { + "message": "Create a user-generated password to protect the export. Use this to create an export that can be used in other accounts." + }, + "fileTypeHeading": { + "message": "File Type" + }, + "confirmVaultImport": { + "message": "Confirm Vault Import" + }, + "confirmVaultImportDesc": { + "message": "This file is password protected. Please enter the file password to import data." + }, "exportSuccess": { "message": "Your vault data has been exported." }, @@ -4701,6 +4725,9 @@ "confirmIdentity": { "message": "Confirm your identity to continue." }, + "exportPasswordDescription" : { + "message": "This password will be used to export and import this file" + }, "verificationCodeRequired": { "message": "Verification code is required." }, diff --git a/apps/web/src/services/filePasswordPrompt.service.ts b/apps/web/src/services/filePasswordPrompt.service.ts new file mode 100644 index 00000000000..40cd9ad6fed --- /dev/null +++ b/apps/web/src/services/filePasswordPrompt.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@angular/core"; + +import { FilePasswordPromptService as BaseFilePasswordPromptService } from "@bitwarden/angular/services/filePasswordPrompt.service"; + +import { FilePasswordPromptComponent } from "../app/components/file-password-prompt.component"; + +@Injectable() +export class FilePasswordPromptService extends BaseFilePasswordPromptService { + component = FilePasswordPromptComponent; +} diff --git a/libs/angular/src/components/export.component.ts b/libs/angular/src/components/export.component.ts index 353a58dc726..f139aabdb1a 100644 --- a/libs/angular/src/components/export.component.ts +++ b/libs/angular/src/components/export.component.ts @@ -1,6 +1,14 @@ -import { Directive, EventEmitter, OnInit, Output } from "@angular/core"; +import { + Directive, + EventEmitter, + OnInit, + Output, + ViewChild, + ViewContainerRef, +} from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; @@ -8,20 +16,32 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification.service"; import { EventType } from "@bitwarden/common/enums/eventType"; import { PolicyType } from "@bitwarden/common/enums/policyType"; +import { ModalService } from "../services/modal.service"; + @Directive() export class ExportComponent implements OnInit { @Output() onSaved = new EventEmitter(); formPromise: Promise; disabledByPolicy = false; + private alreadyVerified = false; + + @ViewChild("viewUserApiKeyTemplate", { read: ViewContainerRef, static: true }) + viewUserApiKeyModalRef: ViewContainerRef; + + encryptionPassword: string; exportForm = this.formBuilder.group({ format: ["json"], secret: [""], + password: [""], + confirmPassword: [""], + fileEncryptionType: [""], }); formatOptions = [ @@ -40,7 +60,10 @@ export class ExportComponent implements OnInit { protected win: Window, private logService: LogService, private userVerificationService: UserVerificationService, - private formBuilder: FormBuilder + private formBuilder: FormBuilder, + protected modalService: ModalService, + protected apiService: ApiService, + protected stateService: StateService ) {} async ngOnInit() { @@ -60,6 +83,15 @@ export class ExportComponent implements OnInit { return this.format === "encrypted_json"; } + async submitWithSecretAlreadyVerified() { + if (!this.validForm) { + return; + } + + this.alreadyVerified = true; + this.submit(); + } + async submit() { if (this.disabledByPolicy) { this.platformUtilsService.showToast( @@ -70,19 +102,24 @@ export class ExportComponent implements OnInit { return; } - const acceptedWarning = await this.warningDialog(); - if (!acceptedWarning) { - return; - } + if (!this.alreadyVerified) { + const acceptedWarning = await this.warningDialog(); + if (!acceptedWarning) { + return; + } + const secret = this.exportForm.get("secret").value; - const secret = this.exportForm.get("secret").value; - try { - await this.userVerificationService.verifyUser(secret); - } catch (e) { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message); - return; + try { + await this.userVerificationService.verifyUser(secret); + } catch (e) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + e.message + ); + return; + } } - try { this.formPromise = this.getExportData(); const data = await this.formPromise; @@ -124,7 +161,7 @@ export class ExportComponent implements OnInit { } protected getExportData() { - return this.exportService.getExport(this.format); + return this.exportService.getExport(this.format, null, this.encryptionPassword); } protected getFileName(prefix?: string) { @@ -144,10 +181,52 @@ export class ExportComponent implements OnInit { await this.eventService.collect(EventType.User_ClientExportedVault); } + get validForm() { + //TODO check if fileEncryption type is null? + if (this.fileEncryptionType == 2 && this.format == "encrypted_json") { + //password encryption type + const password = this.password; + const confirmPassword = this.confirmPassword; + + if (password.length > 0 || confirmPassword.length > 0) { + if (password != confirmPassword) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + "File Password and Confirm File Password do not match." + ); + return false; + } + + this.encryptionPassword = password; + return true; + } + } else { + this.clearPasswordField(); + return true; + } + } + + protected clearPasswordField() { + this.encryptionPassword = ""; + } + get format() { return this.exportForm.get("format").value; } + get password() { + return this.exportForm.get("password").value; + } + + get confirmPassword() { + return this.exportForm.get("confirmPassword").value; + } + + get fileEncryptionType() { + return this.exportForm.get("fileEncryptionType").value; + } + private downloadFile(csv: string): void { const fileName = this.getFileName(); this.platformUtilsService.saveFile(this.win, csv, { type: "text/plain" }, fileName); diff --git a/libs/angular/src/components/file-password-prompt.component.ts b/libs/angular/src/components/file-password-prompt.component.ts new file mode 100644 index 00000000000..f148e0d45d9 --- /dev/null +++ b/libs/angular/src/components/file-password-prompt.component.ts @@ -0,0 +1,58 @@ +import { Directive } from "@angular/core"; + +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { ImportService } from "@bitwarden/common/abstractions/import.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; + +import { ModalConfig } from "../services/modal.service"; + +import { ModalRef } from "./modal/modal.ref"; + +/** + * Used to verify the user's File password to import their encyrpted export file" feature only. + */ +@Directive() +export class FilePasswordPromptComponent { + showPassword = false; + filePassword = ""; + organizationId = ""; + fileContents = ""; + + constructor( + private modalRef: ModalRef, + private cryptoService: CryptoService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private importService: ImportService, + config: ModalConfig + ) { + this.fileContents = config.data.fileContents; + this.organizationId = config.data.organizationId; + } + + togglePassword() { + this.showPassword = !this.showPassword; + } + + async submit() { + const importerPassword = this.importService.getImporter( + "bitwardenpasswordprotected", + this.organizationId, + this.filePassword + ); + + const formPromise = this.importService.import( + importerPassword, + this.fileContents, + this.organizationId + ); + const passwordError = await formPromise; + + if (passwordError != null) { + this.modalRef.close(false); + } else { + this.modalRef.close(true); + } + } +} diff --git a/libs/angular/src/services/filePasswordPrompt.service.ts b/libs/angular/src/services/filePasswordPrompt.service.ts new file mode 100644 index 00000000000..911bd5bbe4c --- /dev/null +++ b/libs/angular/src/services/filePasswordPrompt.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from "@angular/core"; + +import { FilePasswordPromptService as FilePasswordPromptServiceAbstraction } from "@bitwarden/common/abstractions/filePasswordPrompt.service"; +import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service"; + + +import { FilePasswordPromptComponent } from "../components/file-password-prompt.component"; + +import { ModalService } from "./modal.service"; + +/** + * Used to verify the user's File Password for the "Import passwords using File Password" feature only. + */ +@Injectable() +export class FilePasswordPromptService implements FilePasswordPromptServiceAbstraction { + protected component = FilePasswordPromptComponent; + + constructor( + private modalService: ModalService, + private keyConnectorService: KeyConnectorService + ) {} + + protectedFields() { + return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"]; + } + + async showPasswordPrompt(fcontents: string, organizationId: string) { + if (!(await this.enabled())) { + return true; + } + + const ref = this.modalService.open(this.component, { + allowMultipleModals: true, + data: { + fileContents: fcontents, + organizationId: organizationId, + }, + }); + + if (ref == null) { + return false; + } + + const result = await ref.onClosedPromise(); + return result === true; + } + + async enabled() { + return !(await this.keyConnectorService.getUsesKeyConnector()); + } +} diff --git a/libs/common/src/abstractions/export.service.ts b/libs/common/src/abstractions/export.service.ts index b0266530bd6..9dc5fa8044d 100644 --- a/libs/common/src/abstractions/export.service.ts +++ b/libs/common/src/abstractions/export.service.ts @@ -3,7 +3,7 @@ import { EventView } from "../models/view/eventView"; export type ExportFormat = "csv" | "json" | "encrypted_json"; export abstract class ExportService { - getExport: (format?: ExportFormat, organizationId?: string) => Promise; + getExport: (format?: ExportFormat, organizationId?: string, password?: string) => Promise; getPasswordProtectedExport: (password: string, organizationId?: string) => Promise; getOrganizationExport: (organizationId: string, format?: ExportFormat) => Promise; getEventExport: (events: EventView[]) => Promise; diff --git a/libs/common/src/abstractions/filePasswordPrompt.service.ts b/libs/common/src/abstractions/filePasswordPrompt.service.ts new file mode 100644 index 00000000000..f7b0900b647 --- /dev/null +++ b/libs/common/src/abstractions/filePasswordPrompt.service.ts @@ -0,0 +1,5 @@ +export abstract class FilePasswordPromptService { + protectedFields: () => string[]; + showPasswordPrompt: (fcontents: string, organizationId: string) => Promise; + enabled: () => Promise; +} diff --git a/libs/common/src/services/export.service.ts b/libs/common/src/services/export.service.ts index c6f08be6d26..299d9697b6a 100644 --- a/libs/common/src/services/export.service.ts +++ b/libs/common/src/services/export.service.ts @@ -36,13 +36,19 @@ export class ExportService implements ExportServiceAbstraction { private cryptoFunctionService: CryptoFunctionService ) {} - async getExport(format: ExportFormat = "csv", organizationId?: string): Promise { + async getExport( + format: ExportFormat = "csv", + organizationId?: string, + password?: string + ): Promise { if (organizationId) { return await this.getOrganizationExport(organizationId, format); } if (format === "encrypted_json") { - return this.getEncryptedExport(); + return password == undefined || password == "" + ? this.getEncryptedExport() + : this.getPasswordProtectedExport(password); } else { return this.getDecryptedExport(format); }