From 6a8361fafa745206546c6dc56cc03bc533ee81c3 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 5 Sep 2025 16:41:49 -0400 Subject: [PATCH] WIP --- .../input-password.component.ts | 47 ++++++++++-- .../tools/generator/core/src/metadata/data.ts | 3 +- .../src/metadata/password/random-password.ts | 75 +++++++++++++++++++ .../master-password-policy-reducer.ts | 19 +++++ .../providers/generator-profile-provider.ts | 9 ++- 5 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 libs/tools/generator/core/src/policies/master-password-policy-reducer.ts diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 32b02bf016a..bc715b69384 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -1,6 +1,15 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core"; +import { + Component, + DestroyRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ReactiveFormsModule, FormBuilder, Validators, FormControl } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { filter, firstValueFrom, Subject } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -10,6 +19,7 @@ import { import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -32,6 +42,13 @@ import { ToastService, Translation, } from "@bitwarden/components"; +import { GeneratorServicesModule } from "@bitwarden/generator-components"; +import { + CredentialGeneratorService, + GenerateRequest, + Profile, + Type, +} from "@bitwarden/generator-core"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { DEFAULT_KDF_CONFIG, @@ -107,6 +124,7 @@ interface InputPasswordForm { ButtonModule, CheckboxModule, FormFieldModule, + GeneratorServicesModule, IconButtonModule, InputModule, JslibModule, @@ -149,6 +167,9 @@ export class InputPasswordComponent implements OnInit { protected showErrorSummary = false; protected showPassword = false; + private generate$: Subject = new Subject(); + private account$ = this.accountService.activeAccount$.pipe(filter((acc) => acc != null)); + protected formGroup = this.formBuilder.nonNullable.group( { newPassword: this.formBuilder.nonNullable.control("", [ @@ -194,11 +215,22 @@ export class InputPasswordComponent implements OnInit { private policyService: PolicyService, private toastService: ToastService, private validationService: ValidationService, + private credentialGeneratorService: CredentialGeneratorService, + private accountService: AccountService, + private destroy$: DestroyRef, ) {} ngOnInit(): void { this.addFormFieldsIfNecessary(); this.setButtonText(); + this.credentialGeneratorService + .generate$({ on$: this.generate$, account$: this.account$ }) + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe((generated) => { + this.formGroup.patchValue({ + newPassword: generated.credential, + }); + }); } private addFormFieldsIfNecessary() { @@ -634,10 +666,13 @@ export class InputPasswordComponent implements OnInit { } protected async generatePassword() { - const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; - this.formGroup.patchValue({ - newPassword: await this.passwordGenerationService.generatePassword(options), - }); + const request: GenerateRequest = { + type: Type.password, + profile: Profile.masterPassword, + source: "master-password", + }; + + this.generate$.next(request); if (!this.passwordStrengthComponent) { throw new Error("PasswordStrengthComponent is not initialized"); diff --git a/libs/tools/generator/core/src/metadata/data.ts b/libs/tools/generator/core/src/metadata/data.ts index 2b9dad50557..6b244259052 100644 --- a/libs/tools/generator/core/src/metadata/data.ts +++ b/libs/tools/generator/core/src/metadata/data.ts @@ -31,8 +31,7 @@ export const Profile = Object.freeze({ * @remarks these are the options displayed on the generator tab */ account: "account", - - // FIXME: consider adding a profile for bitwarden's master password + masterPassword: "masterPassword", }); /** Credential generation algorithms grouped by purpose. */ diff --git a/libs/tools/generator/core/src/metadata/password/random-password.ts b/libs/tools/generator/core/src/metadata/password/random-password.ts index 721be8dc3f0..0da6c4aa484 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.ts @@ -5,6 +5,7 @@ import { deepFreeze } from "@bitwarden/common/tools/util"; import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies"; +import { masterPasswordReducer } from "../../policies/master-password-policy-reducer"; import { GeneratorDependencyProvider } from "../../providers"; import { CredentialGenerator, PasswordGeneratorSettings } from "../../types"; import { Algorithm, Profile, Type } from "../data"; @@ -111,6 +112,80 @@ const password: GeneratorMetadata = deepFreeze({ }, }, }, + [Profile.masterPassword]: { + type: "core", + storage: { + key: "passwordGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "length", + "ambiguous", + "uppercase", + "minUppercase", + "lowercase", + "minLowercase", + "number", + "minNumber", + "special", + "minSpecial", + ]), + state: GENERATOR_DISK, + initial: { + length: 14, + ambiguous: true, + uppercase: true, + minUppercase: 1, + lowercase: true, + minLowercase: 1, + number: true, + minNumber: 1, + special: false, + minSpecial: 0, + }, + options: { + deserializer(value) { + return value; + }, + clearOn: ["logout"], + }, + }, + constraints: { + type: PolicyType.MasterPassword, + default: { + length: { + min: 5, + max: 128, + recommendation: 14, + }, + minNumber: { + min: 0, + max: 9, + }, + minSpecial: { + min: 0, + max: 9, + }, + }, + create(policies, context) { + const initial = { + minLength: 0, + useUppercase: false, + useLowercase: true, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, + }; + const policy = policies.reduce(masterPasswordReducer, initial); + const constraints = new DynamicPasswordPolicyConstraints( + policy, + context.defaultConstraints, + ); + return constraints; + }, + }, + }, }, }); diff --git a/libs/tools/generator/core/src/policies/master-password-policy-reducer.ts b/libs/tools/generator/core/src/policies/master-password-policy-reducer.ts new file mode 100644 index 00000000000..357c8cc5c9d --- /dev/null +++ b/libs/tools/generator/core/src/policies/master-password-policy-reducer.ts @@ -0,0 +1,19 @@ +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; + +import { PasswordGeneratorPolicy } from "../types"; + +export function masterPasswordReducer(acc: PasswordGeneratorPolicy, policy: Policy) { + if (policy.type !== PolicyType.MasterPassword || !policy.enabled) { + return acc; + } + return { + minLength: Math.max(acc.minLength, policy.data.minLength ?? acc.minLength), + useUppercase: policy.data.requireUpper || acc.useUppercase, + useLowercase: policy.data.requireLower || acc.useLowercase, + useNumbers: policy.data.requireNumbers || acc.useNumbers, + numberCount: acc.numberCount, + useSpecial: policy.data.requireSpecial || acc.useSpecial, + specialCount: acc.specialCount, + }; +} diff --git a/libs/tools/generator/core/src/providers/generator-profile-provider.ts b/libs/tools/generator/core/src/providers/generator-profile-provider.ts index 4117d1f2a78..6e9ee337110 100644 --- a/libs/tools/generator/core/src/providers/generator-profile-provider.ts +++ b/libs/tools/generator/core/src/providers/generator-profile-provider.ts @@ -86,8 +86,15 @@ export class GeneratorProfileProvider { "initializing constraints$", ); + // The problem is here, this just gets applicable policies to the current user, which the owner/admin may be exempt from if generating a pw for another user via account recovery. + //const policies$ = profile.constraints.type + // ? this.policyService.policiesByType$(profile.constraints.type, account.id) + // : of([]); + const policies$ = profile.constraints.type - ? this.policyService.policiesByType$(profile.constraints.type, account.id) + ? this.policyService + .policies$(account.id) + .pipe(map((policies) => policies.filter((p) => p.type === profile.constraints.type))) : of([]); const context: ProfileContext = {