mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
[PM-23995] Updated change kdf component for Forced update KDF settings (#16516)
* move change-kdf into KM ownership * Change kdf component update for Forced KDF update * correct validators load on init * incorrect feature flag observable check * unit test coverage * unit test coverage * remove Close button, wrong icon * change to `pm-23995-no-logout-on-kdf-change` feature flag * updated unit tests * revert bad merge Signed-off-by: Maciej Zieniuk <mzieniuk@bitwarden.com> * updated wording, TS strict enabled, use form controls, updated tests * use localisation for button label * small margin in confirmation dialog * simpler I18nService mock --------- Signed-off-by: Maciej Zieniuk <mzieniuk@bitwarden.com>
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
<form [formGroup]="form" [bitSubmit]="submit" autocomplete="off">
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ "changeKdf" | i18n }}
|
||||
{{ "updateYourEncryptionSettings" | i18n }}
|
||||
</span>
|
||||
|
||||
<span bitDialogContent>
|
||||
@if (!(noLogoutOnKdfChangeFeatureFlag$ | async)) {
|
||||
<bit-callout type="warning">{{ "kdfSettingsChangeLogoutWarning" | i18n }}</bit-callout>
|
||||
<bit-form-field>
|
||||
}
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input bitInput type="password" formControlName="masterPassword" appAutofocus />
|
||||
<button
|
||||
@@ -18,12 +20,12 @@
|
||||
></button>
|
||||
<bit-hint>
|
||||
{{ "confirmIdentity" | i18n }}
|
||||
</bit-hint></bit-form-field
|
||||
>
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" type="submit" bitFormButton>
|
||||
<span>{{ "changeKdf" | i18n }}</span>
|
||||
<span>{{ "updateSettings" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" bitFormButton bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DIALOG_DATA, DialogRef, ToastService } from "@bitwarden/components";
|
||||
import { KdfType, PBKDF2KdfConfig, Argon2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";
|
||||
|
||||
describe("ChangeKdfConfirmationComponent", () => {
|
||||
let component: ChangeKdfConfirmationComponent;
|
||||
let fixture: ComponentFixture<ChangeKdfConfirmationComponent>;
|
||||
|
||||
// Mock Services
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockMessagingService: MockProxy<MessagingService>;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
let mockDialogRef: MockProxy<DialogRef<ChangeKdfConfirmationComponent>>;
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let accountService: FakeAccountService;
|
||||
let mockChangeKdfService: MockProxy<ChangeKdfService>;
|
||||
|
||||
const mockUserId = "user-id" as UserId;
|
||||
const mockEmail = "email";
|
||||
const mockMasterPassword = "master-password";
|
||||
const mockDialogData = jest.fn();
|
||||
const kdfConfig = new PBKDF2KdfConfig(600_001);
|
||||
|
||||
beforeEach(() => {
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockMessagingService = mock<MessagingService>();
|
||||
mockToastService = mock<ToastService>();
|
||||
mockDialogRef = mock<DialogRef<ChangeKdfConfirmationComponent>>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
accountService = mockAccountServiceWith(mockUserId, { email: mockEmail });
|
||||
mockChangeKdfService = mock<ChangeKdfService>();
|
||||
|
||||
mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`);
|
||||
|
||||
// Mock config service feature flag
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
|
||||
mockDialogData.mockReturnValue({
|
||||
kdf: KdfType.PBKDF2_SHA256,
|
||||
kdfConfig,
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ChangeKdfConfirmationComponent],
|
||||
imports: [SharedModule],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: MessagingService, useValue: mockMessagingService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ToastService, useValue: mockToastService },
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ChangeKdfService, useValue: mockChangeKdfService },
|
||||
{
|
||||
provide: DIALOG_DATA,
|
||||
useFactory: mockDialogData,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Initialization", () => {
|
||||
it("should create component with PBKDF2 config", () => {
|
||||
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.kdfConfig).toBeInstanceOf(PBKDF2KdfConfig);
|
||||
expect(component.kdfConfig.iterations).toBe(600_001);
|
||||
});
|
||||
|
||||
it("should create component with Argon2id config", () => {
|
||||
mockDialogData.mockReturnValue({
|
||||
kdf: KdfType.Argon2id,
|
||||
kdfConfig: new Argon2KdfConfig(4, 65, 5),
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.kdfConfig).toBeInstanceOf(Argon2KdfConfig);
|
||||
const kdfConfig = component.kdfConfig as Argon2KdfConfig;
|
||||
expect(kdfConfig.iterations).toBe(4);
|
||||
expect(kdfConfig.memory).toBe(65);
|
||||
expect(kdfConfig.parallelism).toBe(5);
|
||||
});
|
||||
|
||||
it("should initialize form with required master password field", () => {
|
||||
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
expect(component.form.controls.masterPassword).toBeInstanceOf(FormControl);
|
||||
expect(component.form.controls.masterPassword.value).toEqual(null);
|
||||
expect(component.form.controls.masterPassword.hasError("required")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Form Validation", () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("should be invalid when master password is empty", () => {
|
||||
component.form.controls.masterPassword.setValue("");
|
||||
expect(component.form.invalid).toBe(true);
|
||||
});
|
||||
|
||||
it("should be valid when master password is provided", () => {
|
||||
component.form.controls.masterPassword.setValue(mockMasterPassword);
|
||||
expect(component.form.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("submit method", () => {
|
||||
describe("should not update kdf and not show success toast", () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.form.controls.masterPassword.setValue(mockMasterPassword);
|
||||
});
|
||||
|
||||
it("when form is invalid", async () => {
|
||||
// Arrange
|
||||
component.form.controls.masterPassword.setValue("");
|
||||
expect(component.form.invalid).toBe(true);
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(mockChangeKdfService.updateUserKdfParams).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when no active account", async () => {
|
||||
accountService.activeAccount$ = of(null);
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Null or undefined account");
|
||||
|
||||
expect(mockChangeKdfService.updateUserKdfParams).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when kdf is invalid", async () => {
|
||||
// Arrange
|
||||
component.kdfConfig = new PBKDF2KdfConfig(1);
|
||||
|
||||
// Act
|
||||
await expect(component.submit()).rejects.toThrow();
|
||||
|
||||
expect(mockChangeKdfService.updateUserKdfParams).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should update kdf and show success toast", () => {
|
||||
it("should set loading to true during submission", async () => {
|
||||
// Arrange
|
||||
let loadingDuringExecution = false;
|
||||
mockChangeKdfService.updateUserKdfParams.mockImplementation(async () => {
|
||||
loadingDuringExecution = component.loading;
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
component.form.controls.masterPassword.setValue(mockMasterPassword);
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
expect(loadingDuringExecution).toBe(true);
|
||||
expect(component.loading).toBe(false);
|
||||
});
|
||||
|
||||
it("doesn't logout and closes the dialog when feature flag is enabled", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
component.form.controls.masterPassword.setValue(mockMasterPassword);
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(mockChangeKdfService.updateUserKdfParams).toHaveBeenCalledWith(
|
||||
mockMasterPassword,
|
||||
kdfConfig,
|
||||
mockUserId,
|
||||
);
|
||||
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
message: "encKeySettingsChanged-used-i18n",
|
||||
});
|
||||
expect(mockDialogRef.close).toHaveBeenCalled();
|
||||
expect(mockMessagingService.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends a logout and displays a log back in toast when feature flag is disabled", async () => {
|
||||
// Arrange
|
||||
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
component.form.controls.masterPassword.setValue(mockMasterPassword);
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(mockChangeKdfService.updateUserKdfParams).toHaveBeenCalledWith(
|
||||
mockMasterPassword,
|
||||
kdfConfig,
|
||||
mockUserId,
|
||||
);
|
||||
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: "encKeySettingsChanged-used-i18n",
|
||||
message: "logBackIn-used-i18n",
|
||||
});
|
||||
expect(mockMessagingService.send).toHaveBeenCalledWith("logout");
|
||||
expect(mockDialogRef.close).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormGroup, FormControl, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DIALOG_DATA, ToastService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, DialogRef, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfig, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -23,12 +23,13 @@ export class ChangeKdfConfirmationComponent {
|
||||
kdfConfig: KdfConfig;
|
||||
|
||||
form = new FormGroup({
|
||||
masterPassword: new FormControl(null, Validators.required),
|
||||
masterPassword: new FormControl<string | null>(null, Validators.required),
|
||||
});
|
||||
showPassword = false;
|
||||
masterPassword: string;
|
||||
loading = false;
|
||||
|
||||
noLogoutOnKdfChangeFeatureFlag$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private messagingService: MessagingService,
|
||||
@@ -36,9 +37,13 @@ export class ChangeKdfConfirmationComponent {
|
||||
private accountService: AccountService,
|
||||
private toastService: ToastService,
|
||||
private changeKdfService: ChangeKdfService,
|
||||
private dialogRef: DialogRef<ChangeKdfConfirmationComponent>,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
this.kdfConfig = params.kdfConfig;
|
||||
this.masterPassword = null;
|
||||
this.noLogoutOnKdfChangeFeatureFlag$ = configService.getFeatureFlag$(
|
||||
FeatureFlag.NoLogoutOnKdfChange,
|
||||
);
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
@@ -46,24 +51,32 @@ export class ChangeKdfConfirmationComponent {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
await this.makeKeyAndSaveAsync();
|
||||
await this.makeKeyAndSave();
|
||||
if (await firstValueFrom(this.noLogoutOnKdfChangeFeatureFlag$)) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("encKeySettingsChanged"),
|
||||
});
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("encKeySettingsChanged"),
|
||||
message: this.i18nService.t("logBackIn"),
|
||||
});
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
this.loading = false;
|
||||
};
|
||||
|
||||
private async makeKeyAndSaveAsync() {
|
||||
const masterPassword = this.form.value.masterPassword;
|
||||
private async makeKeyAndSave() {
|
||||
const activeAccountId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
const masterPassword = this.form.value.masterPassword!;
|
||||
|
||||
// Ensure the KDF config is valid.
|
||||
this.kdfConfig.validateKdfConfigForSetting();
|
||||
|
||||
const activeAccountId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
await this.changeKdfService.updateUserKdfParams(
|
||||
masterPassword,
|
||||
this.kdfConfig,
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
<h2 bitTypography="h2">{{ "encKeySettings" | i18n }}</h2>
|
||||
<h2 bitTypography="h2" class="tw-mt-6">
|
||||
{{ "encKeySettings" | i18n }}
|
||||
</h2>
|
||||
@if (!(noLogoutOnKdfChangeFeatureFlag$ | async)) {
|
||||
<bit-callout type="warning">{{ "kdfSettingsChangeLogoutWarning" | i18n }}</bit-callout>
|
||||
<p bitTypography="body1">
|
||||
{{ "higherKDFIterations" | i18n }}
|
||||
}
|
||||
<p bitTypography="body1" class="tw-mt-4">
|
||||
{{ "encryptionKeySettingsHowShouldWeEncryptYourData" | i18n }}
|
||||
</p>
|
||||
<p bitTypography="body1">
|
||||
{{
|
||||
"kdfToHighWarningIncreaseInIncrements"
|
||||
| i18n: (isPBKDF2(kdfConfig) ? ("incrementsOf100,000" | i18n) : ("smallIncrements" | i18n))
|
||||
}}
|
||||
{{ "encryptionKeySettingsIncreaseImproveSecurity" | i18n }}
|
||||
</p>
|
||||
<form [formGroup]="formGroup" autocomplete="off">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-x-4">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field>
|
||||
<bit-label
|
||||
>{{ "kdfAlgorithm" | i18n }}
|
||||
<a
|
||||
class="tw-ml-auto"
|
||||
<bit-label>
|
||||
{{ "algorithm" | i18n }}
|
||||
<button
|
||||
type="button"
|
||||
class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-p-0"
|
||||
[bitPopoverTriggerFor]="algorithmPopover"
|
||||
appA11yTitle="{{ 'encryptionKeySettingsAlgorithmPopoverTitle' | i18n }}"
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/kdf-algorithms"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMoreAboutEncryptionAlgorithms' | i18n }}"
|
||||
slot="end"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</button>
|
||||
</bit-label>
|
||||
<bit-select formControlName="kdf">
|
||||
<bit-option
|
||||
@@ -35,33 +34,12 @@
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
<bit-form-field formGroupName="kdfConfig" *ngIf="isArgon2(kdfConfig)">
|
||||
<bit-label>{{ "kdfMemory" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
formControlName="memory"
|
||||
type="number"
|
||||
[min]="ARGON2_MEMORY.min"
|
||||
[max]="ARGON2_MEMORY.max"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-0">
|
||||
<bit-form-field formGroupName="kdfConfig" *ngIf="isPBKDF2(kdfConfig)">
|
||||
@if (isPBKDF2(kdfConfig)) {
|
||||
<bit-form-field formGroupName="kdfConfig">
|
||||
<bit-label>
|
||||
{{ "kdfIterations" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
class="tw-ml-auto"
|
||||
href="https://bitwarden.com/help/what-encryption-is-used/#changing-kdf-iterations"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMoreAboutKDFIterations' | i18n }}"
|
||||
slot="end"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
@@ -72,7 +50,21 @@
|
||||
/>
|
||||
<bit-hint>{{ "kdfIterationRecommends" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<ng-container *ngIf="isArgon2(kdfConfig)">
|
||||
} @else if (isArgon2(kdfConfig)) {
|
||||
<bit-form-field formGroupName="kdfConfig">
|
||||
<bit-label>{{ "kdfMemory" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
formControlName="memory"
|
||||
type="number"
|
||||
[min]="ARGON2_MEMORY.min"
|
||||
[max]="ARGON2_MEMORY.max"
|
||||
/>
|
||||
</bit-form-field>
|
||||
}
|
||||
</div>
|
||||
@if (isArgon2(kdfConfig)) {
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field formGroupName="kdfConfig">
|
||||
<bit-label>
|
||||
{{ "kdfIterations" | i18n }}
|
||||
@@ -85,6 +77,8 @@
|
||||
[max]="ARGON2_ITERATIONS.max"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field formGroupName="kdfConfig">
|
||||
<bit-label>
|
||||
{{ "kdfParallelism" | i18n }}
|
||||
@@ -97,9 +91,8 @@
|
||||
[max]="ARGON2_PARALLELISM.max"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
(click)="openConfirmationModal()"
|
||||
@@ -107,7 +100,27 @@
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
class="tw-mt-2"
|
||||
>
|
||||
{{ "changeKdf" | i18n }}
|
||||
{{ "updateEncryptionSettings" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<bit-popover [title]="'encryptionKeySettingsAlgorithmPopoverTitle' | i18n" #algorithmPopover>
|
||||
<ul class="tw-mt-2 tw-mb-0 tw-ps-4">
|
||||
<li class="tw-mb-2">{{ "encryptionKeySettingsAlgorithmPopoverPBKDF2" | i18n }}</li>
|
||||
<li>{{ "encryptionKeySettingsAlgorithmPopoverArgon2Id" | i18n }}</li>
|
||||
</ul>
|
||||
<div class="tw-mt-4 tw-mb-1">
|
||||
<a
|
||||
href="https://bitwarden.com/help/kdf-algorithms/"
|
||||
bitLink
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMoreAboutEncryptionAlgorithms' | i18n }}"
|
||||
>
|
||||
{{ "learnMore" | i18n }}
|
||||
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</bit-popover>
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder, FormControl } from "@angular/forms";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, PopoverModule, CalloutModule } from "@bitwarden/components";
|
||||
import {
|
||||
KdfConfigService,
|
||||
Argon2KdfConfig,
|
||||
PBKDF2KdfConfig,
|
||||
KdfType,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { ChangeKdfComponent } from "./change-kdf.component";
|
||||
|
||||
describe("ChangeKdfComponent", () => {
|
||||
let component: ChangeKdfComponent;
|
||||
let fixture: ComponentFixture<ChangeKdfComponent>;
|
||||
|
||||
// Mock Services
|
||||
let mockDialogService: MockProxy<DialogService>;
|
||||
let mockKdfConfigService: MockProxy<KdfConfigService>;
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let accountService: FakeAccountService;
|
||||
let formBuilder: FormBuilder;
|
||||
|
||||
const mockUserId = "user-id" as UserId;
|
||||
|
||||
// Helper functions for validation testing
|
||||
function expectPBKDF2Validation(
|
||||
iterationsControl: FormControl<number | null>,
|
||||
memoryControl: FormControl<number | null>,
|
||||
parallelismControl: FormControl<number | null>,
|
||||
) {
|
||||
// Assert current validators state
|
||||
expect(iterationsControl.hasError("required")).toBe(false);
|
||||
expect(iterationsControl.hasError("min")).toBe(false);
|
||||
expect(iterationsControl.hasError("max")).toBe(false);
|
||||
expect(memoryControl.validator).toBeNull();
|
||||
expect(parallelismControl.validator).toBeNull();
|
||||
|
||||
// Test validation boundaries
|
||||
iterationsControl.setValue(PBKDF2KdfConfig.ITERATIONS.min - 1);
|
||||
expect(iterationsControl.hasError("min")).toBe(true);
|
||||
|
||||
iterationsControl.setValue(PBKDF2KdfConfig.ITERATIONS.max + 1);
|
||||
expect(iterationsControl.hasError("max")).toBe(true);
|
||||
}
|
||||
|
||||
function expectArgon2Validation(
|
||||
iterationsControl: FormControl<number | null>,
|
||||
memoryControl: FormControl<number | null>,
|
||||
parallelismControl: FormControl<number | null>,
|
||||
) {
|
||||
// Assert current validators state
|
||||
expect(iterationsControl.hasError("required")).toBe(false);
|
||||
expect(memoryControl.hasError("required")).toBe(false);
|
||||
expect(parallelismControl.hasError("required")).toBe(false);
|
||||
|
||||
// Test validation boundaries - min values
|
||||
iterationsControl.setValue(Argon2KdfConfig.ITERATIONS.min - 1);
|
||||
expect(iterationsControl.hasError("min")).toBe(true);
|
||||
|
||||
memoryControl.setValue(Argon2KdfConfig.MEMORY.min - 1);
|
||||
expect(memoryControl.hasError("min")).toBe(true);
|
||||
|
||||
parallelismControl.setValue(Argon2KdfConfig.PARALLELISM.min - 1);
|
||||
expect(parallelismControl.hasError("min")).toBe(true);
|
||||
|
||||
// Test validation boundaries - max values
|
||||
iterationsControl.setValue(Argon2KdfConfig.ITERATIONS.max + 1);
|
||||
expect(iterationsControl.hasError("max")).toBe(true);
|
||||
|
||||
memoryControl.setValue(Argon2KdfConfig.MEMORY.max + 1);
|
||||
expect(memoryControl.hasError("max")).toBe(true);
|
||||
|
||||
parallelismControl.setValue(Argon2KdfConfig.PARALLELISM.max + 1);
|
||||
expect(parallelismControl.hasError("max")).toBe(true);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockDialogService = mock<DialogService>();
|
||||
mockKdfConfigService = mock<KdfConfigService>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
formBuilder = new FormBuilder();
|
||||
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
|
||||
mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ChangeKdfComponent],
|
||||
imports: [SharedModule, PopoverModule, CalloutModule],
|
||||
providers: [
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
{ provide: KdfConfigService, useValue: mockKdfConfigService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: FormBuilder, useValue: formBuilder },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Initialization", () => {
|
||||
describe("given PBKDF2 configuration", () => {
|
||||
it("should initialize form with PBKDF2 values and validators when component loads", async () => {
|
||||
// Arrange
|
||||
const mockPBKDF2Config = new PBKDF2KdfConfig(600_000);
|
||||
mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config);
|
||||
|
||||
// Act
|
||||
fixture = TestBed.createComponent(ChangeKdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
|
||||
// Extract form controls
|
||||
const formGroup = component["formGroup"];
|
||||
|
||||
// Assert form values
|
||||
expect(formGroup.controls.kdf.value).toBe(KdfType.PBKDF2_SHA256);
|
||||
const kdfConfigFormGroup = formGroup.controls.kdfConfig;
|
||||
expect(kdfConfigFormGroup.controls.iterations.value).toBe(600_000);
|
||||
expect(kdfConfigFormGroup.controls.memory.value).toBeNull();
|
||||
expect(kdfConfigFormGroup.controls.parallelism.value).toBeNull();
|
||||
expect(component.kdfConfig).toEqual(mockPBKDF2Config);
|
||||
|
||||
// Assert validators
|
||||
expectPBKDF2Validation(
|
||||
kdfConfigFormGroup.controls.iterations,
|
||||
kdfConfigFormGroup.controls.memory,
|
||||
kdfConfigFormGroup.controls.parallelism,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given Argon2id configuration", () => {
|
||||
it("should initialize form with Argon2id values and validators when component loads", async () => {
|
||||
// Arrange
|
||||
const mockArgon2Config = new Argon2KdfConfig(3, 64, 4);
|
||||
mockKdfConfigService.getKdfConfig.mockResolvedValue(mockArgon2Config);
|
||||
|
||||
// Act
|
||||
fixture = TestBed.createComponent(ChangeKdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
|
||||
// Extract form controls
|
||||
const formGroup = component["formGroup"];
|
||||
|
||||
// Assert form values
|
||||
expect(formGroup.controls.kdf.value).toBe(KdfType.Argon2id);
|
||||
const kdfConfigFormGroup = formGroup.controls.kdfConfig;
|
||||
expect(kdfConfigFormGroup.controls.iterations.value).toBe(3);
|
||||
expect(kdfConfigFormGroup.controls.memory.value).toBe(64);
|
||||
expect(kdfConfigFormGroup.controls.parallelism.value).toBe(4);
|
||||
expect(component.kdfConfig).toEqual(mockArgon2Config);
|
||||
|
||||
// Assert validators
|
||||
expectArgon2Validation(
|
||||
kdfConfigFormGroup.controls.iterations,
|
||||
kdfConfigFormGroup.controls.memory,
|
||||
kdfConfigFormGroup.controls.parallelism,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, false],
|
||||
[false, true],
|
||||
])(
|
||||
"should show log out banner = %s when feature flag observable is %s",
|
||||
async (showLogOutBanner, forceUpgradeKdfFeatureFlag) => {
|
||||
// Arrange
|
||||
const mockPBKDF2Config = new PBKDF2KdfConfig(600_000);
|
||||
mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config);
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(forceUpgradeKdfFeatureFlag));
|
||||
|
||||
// Act
|
||||
fixture = TestBed.createComponent(ChangeKdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const calloutElement = fixture.debugElement.query((el) =>
|
||||
el.nativeElement.textContent?.includes("kdfSettingsChangeLogoutWarning"),
|
||||
);
|
||||
|
||||
if (showLogOutBanner) {
|
||||
expect(calloutElement).not.toBeNull();
|
||||
expect(calloutElement.nativeElement.textContent).toContain(
|
||||
"kdfSettingsChangeLogoutWarning-used-i18n",
|
||||
);
|
||||
} else {
|
||||
expect(calloutElement).toBeNull();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("KDF Type Switching", () => {
|
||||
describe("switching from PBKDF2 to Argon2id", () => {
|
||||
beforeEach(async () => {
|
||||
// Setup component with initial PBKDF2 configuration
|
||||
const mockPBKDF2Config = new PBKDF2KdfConfig(600_001);
|
||||
mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config);
|
||||
|
||||
fixture = TestBed.createComponent(ChangeKdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
});
|
||||
|
||||
it("should update form structure and default values when KDF type changes to Argon2id", () => {
|
||||
// Arrange
|
||||
const formGroup = component["formGroup"];
|
||||
|
||||
// Act - change KDF type to Argon2id
|
||||
formGroup.controls.kdf.setValue(KdfType.Argon2id);
|
||||
|
||||
// Assert form values update to Argon2id defaults
|
||||
expect(formGroup.controls.kdf.value).toBe(KdfType.Argon2id);
|
||||
const kdfConfigFormGroup = formGroup.controls.kdfConfig;
|
||||
expect(kdfConfigFormGroup.controls.iterations.value).toBe(3); // Argon2id default
|
||||
expect(kdfConfigFormGroup.controls.memory.value).toBe(64); // Argon2id default
|
||||
expect(kdfConfigFormGroup.controls.parallelism.value).toBe(4); // Argon2id default
|
||||
});
|
||||
|
||||
it("should update validators when KDF type changes to Argon2id", () => {
|
||||
// Arrange
|
||||
const formGroup = component["formGroup"];
|
||||
|
||||
// Act - change KDF type to Argon2id
|
||||
formGroup.controls.kdf.setValue(KdfType.Argon2id);
|
||||
|
||||
// Assert validators update to Argon2id validation rules
|
||||
const kdfConfigFormGroup = formGroup.controls.kdfConfig;
|
||||
expectArgon2Validation(
|
||||
kdfConfigFormGroup.controls.iterations,
|
||||
kdfConfigFormGroup.controls.memory,
|
||||
kdfConfigFormGroup.controls.parallelism,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("switching from Argon2id to PBKDF2", () => {
|
||||
beforeEach(async () => {
|
||||
// Setup component with initial Argon2id configuration
|
||||
const mockArgon2IdConfig = new Argon2KdfConfig(4, 65, 5);
|
||||
mockKdfConfigService.getKdfConfig.mockResolvedValue(mockArgon2IdConfig);
|
||||
|
||||
fixture = TestBed.createComponent(ChangeKdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
});
|
||||
|
||||
it("should update form structure and default values when KDF type changes to PBKDF2", () => {
|
||||
// Arrange
|
||||
const formGroup = component["formGroup"];
|
||||
|
||||
// Act - change KDF type back to PBKDF2
|
||||
formGroup.controls.kdf.setValue(KdfType.PBKDF2_SHA256);
|
||||
|
||||
// Assert form values update to PBKDF2 defaults
|
||||
expect(formGroup.controls.kdf.value).toBe(KdfType.PBKDF2_SHA256);
|
||||
const kdfConfigFormGroup = formGroup.controls.kdfConfig;
|
||||
expect(kdfConfigFormGroup.controls.iterations.value).toBe(600_000); // PBKDF2 default
|
||||
expect(kdfConfigFormGroup.controls.memory.value).toBeNull(); // PBKDF2 doesn't use memory
|
||||
expect(kdfConfigFormGroup.controls.parallelism.value).toBeNull(); // PBKDF2 doesn't use parallelism
|
||||
});
|
||||
|
||||
it("should update validators when KDF type changes to PBKDF2", () => {
|
||||
// Arrange
|
||||
const formGroup = component["formGroup"];
|
||||
|
||||
// Act - change KDF type back to PBKDF2
|
||||
formGroup.controls.kdf.setValue(KdfType.PBKDF2_SHA256);
|
||||
|
||||
// Assert validators update to PBKDF2 validation rules
|
||||
const kdfConfigFormGroup = formGroup.controls.kdfConfig;
|
||||
expectPBKDF2Validation(
|
||||
kdfConfigFormGroup.controls.iterations,
|
||||
kdfConfigFormGroup.controls.memory,
|
||||
kdfConfigFormGroup.controls.parallelism,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("openConfirmationModal", () => {
|
||||
describe("when form is valid", () => {
|
||||
it("should open confirmation modal with PBKDF2 config when form is submitted", async () => {
|
||||
// Arrange
|
||||
const mockPBKDF2Config = new PBKDF2KdfConfig(600_001);
|
||||
mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config);
|
||||
|
||||
fixture = TestBed.createComponent(ChangeKdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
|
||||
// Act
|
||||
await component.openConfirmationModal();
|
||||
|
||||
// Assert
|
||||
expect(mockDialogService.open).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
kdfConfig: mockPBKDF2Config,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should open confirmation modal with Argon2id config when form is submitted", async () => {
|
||||
// Arrange
|
||||
const mockArgon2Config = new Argon2KdfConfig(4, 65, 5);
|
||||
mockKdfConfigService.getKdfConfig.mockResolvedValue(mockArgon2Config);
|
||||
|
||||
fixture = TestBed.createComponent(ChangeKdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
|
||||
// Act
|
||||
await component.openConfirmationModal();
|
||||
|
||||
// Assert
|
||||
expect(mockDialogService.open).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
kdfConfig: mockArgon2Config,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not open modal when form is invalid", async () => {
|
||||
// Arrange
|
||||
const mockPBKDF2Config = new PBKDF2KdfConfig(PBKDF2KdfConfig.ITERATIONS.min - 1);
|
||||
mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config);
|
||||
|
||||
fixture = TestBed.createComponent(ChangeKdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
|
||||
// Act
|
||||
await component.openConfirmationModal();
|
||||
|
||||
// Assert
|
||||
expect(mockDialogService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ValidatorFn, Validators } from "@angular/forms";
|
||||
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
||||
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||
import { Subject, firstValueFrom, takeUntil, Observable } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
KdfConfigService,
|
||||
@@ -31,11 +31,11 @@ export class ChangeKdfComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
kdf: new FormControl(KdfType.PBKDF2_SHA256, [Validators.required]),
|
||||
kdf: new FormControl<KdfType>(KdfType.PBKDF2_SHA256, [Validators.required]),
|
||||
kdfConfig: this.formBuilder.group({
|
||||
iterations: [this.kdfConfig.iterations],
|
||||
memory: [null as number],
|
||||
parallelism: [null as number],
|
||||
iterations: new FormControl<number | null>(null),
|
||||
memory: new FormControl<number | null>(null),
|
||||
parallelism: new FormControl<number | null>(null),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -45,95 +45,102 @@ export class ChangeKdfComponent implements OnInit, OnDestroy {
|
||||
protected ARGON2_MEMORY = Argon2KdfConfig.MEMORY;
|
||||
protected ARGON2_PARALLELISM = Argon2KdfConfig.PARALLELISM;
|
||||
|
||||
noLogoutOnKdfChangeFeatureFlag$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private accountService: AccountService,
|
||||
private formBuilder: FormBuilder,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
this.kdfOptions = [
|
||||
{ name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 },
|
||||
{ name: "Argon2id", value: KdfType.Argon2id },
|
||||
];
|
||||
this.noLogoutOnKdfChangeFeatureFlag$ = configService.getFeatureFlag$(
|
||||
FeatureFlag.NoLogoutOnKdfChange,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
this.formGroup.get("kdf").setValue(this.kdfConfig.kdfType);
|
||||
this.formGroup.controls.kdf.setValue(this.kdfConfig.kdfType);
|
||||
this.setFormControlValues(this.kdfConfig);
|
||||
this.setFormValidators(this.kdfConfig.kdfType);
|
||||
|
||||
this.formGroup
|
||||
.get("kdf")
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
this.formGroup.controls.kdf.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((newValue) => {
|
||||
this.updateKdfConfig(newValue);
|
||||
this.updateKdfConfig(newValue!);
|
||||
});
|
||||
}
|
||||
private updateKdfConfig(newValue: KdfType) {
|
||||
let config: KdfConfig;
|
||||
const validators: { [key: string]: ValidatorFn[] } = {
|
||||
iterations: [],
|
||||
memory: [],
|
||||
parallelism: [],
|
||||
};
|
||||
|
||||
switch (newValue) {
|
||||
case KdfType.PBKDF2_SHA256:
|
||||
config = new PBKDF2KdfConfig();
|
||||
validators.iterations = [
|
||||
Validators.required,
|
||||
Validators.min(PBKDF2KdfConfig.ITERATIONS.min),
|
||||
Validators.max(PBKDF2KdfConfig.ITERATIONS.max),
|
||||
];
|
||||
break;
|
||||
case KdfType.Argon2id:
|
||||
config = new Argon2KdfConfig();
|
||||
validators.iterations = [
|
||||
Validators.required,
|
||||
Validators.min(Argon2KdfConfig.ITERATIONS.min),
|
||||
Validators.max(Argon2KdfConfig.ITERATIONS.max),
|
||||
];
|
||||
validators.memory = [
|
||||
Validators.required,
|
||||
Validators.min(Argon2KdfConfig.MEMORY.min),
|
||||
Validators.max(Argon2KdfConfig.MEMORY.max),
|
||||
];
|
||||
validators.parallelism = [
|
||||
Validators.required,
|
||||
Validators.min(Argon2KdfConfig.PARALLELISM.min),
|
||||
Validators.max(Argon2KdfConfig.PARALLELISM.max),
|
||||
];
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown KDF type.");
|
||||
}
|
||||
|
||||
this.kdfConfig = config;
|
||||
this.setFormValidators(validators);
|
||||
this.setFormValidators(newValue);
|
||||
this.setFormControlValues(this.kdfConfig);
|
||||
}
|
||||
|
||||
private setFormValidators(validators: { [key: string]: ValidatorFn[] }) {
|
||||
this.setValidators("kdfConfig.iterations", validators.iterations);
|
||||
this.setValidators("kdfConfig.memory", validators.memory);
|
||||
this.setValidators("kdfConfig.parallelism", validators.parallelism);
|
||||
}
|
||||
private setValidators(controlName: string, validators: ValidatorFn[]) {
|
||||
const control = this.formGroup.get(controlName);
|
||||
if (control) {
|
||||
control.setValidators(validators);
|
||||
control.updateValueAndValidity();
|
||||
private setFormValidators(kdfType: KdfType) {
|
||||
const kdfConfigFormGroup = this.formGroup.controls.kdfConfig;
|
||||
switch (kdfType) {
|
||||
case KdfType.PBKDF2_SHA256:
|
||||
kdfConfigFormGroup.controls.iterations.setValidators([
|
||||
Validators.required,
|
||||
Validators.min(PBKDF2KdfConfig.ITERATIONS.min),
|
||||
Validators.max(PBKDF2KdfConfig.ITERATIONS.max),
|
||||
]);
|
||||
kdfConfigFormGroup.controls.memory.setValidators([]);
|
||||
kdfConfigFormGroup.controls.parallelism.setValidators([]);
|
||||
break;
|
||||
case KdfType.Argon2id:
|
||||
kdfConfigFormGroup.controls.iterations.setValidators([
|
||||
Validators.required,
|
||||
Validators.min(Argon2KdfConfig.ITERATIONS.min),
|
||||
Validators.max(Argon2KdfConfig.ITERATIONS.max),
|
||||
]);
|
||||
kdfConfigFormGroup.controls.memory.setValidators([
|
||||
Validators.required,
|
||||
Validators.min(Argon2KdfConfig.MEMORY.min),
|
||||
Validators.max(Argon2KdfConfig.MEMORY.max),
|
||||
]);
|
||||
kdfConfigFormGroup.controls.parallelism.setValidators([
|
||||
Validators.required,
|
||||
Validators.min(Argon2KdfConfig.PARALLELISM.min),
|
||||
Validators.max(Argon2KdfConfig.PARALLELISM.max),
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown KDF type.");
|
||||
}
|
||||
kdfConfigFormGroup.controls.iterations.updateValueAndValidity();
|
||||
kdfConfigFormGroup.controls.memory.updateValueAndValidity();
|
||||
kdfConfigFormGroup.controls.parallelism.updateValueAndValidity();
|
||||
}
|
||||
|
||||
private setFormControlValues(kdfConfig: KdfConfig) {
|
||||
this.formGroup.get("kdfConfig").reset();
|
||||
const kdfConfigFormGroup = this.formGroup.controls.kdfConfig;
|
||||
kdfConfigFormGroup.reset();
|
||||
if (kdfConfig.kdfType === KdfType.PBKDF2_SHA256) {
|
||||
this.formGroup.get("kdfConfig.iterations").setValue(kdfConfig.iterations);
|
||||
kdfConfigFormGroup.controls.iterations.setValue(kdfConfig.iterations);
|
||||
} else if (kdfConfig.kdfType === KdfType.Argon2id) {
|
||||
this.formGroup.get("kdfConfig.iterations").setValue(kdfConfig.iterations);
|
||||
this.formGroup.get("kdfConfig.memory").setValue(kdfConfig.memory);
|
||||
this.formGroup.get("kdfConfig.parallelism").setValue(kdfConfig.parallelism);
|
||||
kdfConfigFormGroup.controls.iterations.setValue(kdfConfig.iterations);
|
||||
kdfConfigFormGroup.controls.memory.setValue(kdfConfig.memory);
|
||||
kdfConfigFormGroup.controls.parallelism.setValue(kdfConfig.parallelism);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,12 +162,14 @@ export class ChangeKdfComponent implements OnInit, OnDestroy {
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const kdfConfigFormGroup = this.formGroup.controls.kdfConfig;
|
||||
if (this.kdfConfig.kdfType === KdfType.PBKDF2_SHA256) {
|
||||
this.kdfConfig.iterations = this.formGroup.get("kdfConfig.iterations").value;
|
||||
this.kdfConfig.iterations = kdfConfigFormGroup.controls.iterations.value!;
|
||||
} else if (this.kdfConfig.kdfType === KdfType.Argon2id) {
|
||||
this.kdfConfig.iterations = this.formGroup.get("kdfConfig.iterations").value;
|
||||
this.kdfConfig.memory = this.formGroup.get("kdfConfig.memory").value;
|
||||
this.kdfConfig.parallelism = this.formGroup.get("kdfConfig.parallelism").value;
|
||||
this.kdfConfig.iterations = kdfConfigFormGroup.controls.iterations.value!;
|
||||
this.kdfConfig.memory = kdfConfigFormGroup.controls.memory.value!;
|
||||
this.kdfConfig.parallelism = kdfConfigFormGroup.controls.parallelism.value!;
|
||||
}
|
||||
this.dialogService.open(ChangeKdfConfirmationComponent, {
|
||||
data: {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { PopoverModule } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";
|
||||
import { ChangeKdfComponent } from "./change-kdf.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, SharedModule],
|
||||
imports: [CommonModule, SharedModule, PopoverModule],
|
||||
declarations: [ChangeKdfComponent, ChangeKdfConfirmationComponent],
|
||||
exports: [ChangeKdfComponent, ChangeKdfConfirmationComponent],
|
||||
})
|
||||
|
||||
@@ -1719,7 +1719,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"dontAskAgainOnThisDeviceFor30Days": {
|
||||
"message": "Don't ask again on this device for 30 days"
|
||||
},
|
||||
@@ -2090,9 +2089,6 @@
|
||||
"encKeySettings": {
|
||||
"message": "Encryption key settings"
|
||||
},
|
||||
"kdfAlgorithm": {
|
||||
"message": "KDF algorithm"
|
||||
},
|
||||
"kdfIterations": {
|
||||
"message": "KDF iterations"
|
||||
},
|
||||
@@ -2127,9 +2123,6 @@
|
||||
"argon2Desc": {
|
||||
"message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker."
|
||||
},
|
||||
"changeKdf": {
|
||||
"message": "Change KDF"
|
||||
},
|
||||
"encKeySettingsChanged": {
|
||||
"message": "Encryption key settings saved"
|
||||
},
|
||||
@@ -10374,27 +10367,9 @@
|
||||
"memberAccessReportAuthenticationEnabledFalse": {
|
||||
"message": "Off"
|
||||
},
|
||||
"higherKDFIterations": {
|
||||
"message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker."
|
||||
},
|
||||
"incrementsOf100,000": {
|
||||
"message": "increments of 100,000"
|
||||
},
|
||||
"smallIncrements": {
|
||||
"message": "small increments"
|
||||
},
|
||||
"kdfIterationRecommends": {
|
||||
"message": "We recommend 600,000 or more"
|
||||
},
|
||||
"kdfToHighWarningIncreaseInIncrements": {
|
||||
"message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1",
|
||||
"example": "increments of 100,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerReinstate": {
|
||||
"message": " Contact Customer Support to reinstate your subscription."
|
||||
},
|
||||
@@ -11878,5 +11853,32 @@
|
||||
},
|
||||
"viewbusinessplans": {
|
||||
"message": "View business plans"
|
||||
},
|
||||
"updateEncryptionSettings": {
|
||||
"message": "Update encryption settings"
|
||||
},
|
||||
"updateYourEncryptionSettings": {
|
||||
"message": "Update your encryption settings"
|
||||
},
|
||||
"updateSettings": {
|
||||
"message": "Update settings"
|
||||
},
|
||||
"algorithm": {
|
||||
"message": "Algorithm"
|
||||
},
|
||||
"encryptionKeySettingsHowShouldWeEncryptYourData": {
|
||||
"message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users."
|
||||
},
|
||||
"encryptionKeySettingsIncreaseImproveSecurity": {
|
||||
"message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result."
|
||||
},
|
||||
"encryptionKeySettingsAlgorithmPopoverTitle": {
|
||||
"message": "About encryption algorithms"
|
||||
},
|
||||
"encryptionKeySettingsAlgorithmPopoverPBKDF2": {
|
||||
"message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users."
|
||||
},
|
||||
"encryptionKeySettingsAlgorithmPopoverArgon2Id": {
|
||||
"message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user