-
-
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);
}