import { concatMap, zip, map, firstValueFrom, combineLatest, pairwise, of, concat, Observable, filter, timeout, } from "rxjs"; import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { GeneratorService, PassphraseGenerationOptions, PassphraseGeneratorPolicy, PasswordGenerationOptions, PasswordGeneratorPolicy, PolicyEvaluator, } from "@bitwarden/generator-core"; import { GeneratedCredential, GeneratorHistoryService, GeneratedPasswordHistory, } from "@bitwarden/generator-history"; import { GeneratorNavigationService, GeneratorNavigation, GeneratorNavigationPolicy, } from "@bitwarden/generator-navigation"; import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; import { PasswordGeneratorOptions } from "./password-generator-options"; type MappedOptions = { generator: GeneratorNavigation; password: PasswordGenerationOptions; passphrase: PassphraseGenerationOptions; policyUpdated: boolean; }; /** Adapts the generator 2.0 design to 1.0 angular services. */ export class LegacyPasswordGenerationService implements PasswordGenerationServiceAbstraction { constructor( private readonly accountService: AccountService, private readonly navigation: GeneratorNavigationService, private readonly passwords: GeneratorService< PasswordGenerationOptions, PasswordGeneratorPolicy >, private readonly passphrases: GeneratorService< PassphraseGenerationOptions, PassphraseGeneratorPolicy >, private readonly history: GeneratorHistoryService, ) {} generatePassword(options: PasswordGeneratorOptions) { if (options.type === "password") { return this.passwords.generate(options); } else { return this.passphrases.generate(options); } } generatePassphrase(options: PasswordGeneratorOptions) { return this.passphrases.generate(options); } private getRawOptions$() { // give the typechecker a nudge to avoid "implicit any" errors type RawOptionsIntermediateType = [ PasswordGenerationOptions, PasswordGenerationOptions, [PolicyEvaluator, number], PassphraseGenerationOptions, PassphraseGenerationOptions, [PolicyEvaluator, number], GeneratorNavigation, GeneratorNavigation, [PolicyEvaluator, number], ]; function withSequenceNumber(observable$: Observable) { return observable$.pipe(map((evaluator, i) => [evaluator, i] as const)); } // initial array ensures that destructuring never fails; sequence numbers // set to `-1` so that the first update reflects that the policy changed from // "unknown" to "whatever was provided by the service". This needs to be called // each time the active user changes or the `concat` will block. function initial$() { const initial: RawOptionsIntermediateType = [ null, null, [null, -1], null, null, [null, -1], null, null, [null, -1], ]; return of(initial); } function intermediatePairsToRawOptions([previous, current]: [ RawOptionsIntermediateType, RawOptionsIntermediateType, ]) { const [, , [, passwordPrevious], , , [, passphrasePrevious], , , [, generatorPrevious]] = previous; const [ passwordOptions, passwordDefaults, [passwordEvaluator, passwordCurrent], passphraseOptions, passphraseDefaults, [passphraseEvaluator, passphraseCurrent], generatorOptions, generatorDefaults, [generatorEvaluator, generatorCurrent], ] = current; // when any of the sequence numbers change, the emission occurs as the result of // a policy update const policyEmitted = passwordPrevious < passwordCurrent || passphrasePrevious < passphraseCurrent || generatorPrevious < generatorCurrent; const result = [ passwordOptions, passwordDefaults, passwordEvaluator, passphraseOptions, passphraseDefaults, passphraseEvaluator, generatorOptions, generatorDefaults, generatorEvaluator, policyEmitted, ] as const; return result; } // look upon my works, ye mighty, and despair! const rawOptions$ = this.accountService.activeAccount$.pipe( concatMap((activeUser) => concat( initial$(), combineLatest([ this.passwords.options$(activeUser.id), this.passwords.defaults$(activeUser.id), withSequenceNumber(this.passwords.evaluator$(activeUser.id)), this.passphrases.options$(activeUser.id), this.passphrases.defaults$(activeUser.id), withSequenceNumber(this.passphrases.evaluator$(activeUser.id)), this.navigation.options$(activeUser.id), this.navigation.defaults$(activeUser.id), withSequenceNumber(this.navigation.evaluator$(activeUser.id)), ]), ), ), pairwise(), map(intermediatePairsToRawOptions), ); return rawOptions$; } getOptions$() { const options$ = this.getRawOptions$().pipe( map( ([ passwordOptions, passwordDefaults, passwordEvaluator, passphraseOptions, passphraseDefaults, passphraseEvaluator, generatorOptions, generatorDefaults, generatorEvaluator, policyUpdated, ]) => { const passwordOptionsWithPolicy = passwordEvaluator.applyPolicy( passwordOptions ?? passwordDefaults, ); const passphraseOptionsWithPolicy = passphraseEvaluator.applyPolicy( passphraseOptions ?? passphraseDefaults, ); const generatorOptionsWithPolicy = generatorEvaluator.applyPolicy( generatorOptions ?? generatorDefaults, ); const options = this.toPasswordGeneratorOptions({ password: passwordEvaluator.sanitize(passwordOptionsWithPolicy), passphrase: passphraseEvaluator.sanitize(passphraseOptionsWithPolicy), generator: generatorEvaluator.sanitize(generatorOptionsWithPolicy), policyUpdated, }); const policy = Object.assign( new PasswordGeneratorPolicyOptions(), passwordEvaluator.policy, passphraseEvaluator.policy, generatorEvaluator.policy, ); return [options, policy] as [PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]; }, ), ); return options$; } async getOptions() { return await firstValueFrom(this.getOptions$()); } async enforcePasswordGeneratorPoliciesOnOptions(options: PasswordGeneratorOptions) { const options$ = this.accountService.activeAccount$.pipe( concatMap((activeUser) => zip( this.passwords.evaluator$(activeUser.id), this.passphrases.evaluator$(activeUser.id), this.navigation.evaluator$(activeUser.id), ), ), map(([passwordEvaluator, passphraseEvaluator, navigationEvaluator]) => { const policy = Object.assign( new PasswordGeneratorPolicyOptions(), passwordEvaluator.policy, passphraseEvaluator.policy, navigationEvaluator.policy, ); const navigationApplied = navigationEvaluator.applyPolicy(options); const navigationSanitized = { ...options, ...navigationEvaluator.sanitize(navigationApplied), }; if (navigationSanitized.type === "password") { const applied = passwordEvaluator.applyPolicy(navigationSanitized); const sanitized = passwordEvaluator.sanitize(applied); return [sanitized, policy]; } else { const applied = passphraseEvaluator.applyPolicy(navigationSanitized); const sanitized = passphraseEvaluator.sanitize(applied); return [sanitized, policy]; } }), ); const [sanitized, policy] = await firstValueFrom(options$); return [ // callers assume this function updates the options parameter Object.assign(options, sanitized), policy, ] as [PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]; } async saveOptions(options: PasswordGeneratorOptions) { const stored = this.toStoredOptions(options); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); // generator settings needs to preserve whether password or passphrase is selected, // so `navigationOptions` is mutated. const navigationOptions$ = zip( this.navigation.options$(activeAccount.id), this.navigation.defaults$(activeAccount.id), ).pipe(map(([options, defaults]) => options ?? defaults)); let navigationOptions = await firstValueFrom(navigationOptions$); navigationOptions = Object.assign(navigationOptions, stored.generator); await this.navigation.saveOptions(activeAccount.id, navigationOptions); // overwrite all other settings with latest values await this.passwords.saveOptions(activeAccount.id, stored.password); await this.passphrases.saveOptions(activeAccount.id, stored.passphrase); } private toStoredOptions(options: PasswordGeneratorOptions): MappedOptions { return { generator: { type: options.type, }, password: { length: options.length, minLength: options.minLength, ambiguous: options.ambiguous, uppercase: options.uppercase, minUppercase: options.minUppercase, lowercase: options.lowercase, minLowercase: options.minLowercase, number: options.number, minNumber: options.minNumber, special: options.special, minSpecial: options.minSpecial, }, passphrase: { numWords: options.numWords, wordSeparator: options.wordSeparator, capitalize: options.capitalize, includeNumber: options.includeNumber, }, policyUpdated: false, }; } private toPasswordGeneratorOptions(options: MappedOptions): PasswordGeneratorOptions { return { type: options.generator.type, length: options.password.length, minLength: options.password.minLength, ambiguous: options.password.ambiguous, uppercase: options.password.uppercase, minUppercase: options.password.minUppercase, lowercase: options.password.lowercase, minLowercase: options.password.minLowercase, number: options.password.number, minNumber: options.password.minNumber, special: options.password.special, minSpecial: options.password.minSpecial, numWords: options.passphrase.numWords, wordSeparator: options.passphrase.wordSeparator, capitalize: options.passphrase.capitalize, includeNumber: options.passphrase.includeNumber, policyUpdated: options.policyUpdated, }; } getHistory() { const history = this.accountService.activeAccount$.pipe( concatMap((account) => this.history.credentials$(account.id)), timeout({ // timeout after 1 second each: 1000, with() { return []; }, }), map((history) => history.map(toGeneratedPasswordHistory)), ); return firstValueFrom(history); } async addHistory(password: string) { const account = await firstValueFrom(this.accountService.activeAccount$); if (account?.id) { // legacy service doesn't distinguish credential types await this.history.track(account.id, password, "password"); } } clear() { const history$ = this.accountService.activeAccount$.pipe( filter((account) => !!account?.id), concatMap((account) => this.history.clear(account.id)), timeout({ // timeout after 1 second each: 1000, with() { return []; }, }), map((history) => history.map(toGeneratedPasswordHistory)), ); return firstValueFrom(history$); } } function toGeneratedPasswordHistory(value: GeneratedCredential) { return new GeneratedPasswordHistory(value.credential, value.generationDate.valueOf()); }