[SM-89] Updates to encrypted export (#2963)

* Rough draft of Export/Import changes w/ password encryption

* fix for encrypted export changes

* Create launch.json

* Updates to export logic modal user secret prompt

* Updates to error handling

* renaming the component for checking the user secret to a name that is more clear about what it accomplishes

* Fixing lint errors

* Adding a comment

* Suggested changes from CR

* Suggested changes from CR

* Making suggested changes

* removing unnecessary properties

* changes suggested

* Fix

* Updating error messages

* Removing unecessary launch.json file commit

* running lint, removing commented code

* removing launch.json

* Updates to remove the userVerificationPromptService

* updates

* Removing unused import, running npm prettier/lint

* Changes to use Form Fields

* Updates

* updates requested by Matt

* Update apps/web/src/app/tools/import-export/export.component.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Suggested Changes from PR

* Fix after merge from Master

* changes to styling

* Removing unused code and cleanup

* Update libs/angular/src/components/user-verification-prompt.component.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Update apps/web/src/locales/en/messages.json

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Changes suggested by Thomas R

* Merging master into branch

* Revert "Merging master into branch"

This reverts commit eb2cdffe49.

* Requested changes and improvements

* merging master into feature branch

* Revert "merging master into feature branch"

This reverts commit e287715251.

* Suggested Changes

* changes

* requested changes

* Requested changes

* removing comments, fixing code

* reducing copied code

* fixing bug

* fixing bug

* changes

* WIP

* Thomas's requested changes

* adding back missing spaces

* change needed after the merge from master into feature branch

* prettier + lint

* Updating the EncryptedExportType Import

* Fixing build errors

Co-authored-by: Thomas Rittson <eliykat@users.noreply.github.com>

* Move FilePasswordPrompt to ImportExportModule

Also remove base class
Also remove duplicate service providers

* Run prettier

* Suggested Changes from Thomas

* only require filePassword and confirmFilePassword if it's type is FileEncrypted

* Update to only enable the field when submitting a file password encrypted file

* Requested changes, moving logic to web

* undoing change to bit button

* Refactor to process file-encrypted imports in main import.component

*  Refactor confirm file password check

* Remove UserVerificationPromptService

* Address CodeScene feedback

* Updates to disable the required file password field when needed

* Subscribe to reactive form changes to adjust validators

* style changes requested by suhkleen

* Delete duplicate classes

Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <eliykat@users.noreply.github.com>
This commit is contained in:
cd-bitwarden 2022-08-29 10:11:44 -04:00 committed by GitHub
parent 231e1bf666
commit a108476c3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 607 additions and 43 deletions

View File

@ -15,7 +15,10 @@
"message": "No Folder"
},
"importEncKeyError": {
"message": "Invalid file password."
"message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data."
},
"invalidFilePassword": {
"message": "Invalid file password, please use the password you entered when you created the export file."
},
"importPasswordRequired": {
"message": "File is password protected, please provide a decryption password."

View File

@ -0,0 +1,26 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()">
<h2 class="tw-mt-6 tw-mb-6 tw-pl-3.5 tw-pr-3.5 tw-font-semibold" id="modalTitle | i18n ">
{{ modalTitle | i18n | uppercase }}
</h2>
<div class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-p-3.5">
{{ confirmDescription | i18n }}
</div>
<div class="tw-p-3.5">
<app-user-verification ngDefaultControl [formControl]="secret" name="secret">
</app-user-verification>
</div>
<div
class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-p-3.5"
>
<button bitButton buttonType="primary" type="submit" appBlurClick>
<span>{{ confirmButtonText | i18n }}</span>
</button>
<button bitButton buttonType="secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,8 @@
import { Component } from "@angular/core";
import { UserVerificationPromptComponent as BaseUserVerificationPrompt } from "@bitwarden/angular/components/user-verification-prompt.component";
@Component({
templateUrl: "user-verification-prompt.component.html",
})
export class UserVerificationPromptComponent extends BaseUserVerificationPrompt {}

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.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";
@ -32,7 +33,8 @@ export class OrganizationExportComponent extends ExportComponent {
logService: LogService,
userVerificationService: UserVerificationService,
formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService
fileDownloadService: FileDownloadService,
modalService: ModalService
) {
super(
cryptoService,
@ -44,7 +46,8 @@ export class OrganizationExportComponent extends ExportComponent {
logService,
userVerificationService,
formBuilder,
fileDownloadService
fileDownloadService,
modalService
);
}

View File

@ -1,6 +1,7 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.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";
@ -26,9 +27,18 @@ export class OrganizationImportComponent extends ImportComponent {
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
private organizationService: OrganizationService,
logService: LogService
logService: LogService,
modalService: ModalService
) {
super(i18nService, importService, router, platformUtilsService, policyService, logService);
super(
i18nService,
importService,
router,
platformUtilsService,
policyService,
logService,
modalService
);
}
async ngOnInit() {

View File

@ -24,6 +24,7 @@ import { NestedCheckboxComponent } from "../components/nested-checkbox.component
import { OrganizationSwitcherComponent } from "../components/organization-switcher.component";
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
import { PremiumBadgeComponent } from "../components/premium-badge.component";
import { UserVerificationPromptComponent } from "../components/user-verification-prompt.component";
import { FooterComponent } from "../layouts/footer.component";
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
import { NavbarComponent } from "../layouts/navbar.component";
@ -253,6 +254,7 @@ import { SharedModule } from ".";
PasswordGeneratorHistoryComponent,
PasswordGeneratorPolicyComponent,
PasswordRepromptComponent,
UserVerificationPromptComponent,
PaymentComponent,
PaymentMethodComponent,
PersonalOwnershipPolicyComponent,

View File

@ -1,9 +1,9 @@
<form
#form
(ngSubmit)="submit()"
ngNativeValidate
[appApiAction]="formPromise"
[formGroup]="exportForm"
*ngIf="exportForm"
>
<div class="page-header">
<h1>{{ "exportVault" | i18n }}</h1>
@ -18,25 +18,150 @@
></app-export-scope-callout>
<div class="row">
<div class="form-group col-6">
<label for="format">{{ "fileFormat" | i18n }}</label>
<select class="form-control" id="format" name="Format" formControlName="format">
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
</select>
<div class="col-6">
<bit-form-field>
<bit-label class="tw-text-lg" for="format">{{ "fileFormat" | i18n }}</bit-label>
<select bitInput name="format" formControlName="format">
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
</select>
</bit-form-field>
</div>
</div>
<div class="row">
<div class="form-group col-6">
<app-user-verification ngDefaultControl formControlName="secret" name="secret">
</app-user-verification>
<ng-container *ngIf="format === 'encrypted_json'">
<div role="radiogroup" aria-labelledby="fileTypeHeading">
<label id="fileTypeHeading" class="tw-semi-bold tw-text-lg">
{{ "fileTypeHeading" | i18n }}
</label>
<div appBoxRow name="FileTypeOptions" class="tw-flex tw-items-center">
<div>
<input
type="radio"
class="radio"
name="fileEncryptionType"
id="AccountEncrypted"
[value]="encryptedExportType.AccountEncrypted"
formControlName="fileEncryptionType"
[checked]="fileEncryptionType === encryptedExportType.AccountEncrypted"
/>
</div>
<div>
<label class="tw-semi-bold tw-text-md tw-ml-1 tw-mt-1 tw-mb-1" for="AccountEncrypted">
{{ "accountBackup" | i18n }}
</label>
</div>
</div>
<div class="tw-regular ml-3 pb-2 tw-text-sm">
{{ "accountBackupOptionDescription" | i18n }}
</div>
<div class="tw-flex tw-items-center">
<div>
<input
type="radio"
class="radio"
name="fileEncryptionType"
id="FileEncrypted"
[value]="encryptedExportType.FileEncrypted"
formControlName="fileEncryptionType"
[checked]="fileEncryptionType === encryptedExportType.FileEncrypted"
/>
</div>
<div>
<label class="tw-semi-bold tw-text-md tw-ml-1 tw-mt-1 tw-mb-1" for="FileEncrypted">{{
"passwordProtected" | i18n
}}</label>
</div>
</div>
<div class="tw-regular ml-3 tw-text-sm">
{{ "passwordProtectedOptionDescription" | i18n }}
</div>
</div>
<br />
<ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted">
<div class="input-group">
<bit-form-field class="tw-w-full">
<bit-label>{{ "filePassword" | i18n }}</bit-label>
<input
bitInput
type="{{ showFilePassword ? 'text' : 'password' }}"
id="filePassword"
formControlName="filePassword"
name="password"
/>
<div class="input-group-append">
<button
bitSuffix
bitButton
buttonType="secondary"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showFilePassword"
(click)="toggleFilePassword()"
type="button"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showFilePassword, 'bwi-eye-slash': showFilePassword }"
></i>
</button>
</div>
</bit-form-field>
<div class="small text-muted">
{{ "exportPasswordDescription" | i18n }}
</div>
</div>
<div class="input-group tw-mt-4">
<bit-form-field class="tw-w-full">
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
<input
bitInput
type="{{ showConfirmFilePassword ? 'text' : 'password' }}"
id="confirmFilePassword"
formControlName="confirmFilePassword"
name="confirmFilePassword"
/>
<div class="input-group-append">
<button
bitSuffix
bitButton
buttonType="secondary"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showConfirmFilePassword"
(click)="toggleConfirmFilePassword()"
type="button"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{
'bwi-eye': !showConfirmFilePassword,
'bwi-eye-slash': showConfirmFilePassword
}"
></i>
</button>
</div>
</bit-form-field>
</div>
</ng-container>
</ng-container>
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="form.loading || disabled"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "confirmFormat" | i18n }}</span>
</button>
</div>
</div>
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="form.loading || exportForm.disabled"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "exportVault" | i18n }}</span>
</button>
</form>

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/components/export.component";
import { ModalService } from "@bitwarden/angular/services/modal.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";
@ -11,6 +12,9 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { EncryptedExportType } from "@bitwarden/common/enums/encryptedExportType";
import { UserVerificationPromptComponent } from "src/app/components/user-verification-prompt.component";
@Component({
selector: "app-export",
@ -18,6 +22,7 @@ import { UserVerificationService } from "@bitwarden/common/abstractions/userVeri
})
export class ExportComponent extends BaseExportComponent {
organizationId: string;
encryptedExportType = EncryptedExportType;
constructor(
cryptoService: CryptoService,
@ -29,7 +34,8 @@ export class ExportComponent extends BaseExportComponent {
logService: LogService,
userVerificationService: UserVerificationService,
formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService
fileDownloadService: FileDownloadService,
private modalService: ModalService
) {
super(
cryptoService,
@ -46,8 +52,78 @@ export class ExportComponent extends BaseExportComponent {
);
}
async submit() {
if (this.isFileEncryptedExport && this.filePassword != this.confirmFilePassword) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("filePasswordAndConfirmFilePasswordDoNotMatch")
);
return;
}
this.exportForm.markAllAsTouched();
if (!this.exportForm.valid) {
return;
}
if (this.disabledByPolicy) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("personalVaultExportPolicyInEffect")
);
return;
}
const userVerified = await this.verifyUser();
if (!userVerified) {
return;
}
this.doExport();
}
protected saved() {
super.saved();
this.platformUtilsService.showToast("success", null, this.i18nService.t("exportSuccess"));
}
private verifyUser() {
let confirmDescription = "exportWarningDesc";
if (this.isFileEncryptedExport) {
confirmDescription = "fileEncryptedExportWarningDesc";
} else if (this.isAccountEncryptedExport) {
confirmDescription = "encExportKeyWarningDesc";
}
const ref = this.modalService.open(UserVerificationPromptComponent, {
allowMultipleModals: true,
data: {
confirmDescription: confirmDescription,
confirmButtonText: "exportVault",
modalTitle: "confirmVaultExport",
},
});
if (ref == null) {
return;
}
return ref.onClosedPromise();
}
get isFileEncryptedExport() {
return (
this.format === "encrypted_json" &&
this.fileEncryptionType === EncryptedExportType.FileEncrypted
);
}
get isAccountEncryptedExport() {
return (
this.format === "encrypted_json" &&
this.fileEncryptionType === EncryptedExportType.AccountEncrypted
);
}
}

View File

@ -0,0 +1,58 @@
<div
class="modal fade"
role="dialog"
aria-modal="true"
[attr.aria-labelledby]="'confirmVaultImport' | i18n"
>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form #form (ngSubmit)="submit()">
<div class="form-group modal-content">
<h2 class="tw-mt-6 tw-mb-6 tw-ml-3.5 tw-font-semibold" id="confirmVaultImport">
{{ "confirmVaultImport" | i18n | uppercase }}
</h2>
<div
class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-pr-3.5 tw-pt-3.5 tw-pl-3.5"
>
{{ "confirmVaultImportDesc" | i18n }}
<bit-form-field class="tw-w-full tw-pt-3.5">
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
<input
bitInput
required
type="{{ showFilePassword ? 'text' : 'password' }}"
name="filePassword"
[formControl]="filePassword"
appAutofocus
appInputVerbatim
/>
<button
bitSuffix
bitButton
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showFilePassword"
(click)="toggleFilePassword()"
type="button"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showFilePassword, 'bwi-eye-slash': showFilePassword }"
></i>
</button>
</bit-form-field>
</div>
<div
class="tw-flex tw-w-full tw-flex-wrap tw-items-center tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-pl-3.5 tw-pr-3.5 tw-pb-3.5 tw-pt-4 tw-pl-4 tw-pr-4"
>
<button bitButton buttonType="primary" class="tw-mr-2" type="submit" appBlurClick>
<span>{{ "importData" | i18n }}</span>
</button>
<button bitButton buttonType="secondary" type="button" (click)="cancel()">
<span>{{ "cancel" | i18n }}</span>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,31 @@
import { Component } from "@angular/core";
import { FormControl, Validators } from "@angular/forms";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
@Component({
templateUrl: "file-password-prompt.component.html",
})
export class FilePasswordPromptComponent {
showFilePassword: boolean;
filePassword = new FormControl("", Validators.required);
constructor(private modalRef: ModalRef) {}
toggleFilePassword() {
this.showFilePassword = !this.showFilePassword;
}
submit() {
this.filePassword.markAsTouched();
if (!this.filePassword.valid) {
return;
}
this.modalRef.close(this.filePassword.value);
}
cancel() {
this.modalRef.close(null);
}
}

View File

@ -12,12 +12,13 @@ import { ImportService } from "@bitwarden/common/services/import.service";
import { LooseComponentsModule, SharedModule } from "../../shared";
import { ExportComponent } from "./export.component";
import { FilePasswordPromptComponent } from "./file-password-prompt.component";
import { ImportExportRoutingModule } from "./import-export-routing.module";
import { ImportComponent } from "./import.component";
@NgModule({
imports: [SharedModule, LooseComponentsModule, ImportExportRoutingModule],
declarations: [ImportComponent, ExportComponent],
declarations: [ImportComponent, ExportComponent, FilePasswordPromptComponent],
providers: [
{
provide: ImportServiceAbstraction,

View File

@ -3,6 +3,7 @@ import { Router } from "@angular/router";
import * as JSZip from "jszip";
import Swal, { SweetAlertIcon } from "sweetalert2";
import { ModalService } from "@bitwarden/angular/services/modal.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 +11,9 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { ImportOption, ImportType } from "@bitwarden/common/enums/importOptions";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { ImportError } from "@bitwarden/common/importers/importError";
import { FilePasswordPromptComponent } from "./file-password-prompt.component";
@Component({
selector: "app-import",
@ -20,7 +24,7 @@ export class ImportComponent implements OnInit {
importOptions: ImportOption[];
format: ImportType = null;
fileContents: string;
formPromise: Promise<Error>;
formPromise: Promise<ImportError>;
loading = false;
importBlockedByPolicy = false;
@ -33,7 +37,8 @@ export class ImportComponent implements OnInit {
protected router: Router,
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
private logService: LogService
private logService: LogService,
protected modalService: ModalService
) {}
async ngOnInit() {
@ -106,12 +111,25 @@ export class ImportComponent implements OnInit {
try {
this.formPromise = this.importService.import(importer, fileContents, this.organizationId);
const error = await this.formPromise;
let error = await this.formPromise;
if (error?.passwordRequired) {
const filePassword = await this.getFilePassword();
if (filePassword == null) {
this.loading = false;
return;
}
error = await this.doPasswordProtectedImport(filePassword, fileContents);
}
if (error != null) {
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) {
@ -225,4 +243,29 @@ export class ImportComponent implements OnInit {
}
);
}
async getFilePassword(): Promise<string> {
const ref = this.modalService.open(FilePasswordPromptComponent, {
allowMultipleModals: true,
});
if (ref == null) {
return null;
}
return await ref.onClosedPromise();
}
async doPasswordProtectedImport(
filePassword: string,
fileContents: string
): Promise<ImportError> {
const passwordProtectedImporter = this.importService.getImporter(
"bitwardenpasswordprotected",
this.organizationId,
filePassword
);
return this.importService.import(passwordProtectedImporter, fileContents, this.organizationId);
}
}

View File

@ -678,6 +678,9 @@
"invalidMasterPassword": {
"message": "Invalid master password"
},
"invalidFilePassword": {
"message": "Invalid file password, please use the password you entered when you created the export file."
},
"lockNow": {
"message": "Lock Now"
},
@ -890,6 +893,48 @@
"fileFormat": {
"message": "File Format"
},
"fileEncryptedExportWarningDesc": {
"message": "This file export will be password protected and require the file password to decrypt."
},
"exportPasswordDescription": {
"message": "This password will be used to export and import this file"
},
"confirmMasterPassword": {
"message": "Confirm Master Password"
},
"confirmFormat": {
"message": "Confirm Format"
},
"filePassword": {
"message": "File Password"
},
"confirmFilePassword": {
"message": "Confirm File Password"
},
"accountBackupOptionDescription": {
"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."
},
"passwordProtectedOptionDescription": {
"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"
},
"accountBackup": {
"message": "Account Backup"
},
"passwordProtected": {
"message": "Password Protected"
},
"filePasswordAndConfirmFilePasswordDoNotMatch": {
"message": "“File password” and “Confirm File Password“ do not match."
},
"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."
},

View File

@ -6,6 +6,21 @@
}
}
.modal-footer-content {
border: none;
border-radius: none;
@include themify($themes) {
background-color: themed("footerBackgroundColor");
}
position: relative;
display: flex;
flex-direction: column;
width: 100%;
pointer-events: auto;
background-clip: padding-box;
outline: 0;
}
.modal-dialog {
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 0.3rem;

View File

@ -1,5 +1,6 @@
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { Directive, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { merge, takeUntil, Subject, startWith } from "rxjs";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
@ -10,19 +11,25 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { EncryptedExportType } from "@bitwarden/common/enums/encryptedExportType";
import { EventType } from "@bitwarden/common/enums/eventType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
@Directive()
export class ExportComponent implements OnInit {
export class ExportComponent implements OnInit, OnDestroy {
@Output() onSaved = new EventEmitter();
formPromise: Promise<string>;
disabledByPolicy = false;
showFilePassword: boolean;
showConfirmFilePassword: boolean;
exportForm = this.formBuilder.group({
format: ["json"],
secret: [""],
filePassword: ["", Validators.required],
confirmFilePassword: ["", Validators.required],
fileEncryptionType: [EncryptedExportType.AccountEncrypted],
});
formatOptions = [
@ -31,6 +38,8 @@ export class ExportComponent implements OnInit {
{ name: ".json (Encrypted)", value: "encrypted_json" },
];
private destroy$ = new Subject<void>();
constructor(
protected cryptoService: CryptoService,
protected i18nService: I18nService,
@ -47,6 +56,18 @@ export class ExportComponent implements OnInit {
async ngOnInit() {
await this.checkExportDisabled();
merge(
this.exportForm.get("format").valueChanges,
this.exportForm.get("fileEncryptionType").valueChanges
)
.pipe(takeUntil(this.destroy$))
.pipe(startWith(0))
.subscribe(() => this.adjustValidators());
}
ngOnDestroy(): void {
this.destroy$.next();
}
async checkExportDisabled() {
@ -62,6 +83,20 @@ export class ExportComponent implements OnInit {
return this.format === "encrypted_json";
}
protected async doExport() {
try {
this.formPromise = this.getExportData();
const data = await this.formPromise;
this.downloadFile(data);
this.saved();
await this.collectEvent();
this.exportForm.get("secret").setValue("");
this.exportForm.clearValidators();
} catch (e) {
this.logService.error(e);
}
}
async submit() {
if (this.disabledByPolicy) {
this.platformUtilsService.showToast(
@ -76,25 +111,15 @@ export class ExportComponent implements OnInit {
if (!acceptedWarning) {
return;
}
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 {
this.formPromise = this.getExportData();
const data = await this.formPromise;
this.downloadFile(data);
this.saved();
await this.collectEvent();
this.exportForm.get("secret").setValue("");
} catch (e) {
this.logService.error(e);
}
this.doExport();
}
async warningDialog() {
@ -126,7 +151,14 @@ export class ExportComponent implements OnInit {
}
protected getExportData() {
return this.exportService.getExport(this.format);
if (
this.format === "encrypted_json" &&
this.fileEncryptionType === EncryptedExportType.FileEncrypted
) {
return this.exportService.getPasswordProtectedExport(this.filePassword);
} else {
return this.exportService.getExport(this.format, null);
}
}
protected getFileName(prefix?: string) {
@ -150,6 +182,41 @@ export class ExportComponent implements OnInit {
return this.exportForm.get("format").value;
}
get filePassword() {
return this.exportForm.get("filePassword").value;
}
get confirmFilePassword() {
return this.exportForm.get("confirmFilePassword").value;
}
get fileEncryptionType() {
return this.exportForm.get("fileEncryptionType").value;
}
toggleFilePassword() {
this.showFilePassword = !this.showFilePassword;
document.getElementById("filePassword").focus();
}
toggleConfirmFilePassword() {
this.showConfirmFilePassword = !this.showConfirmFilePassword;
document.getElementById("confirmFilePassword").focus();
}
adjustValidators() {
this.exportForm.get("confirmFilePassword").reset();
this.exportForm.get("filePassword").reset();
if (this.encryptedFormat && this.fileEncryptionType == EncryptedExportType.FileEncrypted) {
this.exportForm.controls.filePassword.enable();
this.exportForm.controls.confirmFilePassword.enable();
} else {
this.exportForm.controls.filePassword.disable();
this.exportForm.controls.confirmFilePassword.disable();
}
}
private downloadFile(csv: string): void {
const fileName = this.getFileName();
this.fileDownloadService.download({

View File

@ -0,0 +1,46 @@
import { Directive } from "@angular/core";
import { FormBuilder, FormControl } from "@angular/forms";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { ModalConfig } from "../services/modal.service";
import { ModalRef } from "./modal/modal.ref";
/**
* Used to verify the user's identity (using their master password or email-based OTP for Key Connector users). You can customize all of the text in the modal.
*/
@Directive()
export class UserVerificationPromptComponent {
confirmDescription = this.config.data.confirmDescription;
confirmButtonText = this.config.data.confirmButtonText;
modalTitle = this.config.data.modalTitle;
secret = new FormControl();
constructor(
private modalRef: ModalRef,
protected config: ModalConfig,
protected userVerificationService: UserVerificationService,
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
) {}
async submit() {
try {
//Incorrect secret will throw an invalid password error.
await this.userVerificationService.verifyUser(this.secret.value);
} catch (e) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
return;
}
this.modalRef.close(true);
}
}

View File

@ -390,6 +390,7 @@ export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
CipherServiceAbstraction,
ApiServiceAbstraction,
CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction,
],
},
{

View File

@ -0,0 +1,4 @@
export enum EncryptedExportType {
AccountEncrypted = 0,
FileEncrypted = 1,
}

View File

@ -35,7 +35,7 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
if (!(await this.checkPassword(parsedData))) {
result.success = false;
result.errorMessage = this.i18nService.t("importEncKeyError");
result.errorMessage = this.i18nService.t("invalidFilePassword");
return result;
}