mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[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 commiteb2cdffe49
. * Requested changes and improvements * merging master into feature branch * Revert "merging master into feature branch" This reverts commite287715251
. * 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:
parent
231e1bf666
commit
a108476c3c
@ -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."
|
||||
|
@ -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>
|
@ -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 {}
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -390,6 +390,7 @@ export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
|
||||
CipherServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
4
libs/common/src/enums/encryptedExportType.ts
Normal file
4
libs/common/src/enums/encryptedExportType.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum EncryptedExportType {
|
||||
AccountEncrypted = 0,
|
||||
FileEncrypted = 1,
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user