1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

Merge branch 'PS-55-EncryptedExport' into PS55-6-22

This commit is contained in:
CarleyDiaz-Bitwarden
2022-06-22 15:50:29 -04:00
24 changed files with 812 additions and 39 deletions

15
apps/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "https://localhost:8080/#",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@@ -4,6 +4,8 @@ import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/components/export.component";
import { ModalConfig, ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
@@ -12,7 +14,9 @@ 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 { UserVerificationPromptService } from "@bitwarden/common/abstractions/userVerificationPrompt.service";
const BroadcasterSubscriptionId = "ExportComponent";
@@ -31,7 +35,12 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
userVerificationService: UserVerificationService,
formBuilder: FormBuilder,
private broadcasterService: BroadcasterService,
logService: LogService
logService: LogService,
protected modalService: ModalService,
protected apiService: ApiService,
protected stateService: StateService,
protected userVerificationPromptService: UserVerificationPromptService,
protected modalConfig: ModalConfig
) {
super(
cryptoService,
@@ -43,7 +52,12 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
window,
logService,
userVerificationService,
formBuilder
formBuilder,
modalService,
apiService,
stateService,
userVerificationPromptService,
modalConfig
);
}

View File

@@ -0,0 +1,64 @@
<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()">
<div class="tw-text-center bwi-3x">
<i class="bwi bwi-exclamation-triangle text-warning"></i>
</div>
<h2 class="tw-text-center tw-font-semibold" id="confirmUserTitle">
{{ "confirmVaultImport" | i18n }}
</h2>
<div class="modal-body">
<div>
{{ "confirmVaultImportDesc" | i18n }}
</div>
</div>
<div class="modal-footer">
<div class="form-group modal-content">
<label for="filePassword">{{ "confirmFilePassword" | i18n }}</label>
<div>
<div class="input-group">
<input
id="filePassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="filePassword"
class="text-monospace form-control"
[(ngModel)]="filePassword"
required
appAutofocus
appInputVerbatim
/>
<div class="input-group-append">
<div class="action-buttons">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="tw-mt-4">
<button type="submit" class="btn btn-primary btn-submit" appBlurClick>
<span>{{ "importData" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,8 @@
import { Component } from "@angular/core";
import { FilePasswordPromptComponent as BaseFilePasswordPrompt } from "@bitwarden/angular/components/file-password-prompt.component";
@Component({
templateUrl: "file-password-prompt.component.html",
})
export class FilePasswordPromptComponent extends BaseFilePasswordPrompt {}

View File

@@ -0,0 +1,36 @@
<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()">
<div class="tw-text-center bwi-3x">
<i class="bwi bwi-exclamation-triangle text-warning"></i>
</div>
<h2 class="tw-text-center tw-font-semibold" id="confirmUserTitle">
{{ modalTitle | i18n }}
</h2>
<div class="modal-body">
<div>
{{ confirmDescription | i18n }}
</div>
</div>
<div class="modal-footer">
<div class="modal-footer-content">
<div [formGroup]="myGroup">
<div class="form-group">
<app-user-verification ngDefaultControl formControlName="secret" name="secret">
</app-user-verification>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" appBlurClick>
<span>{{ confirmButtonText | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-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

@@ -19,11 +19,13 @@ import { UpdatePasswordComponent } from "../accounts/update-password.component";
import { UpdateTempPasswordComponent } from "../accounts/update-temp-password.component";
import { VerifyEmailTokenComponent } from "../accounts/verify-email-token.component";
import { VerifyRecoverDeleteComponent } from "../accounts/verify-recover-delete.component";
import { FilePasswordPromptComponent } from "../components/file-password-prompt.component";
import { NestedCheckboxComponent } from "../components/nested-checkbox.component";
import { OrganizationSwitcherComponent } from "../components/organization-switcher.component";
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
import { PasswordStrengthComponent } from "../components/password-strength.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";
@@ -271,6 +273,8 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
PasswordGeneratorHistoryComponent,
PasswordGeneratorPolicyComponent,
PasswordRepromptComponent,
FilePasswordPromptComponent,
UserVerificationPromptComponent,
PasswordStrengthComponent,
PaymentComponent,
PaymentMethodComponent,
@@ -430,6 +434,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
PasswordGeneratorHistoryComponent,
PasswordGeneratorPolicyComponent,
PasswordRepromptComponent,
FilePasswordPromptComponent,
PasswordStrengthComponent,
PaymentComponent,
PaymentMethodComponent,

View File

@@ -2,6 +2,9 @@ import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { ModalConfig, ModalService } from "@bitwarden/angular/services/modal.service";
import { UserVerificationPromptService } from "@bitwarden/angular/services/userVerificationPrompt.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { ExportService } from "@bitwarden/common/abstractions/export.service";
@@ -9,6 +12,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification.service";
import { ExportComponent as BaseExportComponent } from "../../tools/export.component";
@@ -28,7 +32,12 @@ export class ExportComponent extends BaseExportComponent {
policyService: PolicyService,
logService: LogService,
userVerificationService: UserVerificationService,
formBuilder: FormBuilder
formBuilder: FormBuilder,
modalService: ModalService,
apiService: ApiService,
stateService: StateService,
userVerificationPromptService: UserVerificationPromptService,
modalConfig: ModalConfig
) {
super(
cryptoService,
@@ -39,7 +48,12 @@ export class ExportComponent extends BaseExportComponent {
policyService,
logService,
userVerificationService,
formBuilder
formBuilder,
modalService,
apiService,
stateService,
userVerificationPromptService,
modalConfig
);
}

View File

@@ -1,8 +1,10 @@
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 { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@@ -25,9 +27,20 @@ export class ImportComponent extends BaseImportComponent {
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
private organizationService: OrganizationService,
logService: LogService
logService: LogService,
modalService: ModalService,
keyConnectorService: KeyConnectorService
) {
super(i18nService, importService, router, platformUtilsService, policyService, logService);
super(
i18nService,
importService,
router,
platformUtilsService,
policyService,
logService,
modalService,
keyConnectorService
);
}
async ngOnInit() {

View File

@@ -9,11 +9,17 @@ import {
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
} from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
import {
ModalService as ModalServiceAbstraction,
ModalConfig as ModalConfigAbstraction,
ModalConfig,
} from "@bitwarden/angular/services/modal.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/common/abstractions/collection.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { ExportService as ExportServiceAbstraction } from "@bitwarden/common/abstractions/export.service";
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/abstractions/folder.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service";
import { ImportService as ImportServiceAbstraction } from "@bitwarden/common/abstractions/import.service";
@@ -24,7 +30,9 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@bitwarden/common/abstractions/storage.service";
import { UserVerificationPromptService as UserVerificationPromptServiceAbstraction } from "@bitwarden/common/abstractions/userVerificationPrompt.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { ExportService } from "@bitwarden/common/services/export.service";
import { ImportService } from "@bitwarden/common/services/import.service";
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
@@ -37,6 +45,7 @@ import { MemoryStorageService } from "../../services/memoryStorage.service";
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
import { StateService } from "../../services/state.service";
import { StateMigrationService } from "../../services/stateMigration.service";
import { UserVerificationPromptService } from "../../services/userVerificationPrompt.service";
import { WebPlatformUtilsService } from "../../services/webPlatformUtils.service";
import { HomeGuard } from "../guards/home.guard";
import { PermissionsGuard as OrgPermissionsGuard } from "../organizations/guards/permissions.guard";
@@ -90,6 +99,7 @@ import { RouterService } from "./router.service";
},
{ provide: MessagingServiceAbstraction, useClass: BroadcasterMessagingService },
{ provide: ModalServiceAbstraction, useClass: ModalService },
{ provide: ModalConfigAbstraction, useClass: ModalConfig },
{
provide: ImportServiceAbstraction,
useClass: ImportService,
@@ -103,6 +113,17 @@ import { RouterService } from "./router.service";
CryptoServiceAbstraction,
],
},
{
provide: ExportServiceAbstraction,
useClass: ExportService,
deps: [
FolderServiceAbstraction,
CipherServiceAbstraction,
ApiServiceAbstraction,
CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction,
],
},
{
provide: StateMigrationServiceAbstraction,
useClass: StateMigrationService,
@@ -128,6 +149,10 @@ import { RouterService } from "./router.service";
provide: PasswordRepromptServiceAbstraction,
useClass: PasswordRepromptService,
},
{
provide: UserVerificationPromptServiceAbstraction,
useClass: UserVerificationPromptService,
},
HomeGuard,
],
})

View File

@@ -4,6 +4,7 @@
ngNativeValidate
[appApiAction]="formPromise"
[formGroup]="exportForm"
*ngIf="exportForm"
>
<div class="page-header">
<h1>{{ "exportVault" | i18n }}</h1>
@@ -20,23 +21,123 @@
<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">
<select class="form-control" name="format" formControlName="format">
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
</select>
</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="radio-header">
{{ "fileTypeHeading" | i18n }}
</label>
<div appBoxRow name="FileTypeOptions">
<input
type="radio"
class="radio"
name="fileEncryptionType"
id="1"
[value]="0"
formControlName="fileEncryptionType"
/>
<label class="unstyled"> {{ "accountBackup" | i18n }} </label>
<div class="small text-muted" style="margin-left: 1.25em">
{{ "accountBackupOptionDescription" | i18n }}
</div>
<input
type="radio"
class="radio"
name="fileEncryptionType"
id="2"
[value]="1"
formControlName="fileEncryptionType"
/>
<label class="unstyled">{{ "passwordProtected" | i18n }}</label>
<div class="small text-muted" style="margin-left: 1.25em">
{{ "passwordProtectedOptionDescription" | i18n }}
</div>
</div>
<br />
</div>
<ng-container *ngIf="fileEncryptionType == 1">
<label for="format">{{ "filePassword" | i18n }}</label>
<div class="input-group">
<input
type="{{ showPassword ? 'text' : 'password' }}"
formControlName="password"
name="password"
class="form-control"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="small text-muted">
{{ "exportPasswordDescription" | i18n }}
</div>
<br />
<label for="format">{{ "confirmFilePassword" | i18n }}</label>
<div class="input-group">
<input
formControlName="confirmPassword"
name="confirmPassword"
class="form-control"
type="{{ showConfirmPassword ? 'text' : 'password' }}"
/>
<div class="input-group-append">
<div class="action-buttons">
<button
type="button"
class="btn btn-outline-secondary"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showConfirmPassword"
(click)="toggleConfirmPassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{
'bwi-eye': !showConfirmPassword,
'bwi-eye-slash': showConfirmPassword
}"
></i>
</button>
</div>
</div>
</div>
<br />
</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

@@ -1,7 +1,9 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder, FormControl, FormGroup } from "@angular/forms";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/components/export.component";
import { ModalConfig, 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,7 +11,10 @@ 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 { UserVerificationPromptService } from "@bitwarden/common/abstractions/userVerificationPrompt.service";
import { EncryptedExportType } from "@bitwarden/common/enums/EncryptedExportType";
@Component({
selector: "app-export",
@@ -17,6 +22,11 @@ import { UserVerificationService } from "@bitwarden/common/abstractions/userVeri
})
export class ExportComponent extends BaseExportComponent {
organizationId: string;
showPassword: boolean;
showConfirmPassword: boolean;
confirmDescription: string;
confirmButtonText: string;
modalTitle: string;
constructor(
cryptoService: CryptoService,
@@ -27,7 +37,12 @@ export class ExportComponent extends BaseExportComponent {
policyService: PolicyService,
logService: LogService,
userVerificationService: UserVerificationService,
formBuilder: FormBuilder
formBuilder: FormBuilder,
modalService: ModalService,
apiService: ApiService,
stateService: StateService,
userVerificationPromptService: UserVerificationPromptService,
modalConfig: ModalConfig
) {
super(
cryptoService,
@@ -39,12 +54,85 @@ export class ExportComponent extends BaseExportComponent {
window,
logService,
userVerificationService,
formBuilder
formBuilder,
modalService,
apiService,
stateService,
userVerificationPromptService,
modalConfig
);
}
async submit() {
const confirmDescription =
this.exportForm.get("fileEncryptionType").value == EncryptedExportType.FileEncrypted
? "confirmVaultExportDesc"
: "encExportKeyWarningDesc";
const confirmButtonText = "exportVault";
const modalTitle = "confirmVaultExport";
if (!this.validForm) {
return;
}
try {
if (
await this.userVerificationPromptService.showUserVerificationPrompt(
confirmDescription,
confirmButtonText,
modalTitle
)
) {
//successful
this.submitWithSecretAlreadyVerified();
}
} catch {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
}
}
togglePassword() {
this.exportForm.get("showPassword").setValue(!this.exportForm.get("showPassword").value);
document.getElementById("newPassword").focus();
}
toggleConfirmPassword() {
this.exportForm
.get("showConfirmPassword")
.setValue(!this.exportForm.get("showConfirmPassword").value);
document.getElementById("newConfirmPassword").focus();
}
protected saved() {
super.saved();
this.platformUtilsService.showToast("success", null, this.i18nService.t("exportSuccess"));
}
get validForm() {
if (
this.fileEncryptionType == EncryptedExportType.FileEncrypted &&
this.format == "encrypted_json"
) {
if (this.password.length > 0 || this.confirmPassword.length > 0) {
if (this.password != this.confirmPassword) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("filePasswordAndConfirmFilePasswordDoNotMatch")
);
return false;
}
this.encryptionPassword = this.password;
return true;
}
} else {
this.clearPasswordField();
return true;
}
}
}

View File

@@ -3,13 +3,18 @@ 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 { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.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 { ImportOption, ImportType } from "@bitwarden/common/enums/importOptions";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { ImportError } from "@bitwarden/common/importers/importError";
import { FilePasswordPromptComponent } from "../components/file-password-prompt.component";
@Component({
selector: "app-import",
@@ -20,9 +25,10 @@ export class ImportComponent implements OnInit {
importOptions: ImportOption[];
format: ImportType = null;
fileContents: string;
formPromise: Promise<Error>;
formPromise: Promise<ImportError>;
loading = false;
importBlockedByPolicy = false;
protected component = FilePasswordPromptComponent;
protected organizationId: string = null;
protected successNavigate: any[] = ["vault"];
@@ -33,7 +39,9 @@ export class ImportComponent implements OnInit {
protected router: Router,
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
private logService: LogService
private logService: LogService,
protected modalService: ModalService,
protected keyConnectorService: KeyConnectorService
) {}
async ngOnInit() {
@@ -55,7 +63,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 +115,23 @@ 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.promptFilePassword(fileContents)) {
//successful
} else {
//failed - File Password issues
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 +141,10 @@ export class ImportComponent implements OnInit {
this.loading = false;
}
private async promptFilePassword(fcontents: string) {
return await this.showPasswordPrompt(fcontents, this.organizationId);
}
getFormatInstructionTitle() {
if (this.format == null) {
return null;
@@ -225,4 +249,33 @@ export class ImportComponent implements OnInit {
}
);
}
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 !this.keyConnectorService.getUsesKeyConnector();
}
}

View File

@@ -666,6 +666,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"
},
@@ -878,6 +881,48 @@
"fileFormat": {
"message": "File Format"
},
"confirmVaultExportDesc" : {
"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

@@ -0,0 +1,10 @@
import { Injectable } from "@angular/core";
import { UserVerificationPromptService as BaseUserVerificationPrompt } from "@bitwarden/angular/services/userVerificationPrompt.service";
import { UserVerificationPromptComponent } from "../app/components/user-verification-prompt.component";
@Injectable()
export class UserVerificationPromptService extends BaseUserVerificationPrompt {
component = UserVerificationPromptComponent;
}

View File

@@ -1,6 +1,15 @@
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
import {
Directive,
EventEmitter,
OnInit,
Output,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ModalConfig, 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";
@@ -8,7 +17,10 @@ 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 { UserVerificationPromptService } from "@bitwarden/common/abstractions/userVerificationPrompt.service";
import { EncryptedExportType } from "@bitwarden/common/enums/EncryptedExportType";
import { EventType } from "@bitwarden/common/enums/eventType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
@@ -19,9 +31,17 @@ export class ExportComponent implements OnInit {
formPromise: Promise<string>;
disabledByPolicy = 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,12 @@ export class ExportComponent implements OnInit {
protected win: Window,
private logService: LogService,
private userVerificationService: UserVerificationService,
private formBuilder: FormBuilder
protected formBuilder: FormBuilder,
protected modalService: ModalService,
protected apiService: ApiService,
protected stateService: StateService,
protected userVerificationPromptService: UserVerificationPromptService,
protected modalConfig: ModalConfig
) {}
async ngOnInit() {
@@ -60,6 +85,30 @@ export class ExportComponent implements OnInit {
return this.format === "encrypted_json";
}
async submitWithSecretAlreadyVerified() {
if (this.disabledByPolicy) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("personalVaultExportPolicyInEffect")
);
return;
}
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.get("password").setValue("");
this.exportForm.get("confirmPassword").setValue("");
} catch (e) {
this.logService.error(e);
}
}
async submit() {
if (this.disabledByPolicy) {
this.platformUtilsService.showToast(
@@ -74,12 +123,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);
const successfulVerification = await this.userVerificationService.verifyUser(secret);
if (!successfulVerification) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
return;
}
@@ -124,7 +176,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 +196,26 @@ export class ExportComponent implements OnInit {
await this.eventService.collect(EventType.User_ClientExportedVault);
}
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);

View File

@@ -0,0 +1,60 @@
import { Directive } from "@angular/core";
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 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.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidFilePassword")
);
} else {
this.modalRef.close(true);
}
}
}

View File

@@ -0,0 +1,56 @@
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.service";
import { ModalConfig } from "../services/modal.service";
import { ModalRef } from "./modal/modal.ref";
/**
* Used to verify the user's secret, you can customize all of the text in the modal.
*/
@Directive()
export class UserVerificationPromptComponent {
showPassword = false;
organizationId = "";
confirmDescription = this.config.data.confirmDescription;
confirmButtonText = this.config.data.confirmButtonText;
modalTitle = this.config.data.modalTitle;
myGroup = this.formBuilder.group({
secret: new FormControl(),
});
constructor(
private modalRef: ModalRef,
protected config: ModalConfig,
protected userVerificationService: UserVerificationService,
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
) {}
togglePassword() {
this.showPassword = !this.showPassword;
}
async submit() {
const secret = this.myGroup.get("secret").value;
try {
//Incorrect secret will throw an invalid password error.
await this.userVerificationService.verifyUser(secret);
} catch (e) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
return;
}
this.modalRef.close(true);
}
}

View File

@@ -0,0 +1,56 @@
import { Injectable } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { UserVerificationPromptService as UserVerificationPromptServiceAbstraction } from "@bitwarden/common/abstractions/userVerificationPrompt.service";
import { UserVerificationPromptComponent } from "../components/user-verification-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 UserVerificationPromptService implements UserVerificationPromptServiceAbstraction {
protected component = UserVerificationPromptComponent;
constructor(
private modalService: ModalService,
private keyConnectorService: KeyConnectorService,
protected i18nService: I18nService
) {}
protectedFields() {
return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"];
}
async showUserVerificationPrompt(
confirmDescription?: string,
confirmButtonText?: string,
modalTitle?: string
) {
if (!(await this.enabled())) {
return true;
}
const ref = await this.modalService.open(this.component, {
allowMultipleModals: true,
data: {
confirmDescription: confirmDescription ? confirmDescription : "passwordConfirmationDesc",
confirmButtonText: confirmButtonText ? confirmButtonText : "ok",
modalTitle: modalTitle ? modalTitle : "passwordConfirmation",
},
});
if (ref == null) {
return false;
}
return (await ref.onClosedPromise()) === true;
}
async enabled() {
return !(await this.keyConnectorService.getUsesKeyConnector());
}
}

View File

@@ -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<string>;
getExport: (format?: ExportFormat, organizationId?: string, password?: string) => Promise<string>;
getPasswordProtectedExport: (password: string, organizationId?: string) => Promise<string>;
getOrganizationExport: (organizationId: string, format?: ExportFormat) => Promise<string>;
getEventExport: (events: EventView[]) => Promise<string>;

View File

@@ -0,0 +1,9 @@
export abstract class UserVerificationPromptService {
protectedFields: () => string[];
showUserVerificationPrompt: (
confirmDescription?: string,
confirmButtonText?: string,
modalTitle?: string
) => Promise<boolean>;
enabled: () => Promise<boolean>;
}

View File

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

View File

@@ -36,13 +36,19 @@ export class ExportService implements ExportServiceAbstraction {
private cryptoFunctionService: CryptoFunctionService
) {}
async getExport(format: ExportFormat = "csv", organizationId?: string): Promise<string> {
async getExport(
format: ExportFormat = "csv",
organizationId?: string,
password?: string
): Promise<string> {
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);
}