mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00:00
* 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
387 lines
12 KiB
TypeScript
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());
|
|
}
|