1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00
Files
browser/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.ts
Matt Gibson 9c1e2ebd67 Typescript-strict-plugin (#12235)
* Use typescript-strict-plugin to iteratively turn on strict

* Add strict testing to pipeline

Can be executed locally through either `npm run test:types` for full type checking including spec files, or `npx tsc-strict` for only tsconfig.json included files.

* turn on strict for scripts directory

* Use plugin for all tsconfigs in monorepo

vscode is capable of executing tsc with plugins, but uses the most relevant tsconfig to do so. If the plugin is not a part of that config, it is skipped and developers get no feedback of strict compile time issues. These updates remedy that at the cost of slightly more complex removal of the plugin when the time comes.

* remove plugin from configs that extend one that already has it

* Update workspace settings to honor strict plugin

* Apply strict-plugin to native message test runner

* Update vscode workspace to use root tsc version

* `./node_modules/.bin/update-strict-comments` 🤖

This is a one-time operation. All future files should adhere to strict type checking.

* Add fixme to `ts-strict-ignore` comments

* `update-strict-comments` 🤖

repeated for new merge files
2024-12-09 20:58:50 +01:00

387 lines
12 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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<PasswordGeneratorPolicy, PasswordGenerationOptions>, number],
PassphraseGenerationOptions,
PassphraseGenerationOptions,
[PolicyEvaluator<PassphraseGeneratorPolicy, PassphraseGenerationOptions>, number],
GeneratorNavigation,
GeneratorNavigation,
[PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>, number],
];
function withSequenceNumber<T>(observable$: Observable<T>) {
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());
}