1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +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:
Maciej Zieniuk
2025-10-22 20:29:36 +02:00
committed by GitHub
parent 91be36bfcf
commit 8154613462
8 changed files with 848 additions and 199 deletions

View File

@@ -1,12 +1,14 @@
<form [formGroup]="form" [bitSubmit]="submit" autocomplete="off">
<bit-dialog>
<span bitDialogTitle>
{{ "changeKdf" | i18n }}
{{ "updateYourEncryptionSettings" | i18n }}
</span>
<span bitDialogContent>
<bit-callout type="warning">{{ "kdfSettingsChangeLogoutWarning" | i18n }}</bit-callout>
<bit-form-field>
@if (!(noLogoutOnKdfChangeFeatureFlag$ | async)) {
<bit-callout type="warning">{{ "kdfSettingsChangeLogoutWarning" | i18n }}</bit-callout>
}
<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 }}

View File

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

View File

@@ -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();
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("encKeySettingsChanged"),
message: this.i18nService.t("logBackIn"),
});
this.messagingService.send("logout");
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,

View File

@@ -1,31 +1,30 @@
<h2 bitTypography="h2">{{ "encKeySettings" | i18n }}</h2>
<bit-callout type="warning">{{ "kdfSettingsChangeLogoutWarning" | i18n }}</bit-callout>
<p bitTypography="body1">
{{ "higherKDFIterations" | i18n }}
<h2 bitTypography="h2" class="tw-mt-6">
{{ "encKeySettings" | i18n }}
</h2>
@if (!(noLogoutOnKdfChangeFeatureFlag$ | async)) {
<bit-callout type="warning">{{ "kdfSettingsChangeLogoutWarning" | i18n }}</bit-callout>
}
<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,34 +50,49 @@
/>
<bit-hint>{{ "kdfIterationRecommends" | i18n }}</bit-hint>
</bit-form-field>
<ng-container *ngIf="isArgon2(kdfConfig)">
<bit-form-field formGroupName="kdfConfig">
<bit-label>
{{ "kdfIterations" | i18n }}
</bit-label>
<input
bitInput
type="number"
formControlName="iterations"
[min]="ARGON2_ITERATIONS.min"
[max]="ARGON2_ITERATIONS.max"
/>
</bit-form-field>
<bit-form-field formGroupName="kdfConfig">
<bit-label>
{{ "kdfParallelism" | i18n }}
</bit-label>
<input
bitInput
type="number"
formControlName="parallelism"
[min]="ARGON2_PARALLELISM.min"
[max]="ARGON2_PARALLELISM.max"
/>
</bit-form-field>
</ng-container>
</div>
} @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 }}
</bit-label>
<input
bitInput
type="number"
formControlName="iterations"
[min]="ARGON2_ITERATIONS.min"
[max]="ARGON2_ITERATIONS.max"
/>
</bit-form-field>
</div>
<div class="tw-col-span-6">
<bit-form-field formGroupName="kdfConfig">
<bit-label>
{{ "kdfParallelism" | i18n }}
</bit-label>
<input
bitInput
type="number"
formControlName="parallelism"
[min]="ARGON2_PARALLELISM.min"
[max]="ARGON2_PARALLELISM.max"
/>
</bit-form-field>
</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>

View File

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

View File

@@ -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: {

View File

@@ -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],
})

View File

@@ -11,7 +11,7 @@
"criticalApplications": {
"message": "Critical applications"
},
"noCriticalAppsAtRisk":{
"noCriticalAppsAtRisk": {
"message": "No critical applications at risk"
},
"accessIntelligence": {
@@ -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"
},
@@ -2146,22 +2139,22 @@
"message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour."
},
"newDeviceLoginProtection": {
"message":"New device login"
"message": "New device login"
},
"turnOffNewDeviceLoginProtection": {
"message":"Turn off new device login protection"
"message": "Turn off new device login protection"
},
"turnOnNewDeviceLoginProtection": {
"message":"Turn on new device login protection"
"message": "Turn on new device login protection"
},
"turnOffNewDeviceLoginProtectionModalDesc": {
"message":"Proceed below to turn off the verification emails bitwarden sends when you login from a new device."
"message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device."
},
"turnOnNewDeviceLoginProtectionModalDesc": {
"message":"Proceed below to have bitwarden send you verification emails when you login from a new device."
"message": "Proceed below to have bitwarden send you verification emails when you login from a new device."
},
"turnOffNewDeviceLoginProtectionWarning": {
"message":"With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login."
"message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login."
},
"accountNewDeviceLoginProtectionSaved": {
"message": "New device login protection changes saved"
@@ -2297,7 +2290,7 @@
"selectImportCollection": {
"message": "Select a collection"
},
"importTargetHintCollection": {
"importTargetHintCollection": {
"message": "Select this option if you want the imported file contents moved to a collection"
},
"importTargetHintFolder": {
@@ -5700,7 +5693,7 @@
"message": "All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the ",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the credential lifecycle.'"
},
"organizationDataOwnershipContentAnchor":{
"organizationDataOwnershipContentAnchor": {
"message": "credential lifecycle",
"description": "This will be used as a hyperlink"
},
@@ -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."
},
@@ -11079,7 +11054,7 @@
"orgTrustWarning1": {
"message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint."
},
"trustUser":{
"trustUser": {
"message": "Trust user"
},
"sshKeyWrongPassword": {
@@ -11115,7 +11090,7 @@
"openingExtension": {
"message": "Opening the Bitwarden browser extension"
},
"somethingWentWrong":{
"somethingWentWrong": {
"message": "Something went wrong..."
},
"openingExtensionError": {
@@ -11202,7 +11177,7 @@
}
}
},
"accountDeprovisioningNotification" : {
"accountDeprovisioningNotification": {
"message": "Administrators now have the ability to delete member accounts that belong to a claimed domain."
},
"deleteManagedUserWarningDesc": {
@@ -11293,14 +11268,14 @@
"upgradeForFullEventsMessage": {
"message": "Event logs are not stored for your organization. Upgrade to a Teams or Enterprise plan to get full access to organization event logs."
},
"upgradeEventLogTitleMessage" : {
"message" : "Upgrade to see event logs from your organization."
"upgradeEventLogTitleMessage": {
"message": "Upgrade to see event logs from your organization."
},
"upgradeEventLogMessage":{
"message" : "These events are examples only and do not reflect real events within your Bitwarden organization."
"upgradeEventLogMessage": {
"message": "These events are examples only and do not reflect real events within your Bitwarden organization."
},
"viewEvents":{
"message" : "View Events"
"viewEvents": {
"message": "View Events"
},
"cannotCreateCollection": {
"message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections."
@@ -11619,14 +11594,14 @@
}
}
},
"unlimitedSecretsAndProjects": {
"unlimitedSecretsAndProjects": {
"message": "Unlimited secrets and projects"
},
"providersubscriptionCanceled": {
"providersubscriptionCanceled": {
"message": "Subscription canceled"
},
"providersubCanceledmessage": {
"message" : "To resubscribe, contact Bitwarden Customer Support."
"message": "To resubscribe, contact Bitwarden Customer Support."
},
"showMore": {
"message": "Show more"
@@ -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."
}
}