diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html index 0627a886b1..4442310fac 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html @@ -1,117 +1,110 @@ -
-

{{ "encKeySettings" | i18n }}

-
+

{{ "encKeySettings" | i18n }}

{{ "kdfSettingsChangeLogoutWarning" | i18n }} -
-
-
-
- - - - - - - - - -
-
-
-
- - +

+ {{ "higherKDFIterations" | i18n }} +

+

+ {{ + "kdfToHighWarningIncreaseInIncrements" + | i18n: (isPBKDF2(kdfConfig) ? ("incrementsOf100,000" | i18n) : ("smallIncrements" | i18n)) + }} +

+ +
+
+ + {{ "kdfAlgorithm" | i18n }} + + + + + + + {{ "kdfMemory" | i18n }} + + +
+
+
+ + + {{ "kdfIterations" | i18n }} + + + + - + {{ "kdfIterationRecommends" | i18n }} + - - - - + + + {{ "kdfIterations" | i18n }} + + + + + + {{ "kdfParallelism" | i18n }} + + +
-
- -

- {{ "kdfIterationsDesc" | i18n: (PBKDF2_ITERATIONS.defaultValue | number) }} -

- - {{ "kdfIterationsWarning" | i18n: (100000 | number) }} - -
- -

{{ "argon2Desc" | i18n }}

- {{ "argon2Warning" | i18n }} -
-
diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts index 5c05f1ba2a..e9b35dd33d 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts @@ -1,4 +1,6 @@ import { Component, OnInit } from "@angular/core"; +import { FormBuilder, FormControl, ValidatorFn, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { @@ -24,8 +26,34 @@ import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.compon }) export class ChangeKdfComponent implements OnInit { kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG; - kdfType = KdfType; kdfOptions: any[] = []; + private destroy$ = new Subject(); + + protected formGroup = this.formBuilder.group({ + kdf: new FormControl(KdfType.PBKDF2_SHA256, [Validators.required]), + kdfConfig: this.formBuilder.group({ + iterations: [ + this.kdfConfig.iterations, + [ + Validators.required, + Validators.min(PBKDF2_ITERATIONS.min), + Validators.max(PBKDF2_ITERATIONS.max), + ], + ], + memory: [ + null as number, + [Validators.required, Validators.min(ARGON2_MEMORY.min), Validators.max(ARGON2_MEMORY.max)], + ], + parallelism: [ + null as number, + [ + Validators.required, + Validators.min(ARGON2_PARALLELISM.min), + Validators.max(ARGON2_PARALLELISM.max), + ], + ], + }), + }); // Default values for template protected PBKDF2_ITERATIONS = PBKDF2_ITERATIONS; @@ -36,6 +64,7 @@ export class ChangeKdfComponent implements OnInit { constructor( private dialogService: DialogService, private kdfConfigService: KdfConfigService, + private formBuilder: FormBuilder, ) { this.kdfOptions = [ { name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 }, @@ -45,6 +74,86 @@ export class ChangeKdfComponent implements OnInit { async ngOnInit() { this.kdfConfig = await this.kdfConfigService.getKdfConfig(); + this.formGroup.get("kdf").setValue(this.kdfConfig.kdfType, { emitEvent: false }); + this.setFormControlValues(this.kdfConfig); + + this.formGroup + .get("kdf") + .valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe((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(PBKDF2_ITERATIONS.min), + Validators.max(PBKDF2_ITERATIONS.max), + ]; + break; + case KdfType.Argon2id: + config = new Argon2KdfConfig(); + validators.iterations = [ + Validators.required, + Validators.min(ARGON2_ITERATIONS.min), + Validators.max(ARGON2_ITERATIONS.max), + ]; + validators.memory = [ + Validators.required, + Validators.min(ARGON2_MEMORY.min), + Validators.max(ARGON2_MEMORY.max), + ]; + validators.parallelism = [ + Validators.required, + Validators.min(ARGON2_PARALLELISM.min), + Validators.max(ARGON2_PARALLELISM.max), + ]; + break; + default: + throw new Error("Unknown KDF type."); + } + + this.kdfConfig = config; + this.setFormValidators(validators); + 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 setFormControlValues(kdfConfig: KdfConfig) { + this.formGroup.get("kdfConfig").reset(); + if (kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { + this.formGroup.get("kdfConfig.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); + } + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } isPBKDF2(t: KdfConfig): t is PBKDF2KdfConfig { @@ -55,17 +164,18 @@ export class ChangeKdfComponent implements OnInit { return t instanceof Argon2KdfConfig; } - async onChangeKdf(newValue: KdfType) { - if (newValue === KdfType.PBKDF2_SHA256) { - this.kdfConfig = new PBKDF2KdfConfig(); - } else if (newValue === KdfType.Argon2id) { - this.kdfConfig = new Argon2KdfConfig(); - } else { - throw new Error("Unknown KDF type."); - } - } - async openConfirmationModal() { + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + return; + } + if (this.kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { + this.kdfConfig.iterations = this.formGroup.get("kdfConfig.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.dialogService.open(ChangeKdfConfirmationComponent, { data: { kdfConfig: this.kdfConfig, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index bd8345ce81..18b45f4d45 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8405,5 +8405,26 @@ }, "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." + }, + "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" + } + } } }