1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00

replace credential generator & eliminate dead code; breaks tests

This commit is contained in:
✨ Audrey ✨
2025-03-26 17:16:55 -04:00
parent 87cd180b29
commit c5e9340964
95 changed files with 1528 additions and 3015 deletions

View File

@@ -7,7 +7,7 @@ import { BehaviorSubject, map } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Generators } from "@bitwarden/generator-core";
import { BuiltIn, Profile } from "@bitwarden/generator-core";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -25,14 +25,22 @@ export class PasswordGeneratorPolicy extends BasePolicy {
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
// these properties forward the application default settings to the UI
// for HTML attribute bindings
protected readonly minLengthMin = Generators.password.settings.constraints.length.min;
protected readonly minLengthMax = Generators.password.settings.constraints.length.max;
protected readonly minNumbersMin = Generators.password.settings.constraints.minNumber.min;
protected readonly minNumbersMax = Generators.password.settings.constraints.minNumber.max;
protected readonly minSpecialMin = Generators.password.settings.constraints.minSpecial.min;
protected readonly minSpecialMax = Generators.password.settings.constraints.minSpecial.max;
protected readonly minNumberWordsMin = Generators.passphrase.settings.constraints.numWords.min;
protected readonly minNumberWordsMax = Generators.passphrase.settings.constraints.numWords.max;
protected readonly minLengthMin =
BuiltIn.password.profiles[Profile.account].constraints.default.length.min;
protected readonly minLengthMax =
BuiltIn.password.profiles[Profile.account].constraints.default.length.max;
protected readonly minNumbersMin =
BuiltIn.password.profiles[Profile.account].constraints.default.minNumber.min;
protected readonly minNumbersMax =
BuiltIn.password.profiles[Profile.account].constraints.default.minNumber.max;
protected readonly minSpecialMin =
BuiltIn.password.profiles[Profile.account].constraints.default.minSpecial.min;
protected readonly minSpecialMax =
BuiltIn.password.profiles[Profile.account].constraints.default.minSpecial.max;
protected readonly minNumberWordsMin =
BuiltIn.passphrase.profiles[Profile.account].constraints.default.numWords.min;
protected readonly minNumberWordsMax =
BuiltIn.passphrase.profiles[Profile.account].constraints.default.numWords.max;
data = this.formBuilder.group({
overridePasswordType: [null],

View File

@@ -17,7 +17,7 @@ export class PrivateClassifier<Data> implements Classifier<Data, Record<string,
}
const secret = picked as Jsonify<Data>;
return { disclosed: {}, secret };
return { disclosed: null, secret };
}
declassify(_disclosed: Jsonify<Record<keyof Data, never>>, secret: Jsonify<Data>) {

View File

@@ -16,7 +16,7 @@ export class PublicClassifier<Data> implements Classifier<Data, Data, Record<str
}
const disclosed = picked as Jsonify<Data>;
return { disclosed, secret: "" };
return { disclosed, secret: null };
}
declassify(disclosed: Jsonify<Data>, _secret: Jsonify<Record<keyof Data, never>>) {

View File

@@ -2,6 +2,11 @@ import { Simplify } from "type-fest";
import { IntegrationId } from "./integration";
/** When this is a string, it contains the i18n key. When it is an object, the `literal` member
* contains text that should not be translated.
*/
export type I18nKeyOrLiteral = string | { literal: string };
/** Constraints that are shared by all primitive field types */
type PrimitiveConstraint = {
/** `true` indicates the field is required; otherwise the field is optional */

View File

@@ -1,3 +1,5 @@
import { I18nKeyOrLiteral } from "./types";
/** Recursively freeze an object's own keys
* @param value the value to freeze
* @returns `value`
@@ -17,3 +19,11 @@ export function deepFreeze<T extends object>(value: T): Readonly<T> {
return Object.freeze(value);
}
export function isI18nKey(value: I18nKeyOrLiteral): value is string {
return typeof value === "string";
}
export function isLiteral(value: I18nKeyOrLiteral): value is string {
return typeof value === "object" && "literal" in value;
}

View File

@@ -58,7 +58,7 @@ import {
ToastService,
} from "@bitwarden/components";
import { GeneratorServicesModule } from "@bitwarden/generator-components";
import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core";
import { CredentialGeneratorService, GenerateRequest, Type } from "@bitwarden/generator-core";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { EncryptedExportType } from "../enums/encrypted-export-type.enum";
@@ -234,7 +234,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
}),
);
this.generatorService
.generate$(Generators.password, { on$: this.onGenerate$, account$ })
.generate$({ on$: this.onGenerate$, account$ })
.pipe(takeUntil(this.destroy$))
.subscribe((generated) => {
this.exportForm.patchValue({
@@ -360,7 +360,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
}
generatePassword = async () => {
this.onGenerate$.next({ source: "export" });
this.onGenerate$.next({ source: "export", type: Type.password });
};
submit = async () => {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
Component,
EventEmitter,
@@ -17,7 +15,7 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import {
CatchallGenerationOptions,
CredentialGeneratorService,
Generators,
BuiltIn,
} from "@bitwarden/generator-core";
/** Options group for catchall emails */
@@ -27,7 +25,6 @@ import {
})
export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
/** Instantiates the component
* @param accountService queries user availability
* @param generatorService settings and policy logic
* @param formBuilder reactive form controls
*/
@@ -36,24 +33,26 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
private generatorService: CredentialGeneratorService,
) {}
/** Binds the component to a specific user's settings.
/** Binds the component to a specific user's settings.\
* @remarks this is initialized to null but since it's a required input it'll
* never have that value in practice.
*/
@Input({ required: true })
account: Account;
account: Account = null!;
private account$ = new ReplaySubject<Account>(1);
/** Emits settings updates and completes if the settings become unavailable.
* @remarks this does not emit the initial settings. If you would like
* to receive live settings updates including the initial update,
* use `CredentialGeneratorService.settings$(...)` instead.
* use `CredentialGeneratorService.settings(...)` instead.
*/
@Output()
readonly onUpdated = new EventEmitter<CatchallGenerationOptions>();
/** The template's control bindings */
protected settings = this.formBuilder.group({
catchallDomain: [Generators.catchall.settings.initial.catchallDomain],
catchallDomain: [""],
});
async ngOnChanges(changes: SimpleChanges) {
@@ -63,7 +62,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
}
async ngOnInit() {
const settings = await this.generatorService.settings(Generators.catchall, {
const settings = await this.generatorService.settings(BuiltIn.catchall, {
account$: this.account$,
});
@@ -78,7 +77,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
this.saveSettings
.pipe(
withLatestFrom(this.settings.valueChanges),
map(([, settings]) => settings),
map(([, settings]) => settings as CatchallGenerationOptions),
takeUntil(this.destroyed$),
)
.subscribe(settings);

View File

@@ -6,6 +6,7 @@ import { BehaviorSubject, ReplaySubject, Subject, map, switchMap, takeUntil, tap
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
SemanticLogger,
@@ -19,10 +20,11 @@ import {
ItemModule,
NoItemsModule,
} from "@bitwarden/components";
import { CredentialGeneratorService } from "@bitwarden/generator-core";
import { AlgorithmsByType, CredentialGeneratorService } from "@bitwarden/generator-core";
import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generator-history";
import { GeneratorModule } from "./generator.module";
import { translate } from "./util";
@Component({
standalone: true,
@@ -45,6 +47,7 @@ export class CredentialGeneratorHistoryComponent implements OnChanges, OnInit, O
constructor(
private generatorService: CredentialGeneratorService,
private history: GeneratorHistoryService,
private i18nService: I18nService,
private logService: LogService,
) {}
@@ -94,13 +97,19 @@ export class CredentialGeneratorHistoryComponent implements OnChanges, OnInit, O
}
protected getCopyText(credential: GeneratedCredential) {
const info = this.generatorService.algorithm(credential.category);
return info.copy;
// there isn't a way way to look up category metadata so
// bodge it by looking up algorithm metadata
const [id] = AlgorithmsByType[credential.category];
const info = this.generatorService.algorithm(id);
return translate(info.i18nKeys.copyCredential, this.i18nService);
}
protected getGeneratedValueText(credential: GeneratedCredential) {
const info = this.generatorService.algorithm(credential.category);
return info.credentialType;
// there isn't a way way to look up category metadata so
// bodge it by looking up algorithm metadata
const [id] = AlgorithmsByType[credential.category];
const info = this.generatorService.algorithm(id);
return translate(info.i18nKeys.credentialType, this.i18nService);
}
ngOnDestroy() {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LiveAnnouncer } from "@angular/cdk/a11y";
import {
Component,
@@ -24,7 +22,6 @@ import {
map,
ReplaySubject,
Subject,
switchMap,
takeUntil,
withLatestFrom,
} from "rxjs";
@@ -32,7 +29,7 @@ import {
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { VendorId } from "@bitwarden/common/tools/extension";
import {
SemanticLogger,
disabledSemanticLoggerProvider,
@@ -41,23 +38,22 @@ import {
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService, Option } from "@bitwarden/components";
import {
AlgorithmInfo,
CredentialAlgorithm,
CredentialCategory,
CredentialType,
CredentialGeneratorService,
GenerateRequest,
GeneratedCredential,
Generators,
getForwarderConfiguration,
isEmailAlgorithm,
isForwarderIntegration,
isPasswordAlgorithm,
isForwarderExtensionId,
isSameAlgorithm,
isEmailAlgorithm,
isUsernameAlgorithm,
toCredentialGeneratorConfiguration,
isPasswordAlgorithm,
CredentialAlgorithm,
AlgorithmMetadata,
} from "@bitwarden/generator-core";
import { GeneratorHistoryService } from "@bitwarden/generator-history";
import { translate } from "./util";
// constants used to identify navigation selections that are not
// generator algorithms
const IDENTIFIER = "identifier";
@@ -87,7 +83,7 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
* the form binds to the active user
*/
@Input()
account: Account | null;
account: Account | null = null;
/** Send structured debug logs from the credential generator component
* to the debugger console.
@@ -126,7 +122,7 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
@Output()
readonly onGenerated = new EventEmitter<GeneratedCredential>();
protected root$ = new BehaviorSubject<{ nav: string }>({
protected root$ = new BehaviorSubject<{ nav: string | null }>({
nav: null,
});
@@ -140,11 +136,11 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
}
protected username = this.formBuilder.group({
nav: [null as string],
nav: [null as string | null],
});
protected forwarder = this.formBuilder.group({
nav: [null as string],
nav: [null as string | null],
});
async ngOnInit() {
@@ -153,23 +149,27 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
});
if (!this.account) {
this.account = await firstValueFrom(this.accountService.activeAccount$);
this.log.info(
{ userId: this.account.id },
"account not specified; using active account settings",
);
this.account$.next(this.account);
const account = await firstValueFrom(this.accountService.activeAccount$);
if (!account) {
this.log.panic("active account cannot be `null`.");
}
this.log.info({ userId: account.id }, "account not specified; using active account settings");
this.account$.next(account);
}
this.generatorService
.algorithms$(["email", "username"], { account$: this.account$ })
combineLatest([
this.generatorService.algorithms$("email", { account$: this.account$ }),
this.generatorService.algorithms$("username", { account$: this.account$ }),
])
.pipe(
map((algorithms) => algorithms.flat()),
map((algorithms) => {
const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id));
const usernames = algorithms.filter((a) => !isForwarderExtensionId(a.id));
const usernameOptions = this.toOptions(usernames);
usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwardedEmail") });
const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id));
const forwarders = algorithms.filter((a) => isForwarderExtensionId(a.id));
const forwarderOptions = this.toOptions(forwarders);
forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") });
@@ -194,9 +194,15 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
)
.subscribe(this.rootOptions$);
this.algorithm$
this.maybeAlgorithm$
.pipe(
map((a) => a?.description),
map((a) => {
if (a && a.i18nKeys.description) {
return translate(a.i18nKeys.description, this.i18nService);
} else {
return "";
}
}),
takeUntil(this.destroyed),
)
.subscribe((hint) => {
@@ -207,9 +213,9 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
});
});
this.algorithm$
this.maybeAlgorithm$
.pipe(
map((a) => a?.category),
map((a) => a?.type),
distinctUntilChanged(),
takeUntil(this.destroyed),
)
@@ -222,10 +228,12 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
});
// wire up the generator
this.algorithm$
this.generatorService
.generate$({
on$: this.generate$,
account$: this.account$,
})
.pipe(
filter((algorithm) => !!algorithm),
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
catchError((error: unknown, generator) => {
if (typeof error === "string") {
this.toastService.showToast({
@@ -240,11 +248,14 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
// continue with origin stream
return generator;
}),
withLatestFrom(this.account$, this.algorithm$),
withLatestFrom(this.account$, this.maybeAlgorithm$),
takeUntil(this.destroyed),
)
.subscribe(([generated, account, algorithm]) => {
this.log.debug({ source: generated.source }, "credential generated");
this.log.debug(
{ source: generated.source ?? null, algorithm: algorithm?.id ?? null },
"credential generated",
);
this.generatorHistoryService
.track(account.id, generated.credential, generated.category, generated.generationDate)
@@ -255,8 +266,8 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
if (generated.source === this.USER_REQUEST) {
this.announce(algorithm.onGeneratedMessage);
if (algorithm && generated.source === this.USER_REQUEST) {
this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService));
}
this.generatedCredential$.next(generated);
@@ -273,36 +284,45 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
this.root$
.pipe(
map(
(root): CascadeValue =>
root.nav === IDENTIFIER
? { nav: root.nav }
: { nav: root.nav, algorithm: JSON.parse(root.nav) },
),
map((root): CascadeValue => {
if (root.nav === FORWARDER) {
return { nav: root.nav };
} else if (root.nav) {
return { nav: root.nav, algorithm: JSON.parse(root.nav) };
} else {
this.log.panic(root, "unknown navigation value.");
}
}),
takeUntil(this.destroyed),
)
.subscribe(activeRoot$);
this.username.valueChanges
.pipe(
map(
(username): CascadeValue =>
username.nav === FORWARDER
? { nav: username.nav }
: { nav: username.nav, algorithm: JSON.parse(username.nav) },
),
map((username): CascadeValue => {
if (username.nav === FORWARDER) {
return { nav: username.nav };
} else if (username.nav) {
return { nav: username.nav, algorithm: JSON.parse(username.nav) };
} else {
this.log.panic(username, "unknown navigation value.");
}
}),
takeUntil(this.destroyed),
)
.subscribe(activeIdentifier$);
this.forwarder.valueChanges
.pipe(
map(
(forwarder): CascadeValue =>
forwarder.nav === NONE_SELECTED
? { nav: forwarder.nav }
: { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) },
),
map((forwarder): CascadeValue => {
if (forwarder.nav === NONE_SELECTED) {
return { nav: forwarder.nav };
} else if (forwarder.nav) {
return { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) };
} else {
this.log.panic(forwarder, "unknown navigation value.");
}
}),
takeUntil(this.destroyed),
)
.subscribe(activeForwarder$);
@@ -313,7 +333,7 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
map(([root, username, forwarder]) => {
const showForwarder = !root.algorithm && !username.algorithm;
const forwarderId =
showForwarder && isForwarderIntegration(forwarder.algorithm)
showForwarder && forwarder.algorithm && isForwarderExtensionId(forwarder.algorithm)
? forwarder.algorithm.forwarder
: null;
return [showForwarder, forwarderId] as const;
@@ -343,47 +363,51 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
return null;
}
}),
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
distinctUntilChanged((prev, next) => {
if (prev === null || next === null) {
return false;
} else {
return isSameAlgorithm(prev.id, next.id);
}
}),
takeUntil(this.destroyed),
)
.subscribe((algorithm) => {
this.log.debug(algorithm, "algorithm selected");
this.log.debug({ algorithm: algorithm?.id ?? null }, "algorithm selected");
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
this.algorithm$.next(algorithm);
this.maybeAlgorithm$.next(algorithm);
});
});
// assume the last-selected generator algorithm is the user's preferred one
const preferences = await this.generatorService.preferences({ account$: this.account$ });
this.algorithm$
.pipe(
filter((algorithm) => !!algorithm),
withLatestFrom(preferences),
takeUntil(this.destroyed),
)
.pipe(withLatestFrom(preferences), takeUntil(this.destroyed))
.subscribe(([algorithm, preference]) => {
function setPreference(category: CredentialCategory, log: SemanticLogger) {
const p = preference[category];
function setPreference(type: CredentialType) {
const p = preference[type];
p.algorithm = algorithm.id;
p.updated = new Date();
log.info({ algorithm, category }, "algorithm preferences updated");
}
// `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference`
if (isEmailAlgorithm(algorithm.id)) {
setPreference("email", this.log);
setPreference("email");
} else if (isUsernameAlgorithm(algorithm.id)) {
setPreference("username", this.log);
setPreference("username");
} else if (isPasswordAlgorithm(algorithm.id)) {
setPreference("password", this.log);
setPreference("password");
} else {
return;
}
this.log.info(
{ algorithm: algorithm.id, type: algorithm.type },
"algorithm preferences updated",
);
preferences.next(preference);
});
@@ -391,7 +415,7 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
preferences
.pipe(
map(({ email, username, password }) => {
const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null;
const forwarderPref = isForwarderExtensionId(email.algorithm) ? email : null;
const usernamePref = email.updated > username.updated ? email : username;
// inject drilldown flags
@@ -410,14 +434,14 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
selection: { nav: rootNav },
active: {
nav: rootNav,
algorithm: rootNav === IDENTIFIER ? null : password.algorithm,
algorithm: rootNav === IDENTIFIER ? undefined : password.algorithm,
} as CascadeValue,
},
username: {
selection: { nav: userNav },
active: {
nav: userNav,
algorithm: forwarderPref ? null : usernamePref.algorithm,
algorithm: forwarderPref ? undefined : usernamePref.algorithm,
},
},
forwarder: {
@@ -447,16 +471,16 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
// automatically regenerate when the algorithm switches if the algorithm
// allows it; otherwise set a placeholder
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
this.zone.run(() => {
if (!a || a.onlyOnRequest) {
this.log.debug("autogeneration disabled; clearing generated credential");
this.generatedCredential$.next(null);
} else {
if (a?.capabilities?.autogenerate) {
this.log.debug("autogeneration enabled");
this.generate("autogenerate").catch((e: unknown) => {
this.log.error(e as object, "a failure occurred during autogeneration");
});
} else {
this.log.debug("autogeneration disabled; clearing generated credential");
this.generatedCredential$.next(undefined);
}
});
});
@@ -468,41 +492,6 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
}
private typeToGenerator$(algorithm: CredentialAlgorithm) {
const dependencies = {
on$: this.generate$,
account$: this.account$,
};
this.log.debug({ algorithm }, "constructing generation stream");
switch (algorithm) {
case "catchall":
return this.generatorService.generate$(Generators.catchall, dependencies);
case "subaddress":
return this.generatorService.generate$(Generators.subaddress, dependencies);
case "username":
return this.generatorService.generate$(Generators.username, dependencies);
case "password":
return this.generatorService.generate$(Generators.password, dependencies);
case "passphrase":
return this.generatorService.generate$(Generators.passphrase, dependencies);
}
if (isForwarderIntegration(algorithm)) {
const forwarder = getForwarderConfiguration(algorithm.forwarder);
const configuration = toCredentialGeneratorConfiguration(forwarder);
const generator = this.generatorService.generate$(configuration, dependencies);
return generator;
}
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
}
/** Lists the top-level credential types supported by the component.
* @remarks This is string-typed because angular doesn't support
* structural equality for objects, which prevents `CredentialAlgorithm`
@@ -518,15 +507,20 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
protected forwarderOptions$ = new BehaviorSubject<Option<string>[]>([]);
/** Tracks the currently selected forwarder. */
protected forwarderId$ = new BehaviorSubject<IntegrationId>(null);
protected forwarderId$ = new BehaviorSubject<VendorId | null>(null);
/** Tracks forwarder control visibility */
protected showForwarder$ = new BehaviorSubject<boolean>(false);
/** tracks the currently selected credential type */
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
protected maybeAlgorithm$ = new ReplaySubject<AlgorithmMetadata | null>(1);
protected showAlgorithm$ = this.algorithm$.pipe(
/** tracks the last valid algorithm selection */
protected algorithm$ = this.maybeAlgorithm$.pipe(
filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm),
);
protected showAlgorithm$ = this.maybeAlgorithm$.pipe(
combineLatestWith(this.showForwarder$),
map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)),
);
@@ -535,33 +529,32 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
* Emits the copy button aria-label respective of the selected credential type
*/
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ copy }) => copy),
map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)),
);
/**
* Emits the generate button aria-label respective of the selected credential type
*/
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ generate }) => generate),
map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)),
);
/**
* Emits the copy credential toast respective of the selected credential type
*/
protected credentialTypeLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ credentialType }) => credentialType),
map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)),
);
/** Emits hint key for the currently selected credential type */
protected credentialTypeHint$ = new ReplaySubject<string>(1);
protected credentialTypeHint$ = new ReplaySubject<string | undefined>(1);
/** tracks the currently selected credential category */
protected category$ = new ReplaySubject<string>(1);
protected category$ = new ReplaySubject<string | undefined>(1);
private readonly generatedCredential$ = new BehaviorSubject<GeneratedCredential>(null);
private readonly generatedCredential$ = new BehaviorSubject<GeneratedCredential | undefined>(
undefined,
);
/** Emits the last generated value. */
protected readonly value$ = this.generatedCredential$.pipe(
@@ -579,15 +572,20 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
* origin in the debugger.
*/
protected async generate(source: string) {
const request = { source, website: this.website };
const algorithm = await firstValueFrom(this.algorithm$);
const request: GenerateRequest = { source, algorithm: algorithm.id };
if (this.website) {
request.website = this.website;
}
this.log.debug(request, "generation requested");
this.generate$.next(request);
}
private toOptions(algorithms: AlgorithmInfo[]) {
private toOptions(algorithms: AlgorithmMetadata[]) {
const options: Option<string>[] = algorithms.map((algorithm) => ({
value: JSON.stringify(algorithm.id),
label: algorithm.name,
label: translate(algorithm.i18nKeys.name, this.i18nService),
}));
return options;

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
Component,
EventEmitter,
@@ -14,13 +12,11 @@ import { FormBuilder } from "@angular/forms";
import { map, ReplaySubject, skip, Subject, switchAll, takeUntil, withLatestFrom } from "rxjs";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { VendorId } from "@bitwarden/common/tools/extension";
import {
CredentialGeneratorConfiguration,
CredentialGeneratorService,
getForwarderConfiguration,
NoPolicy,
toCredentialGeneratorConfiguration,
ForwarderOptions,
GeneratorMetadata,
} from "@bitwarden/generator-core";
const Controls = Object.freeze({
@@ -36,7 +32,6 @@ const Controls = Object.freeze({
})
export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy {
/** Instantiates the component
* @param accountService queries user availability
* @param generatorService settings and policy logic
* @param formBuilder reactive form controls
*/
@@ -46,14 +41,16 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
) {}
/** Binds the component to a specific user's settings.
* @remarks this is initialized to null but since it's a required input it'll
* never have that value in practice.
*/
@Input({ required: true })
account: Account;
account: Account = null!;
protected account$ = new ReplaySubject<Account>(1);
@Input({ required: true })
forwarder: IntegrationId;
forwarder: VendorId = null!;
/** Emits settings updates and completes if the settings become unavailable.
* @remarks this does not emit the initial settings. If you would like
@@ -70,24 +67,19 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
[Controls.baseUrl]: [""],
});
private forwarderId$ = new ReplaySubject<IntegrationId>(1);
private vendor = new ReplaySubject<VendorId>(1);
async ngOnInit() {
const forwarder$ = new ReplaySubject<CredentialGeneratorConfiguration<any, NoPolicy>>(1);
this.forwarderId$
const forwarder$ = new ReplaySubject<GeneratorMetadata<ForwarderOptions>>(1);
this.vendor
.pipe(
map((id) => getForwarderConfiguration(id)),
// type erasure necessary because the configuration properties are
// determined dynamically at runtime
// FIXME: this can be eliminated by unifying the forwarder settings types;
// see `ForwarderConfiguration<...>` for details.
map((forwarder) => toCredentialGeneratorConfiguration<any>(forwarder)),
map((vendor) => this.generatorService.forwarder(vendor)),
takeUntil(this.destroyed$),
)
.subscribe((forwarder) => {
this.displayDomain = forwarder.request.includes("domain");
this.displayToken = forwarder.request.includes("token");
this.displayBaseUrl = forwarder.request.includes("baseUrl");
this.displayDomain = forwarder.capabilities.fields.includes("domain");
this.displayToken = forwarder.capabilities.fields.includes("token");
this.displayBaseUrl = forwarder.capabilities.fields.includes("baseUrl");
forwarder$.next(forwarder);
});
@@ -106,10 +98,10 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
forwarder$.pipe(takeUntil(this.destroyed$)).subscribe((forwarder) => {
for (const name in Controls) {
const control = this.settings.get(name);
if (forwarder.request.includes(name as any)) {
control.enable({ emitEvent: false });
if (forwarder.capabilities.fields.includes(name)) {
control?.enable({ emitEvent: false });
} else {
control.disable({ emitEvent: false });
control?.disable({ emitEvent: false });
}
}
});
@@ -127,7 +119,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
this.saveSettings
.pipe(withLatestFrom(this.settings.valueChanges, settings$), takeUntil(this.destroyed$))
.subscribe(([, value, settings]) => {
settings.next(value);
settings.next(value as ForwarderOptions);
});
}
@@ -139,7 +131,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
async ngOnChanges(changes: SimpleChanges) {
this.refresh$.complete();
if ("forwarder" in changes) {
this.forwarderId$.next(this.forwarder);
this.vendor.next(this.forwarder);
}
if ("account" in changes) {
@@ -147,9 +139,9 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
}
}
protected displayDomain: boolean;
protected displayToken: boolean;
protected displayBaseUrl: boolean;
protected displayDomain: boolean = false;
protected displayToken: boolean = false;
protected displayBaseUrl: boolean = false;
private readonly refresh$ = new Subject<void>();

View File

@@ -3,23 +3,37 @@ import { NgModule } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { Site } from "@bitwarden/common/tools/extension";
import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction";
import { ExtensionService } from "@bitwarden/common/tools/extension/extension.service";
import { DefaultFields, DefaultSites, Extension } from "@bitwarden/common/tools/extension/metadata";
import { RuntimeExtensionRegistry } from "@bitwarden/common/tools/extension/runtime-extension-registry";
import { VendorExtensions, Vendors } from "@bitwarden/common/tools/extension/vendor";
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
import {
BuiltIn,
createRandomizer,
CredentialGeneratorService,
Randomizer,
providers,
DefaultCredentialGeneratorService,
} from "@bitwarden/generator-core";
import { KeyService } from "@bitwarden/key-management";
export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
const GENERATOR_SERVICE_PROVIDER = new SafeInjectionToken<providers.CredentialGeneratorProviders>(
"CredentialGeneratorProviders",
);
const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("SystemServices");
/** Shared module containing generator component dependencies */
@NgModule({
@@ -35,6 +49,98 @@ export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
useClass: KeyServiceLegacyEncryptorProvider,
deps: [EncryptService, KeyService],
}),
safeProvider({
provide: ExtensionRegistry,
useFactory: () => {
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
registry.registerSite(Extension[Site.forwarder]);
for (const vendor of Vendors) {
registry.registerVendor(vendor);
}
for (const extension of VendorExtensions) {
registry.registerExtension(extension);
}
registry.setPermission({ all: true }, "default");
return registry;
},
deps: [],
}),
safeProvider({
provide: SYSTEM_SERVICE_PROVIDER,
useFactory: (
encryptor: LegacyEncryptorProvider,
state: StateProvider,
policy: PolicyService,
registry: ExtensionRegistry,
) => {
const log = disabledSemanticLoggerProvider;
const extension = new ExtensionService(registry, {
encryptor,
state,
log,
});
return {
policy,
extension,
log,
};
},
deps: [LegacyEncryptorProvider, StateProvider, PolicyService, ExtensionRegistry],
}),
safeProvider({
provide: GENERATOR_SERVICE_PROVIDER,
useFactory: (
system: SystemServiceProvider,
random: Randomizer,
encryptor: LegacyEncryptorProvider,
state: StateProvider,
rest: RestClient,
i18n: I18nService,
) => {
const userStateDeps = {
encryptor,
state,
log: system.log,
} satisfies UserStateSubjectDependencyProvider;
const metadata = new providers.GeneratorMetadataProvider(
userStateDeps,
system,
Object.values(BuiltIn),
);
const profile = new providers.GeneratorProfileProvider(userStateDeps, system.policy);
const generator: providers.GeneratorDependencyProvider = {
randomizer: random,
client: rest,
i18nService: i18n,
};
const userState: UserStateSubjectDependencyProvider = {
encryptor,
state,
log: system.log,
};
return {
userState,
generator,
profile,
metadata,
} satisfies providers.CredentialGeneratorProviders;
},
deps: [
SYSTEM_SERVICE_PROVIDER,
RANDOMIZER,
LegacyEncryptorProvider,
StateProvider,
RestClient,
I18nService,
],
}),
safeProvider({
provide: UserStateSubjectDependencyProvider,
useFactory: (encryptor: LegacyEncryptorProvider, state: StateProvider) =>
@@ -47,14 +153,8 @@ export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
}),
safeProvider({
provide: CredentialGeneratorService,
useClass: CredentialGeneratorService,
deps: [
RANDOMIZER,
PolicyService,
ApiService,
I18nService,
UserStateSubjectDependencyProvider,
],
useClass: DefaultCredentialGeneratorService,
deps: [GENERATOR_SERVICE_PROVIDER, SYSTEM_SERVICE_PROVIDER],
}),
],
})

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
OnInit,
@@ -17,9 +15,9 @@ import { skip, takeUntil, Subject, map, withLatestFrom, ReplaySubject } from "rx
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
Generators,
CredentialGeneratorService,
PassphraseGenerationOptions,
BuiltIn,
} from "@bitwarden/generator-core";
const Controls = Object.freeze({
@@ -47,9 +45,11 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
) {}
/** Binds the component to a specific user's settings.
* @remarks this is initialized to null but since it's a required input it'll
* never have that value in practice.
*/
@Input({ required: true })
account: Account;
account: Account = null!;
protected account$ = new ReplaySubject<Account>(1);
@@ -69,20 +69,20 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
/** Emits settings updates and completes if the settings become unavailable.
* @remarks this does not emit the initial settings. If you would like
* to receive live settings updates including the initial update,
* use `CredentialGeneratorService.settings$(...)` instead.
* use `CredentialGeneratorService.settings(...)` instead.
*/
@Output()
readonly onUpdated = new EventEmitter<PassphraseGenerationOptions>();
protected settings = this.formBuilder.group({
[Controls.numWords]: [Generators.passphrase.settings.initial.numWords],
[Controls.wordSeparator]: [Generators.passphrase.settings.initial.wordSeparator],
[Controls.capitalize]: [Generators.passphrase.settings.initial.capitalize],
[Controls.includeNumber]: [Generators.passphrase.settings.initial.includeNumber],
[Controls.numWords]: [0],
[Controls.wordSeparator]: [""],
[Controls.capitalize]: [false],
[Controls.includeNumber]: [false],
});
async ngOnInit() {
const settings = await this.generatorService.settings(Generators.passphrase, {
const settings = await this.generatorService.settings(BuiltIn.passphrase, {
account$: this.account$,
});
@@ -94,13 +94,13 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
let boundariesHint = this.i18nService.t(
"spinboxBoundariesHint",
constraints.numWords.min?.toString(),
constraints.numWords.max?.toString(),
constraints.numWords?.min?.toString(),
constraints.numWords?.max?.toString(),
);
if (state.numWords <= (constraints.numWords.recommendation ?? 0)) {
if ((state.numWords ?? 0) <= (constraints.numWords?.recommendation ?? 0)) {
boundariesHint += this.i18nService.t(
"passphraseNumWordsRecommendationHint",
constraints.numWords.recommendation?.toString(),
constraints.numWords?.recommendation?.toString(),
);
}
this.numWordsBoundariesHint.next(boundariesHint);
@@ -111,11 +111,11 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
// explain policy & disable policy-overridden fields
this.generatorService
.policy$(Generators.passphrase, { account$: this.account$ })
.policy$(BuiltIn.passphrase, { account$: this.account$ })
.pipe(takeUntil(this.destroyed$))
.subscribe(({ constraints }) => {
this.wordSeparatorMaxLength = constraints.wordSeparator.maxLength;
this.policyInEffect = constraints.policyInEffect;
this.wordSeparatorMaxLength = constraints.wordSeparator?.maxLength ?? 0;
this.policyInEffect = constraints.policyInEffect ?? false;
this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly);
this.toggleEnabled(Controls.includeNumber, !constraints.includeNumber?.readonly);
@@ -125,14 +125,14 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
this.saveSettings
.pipe(
withLatestFrom(this.settings.valueChanges),
map(([, settings]) => settings),
map(([, settings]) => settings as PassphraseGenerationOptions),
takeUntil(this.destroyed$),
)
.subscribe(settings);
}
/** attribute binding for wordSeparator[maxlength] */
protected wordSeparatorMaxLength: number;
protected wordSeparatorMaxLength: number = 0;
private saveSettings = new Subject<string>();
save(site: string = "component api call") {
@@ -140,7 +140,7 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
}
/** display binding for enterprise policy notice */
protected policyInEffect: boolean;
protected policyInEffect: boolean = false;
private numWordsBoundariesHint = new ReplaySubject<string>(1);
@@ -149,9 +149,9 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) {
if (enabled) {
this.settings.get(setting).enable({ emitEvent: false });
this.settings.get(setting)?.enable({ emitEvent: false });
} else {
this.settings.get(setting).disable({ emitEvent: false });
this.settings.get(setting)?.disable({ emitEvent: false });
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LiveAnnouncer } from "@angular/cdk/a11y";
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
@@ -22,12 +20,12 @@ import {
map,
ReplaySubject,
Subject,
switchMap,
takeUntil,
withLatestFrom,
} from "rxjs";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
SemanticLogger,
@@ -38,17 +36,20 @@ import { UserId } from "@bitwarden/common/types/guid";
import { ToastService, Option } from "@bitwarden/components";
import {
CredentialGeneratorService,
Generators,
GeneratedCredential,
AlgorithmInfo,
GenerateRequest,
isSameAlgorithm,
CredentialAlgorithm,
isPasswordAlgorithm,
AlgorithmInfo,
isSameAlgorithm,
GenerateRequest,
CredentialCategories,
Algorithm,
AlgorithmMetadata,
Type,
} from "@bitwarden/generator-core";
import { GeneratorHistoryService } from "@bitwarden/generator-history";
import { toAlgorithmInfo, translate } from "./util";
/** Options group for passwords */
@Component({
selector: "tools-password-generator",
@@ -59,6 +60,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
private generatorService: CredentialGeneratorService,
private generatorHistoryService: GeneratorHistoryService,
private toastService: ToastService,
private i18nService: I18nService,
private logService: LogService,
private accountService: AccountService,
private zone: NgZone,
@@ -69,7 +71,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
* the form binds to the active user
*/
@Input()
account: Account | null;
account: Account | null = null;
protected account$ = new ReplaySubject<Account>(1);
@@ -86,7 +88,11 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
async ngOnChanges(changes: SimpleChanges) {
const account = changes?.account;
if (account?.previousValue?.id !== account?.currentValue?.id) {
if (
account &&
account.currentValue.id &&
account.previousValue.id !== account.currentValue.id
) {
this.log.debug(
{
previousUserId: account?.previousValue?.id as UserId,
@@ -94,7 +100,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
},
"account input change detected",
);
this.account$.next(this.account);
this.account$.next(account.currentValue.id);
}
}
@@ -102,7 +108,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
/** tracks the currently selected credential type */
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm>(null);
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm>(Algorithm.password);
/** Emits the last generated value. */
protected readonly value$ = new BehaviorSubject<string>("");
@@ -118,9 +124,11 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
* origin in the debugger.
*/
protected async generate(source: string) {
this.log.debug({ source }, "generation requested");
const algorithm = await firstValueFrom(this.algorithm$);
const request: GenerateRequest = { source, algorithm: algorithm.id };
this.generate$.next({ source });
this.log.debug(request, "generation requested");
this.generate$.next(request);
}
/** Tracks changes to the selected credential type
@@ -149,12 +157,13 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
});
if (!this.account) {
this.account = await firstValueFrom(this.accountService.activeAccount$);
this.log.info(
{ userId: this.account.id },
"account not specified; using active account settings",
);
this.account$.next(this.account);
const account = await firstValueFrom(this.accountService.activeAccount$);
if (!account) {
this.log.panic("active account cannot be `null`.");
}
this.log.info({ userId: account.id }, "account not specified; using active account settings");
this.account$.next(account);
}
this.generatorService
@@ -166,10 +175,12 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
.subscribe(this.passwordOptions$);
// wire up the generator
this.algorithm$
this.generatorService
.generate$({
on$: this.generate$,
account$: this.account$,
})
.pipe(
filter((algorithm) => !!algorithm),
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
catchError((error: unknown, generator) => {
if (typeof error === "string") {
this.toastService.showToast({
@@ -188,7 +199,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
takeUntil(this.destroyed),
)
.subscribe(([generated, account, algorithm]) => {
this.log.debug({ source: generated.source }, "credential generated");
this.log.debug({ source: generated.source ?? null }, "credential generated");
this.generatorHistoryService
.track(account.id, generated.credential, generated.category, generated.generationDate)
@@ -200,7 +211,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
// template bindings refresh immediately
this.zone.run(() => {
if (generated.source === this.USER_REQUEST) {
this.announce(algorithm.onGeneratedMessage);
this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService));
}
this.onGenerated.next(generated);
@@ -218,10 +229,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
)
.subscribe(([algorithm, preference]) => {
if (isPasswordAlgorithm(algorithm)) {
this.log.info(
{ algorithm, category: CredentialCategories.password },
"algorithm preferences updated",
);
this.log.info({ algorithm, type: Type.password }, "algorithm preferences updated");
preference.password.algorithm = algorithm;
preference.password.updated = new Date();
} else {
@@ -235,11 +243,17 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
preferences
.pipe(
map(({ password }) => this.generatorService.algorithm(password.algorithm)),
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
distinctUntilChanged((prev, next) => {
if (prev === null || next === null) {
return false;
} else {
return isSameAlgorithm(prev.id, next.id);
}
}),
takeUntil(this.destroyed),
)
.subscribe((algorithm) => {
this.log.debug(algorithm, "algorithm selected");
this.log.debug({ algorithm: algorithm.id }, "algorithm selected");
// update navigation
this.onCredentialTypeChanged(algorithm.id);
@@ -247,22 +261,22 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
this.algorithm$.next(algorithm);
this.onAlgorithm.next(algorithm);
this.maybeAlgorithm$.next(algorithm);
this.onAlgorithm.next(toAlgorithmInfo(algorithm, this.i18nService));
});
});
// generate on load unless the generator prohibits it
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
this.zone.run(() => {
if (!a || a.onlyOnRequest) {
this.log.debug("autogeneration disabled; clearing generated credential");
this.value$.next("-");
} else {
if (a?.capabilities?.autogenerate) {
this.log.debug("autogeneration enabled");
this.generate("autogenerate").catch((e: unknown) => {
this.log.error(e as object, "a failure occurred during autogeneration");
});
} else {
this.log.debug("autogeneration disabled; clearing generated credential");
this.value$.next("-");
}
});
});
@@ -274,59 +288,42 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
}
private typeToGenerator$(algorithm: CredentialAlgorithm) {
const dependencies = {
on$: this.generate$,
account$: this.account$,
};
this.log.debug({ algorithm }, "constructing generation stream");
switch (algorithm) {
case "password":
return this.generatorService.generate$(Generators.password, dependencies);
case "passphrase":
return this.generatorService.generate$(Generators.passphrase, dependencies);
default:
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
}
}
/** Lists the credential types supported by the component. */
protected passwordOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]);
/** tracks the currently selected credential type */
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
protected maybeAlgorithm$ = new ReplaySubject<AlgorithmMetadata>(1);
/** tracks the last valid algorithm selection */
protected algorithm$ = this.maybeAlgorithm$.pipe(
filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm),
);
/**
* Emits the copy button aria-label respective of the selected credential type
*/
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ copy }) => copy),
map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)),
);
/**
* Emits the generate button aria-label respective of the selected credential type
*/
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ generate }) => generate),
map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)),
);
/**
* Emits the copy credential toast respective of the selected credential type
*/
protected credentialTypeLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ credentialType }) => credentialType),
map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)),
);
private toOptions(algorithms: AlgorithmInfo[]) {
private toOptions(algorithms: AlgorithmMetadata[]) {
const options: Option<CredentialAlgorithm>[] = algorithms.map((algorithm) => ({
value: algorithm.id,
label: algorithm.name,
label: translate(algorithm.i18nKeys.name, this.i18nService),
}));
return options;

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
OnInit,
@@ -17,9 +15,9 @@ import { takeUntil, Subject, map, filter, tap, skip, ReplaySubject, withLatestFr
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
Generators,
CredentialGeneratorService,
PasswordGenerationOptions,
BuiltIn,
} from "@bitwarden/generator-core";
const Controls = Object.freeze({
@@ -51,9 +49,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
) {}
/** Binds the component to a specific user's settings.
* @remarks this is initialized to null but since it's a required input it'll
* never have that value in practice.
*/
@Input({ required: true })
account: Account;
account: Account = null!;
protected account$ = new ReplaySubject<Account>(1);
@@ -77,40 +77,40 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
/** Emits settings updates and completes if the settings become unavailable.
* @remarks this does not emit the initial settings. If you would like
* to receive live settings updates including the initial update,
* use `CredentialGeneratorService.settings$(...)` instead.
* use `CredentialGeneratorService.settings(...)` instead.
*/
@Output()
readonly onUpdated = new EventEmitter<PasswordGenerationOptions>();
protected settings = this.formBuilder.group({
[Controls.length]: [Generators.password.settings.initial.length],
[Controls.uppercase]: [Generators.password.settings.initial.uppercase],
[Controls.lowercase]: [Generators.password.settings.initial.lowercase],
[Controls.number]: [Generators.password.settings.initial.number],
[Controls.special]: [Generators.password.settings.initial.special],
[Controls.minNumber]: [Generators.password.settings.initial.minNumber],
[Controls.minSpecial]: [Generators.password.settings.initial.minSpecial],
[Controls.avoidAmbiguous]: [!Generators.password.settings.initial.ambiguous],
[Controls.length]: [0],
[Controls.uppercase]: [false],
[Controls.lowercase]: [false],
[Controls.number]: [false],
[Controls.special]: [false],
[Controls.minNumber]: [0],
[Controls.minSpecial]: [0],
[Controls.avoidAmbiguous]: [false],
});
private get numbers() {
return this.settings.get(Controls.number);
return this.settings.get(Controls.number)!;
}
private get special() {
return this.settings.get(Controls.special);
return this.settings.get(Controls.special)!;
}
private get minNumber() {
return this.settings.get(Controls.minNumber);
return this.settings.get(Controls.minNumber)!;
}
private get minSpecial() {
return this.settings.get(Controls.minSpecial);
return this.settings.get(Controls.minSpecial)!;
}
async ngOnInit() {
const settings = await this.generatorService.settings(Generators.password, {
const settings = await this.generatorService.settings(BuiltIn.password, {
account$: this.account$,
});
@@ -129,13 +129,13 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
.subscribe(([state, constraints]) => {
let boundariesHint = this.i18nService.t(
"spinboxBoundariesHint",
constraints.length.min?.toString(),
constraints.length.max?.toString(),
constraints.length?.min?.toString(),
constraints.length?.max?.toString(),
);
if (state.length <= (constraints.length.recommendation ?? 0)) {
if (state.length <= (constraints.length?.recommendation ?? 0)) {
boundariesHint += this.i18nService.t(
"passwordLengthRecommendationHint",
constraints.length.recommendation?.toString(),
constraints.length?.recommendation?.toString(),
);
}
this.lengthBoundariesHint.next(boundariesHint);
@@ -146,19 +146,25 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
// explain policy & disable policy-overridden fields
this.generatorService
.policy$(Generators.password, { account$: this.account$ })
.policy$(BuiltIn.password, { account$: this.account$ })
.pipe(takeUntil(this.destroyed$))
.subscribe(({ constraints }) => {
this.policyInEffect = constraints.policyInEffect;
this.policyInEffect = constraints.policyInEffect ?? false;
const toggles = [
[Controls.length, constraints.length.min < constraints.length.max],
[Controls.length, (constraints.length?.min ?? 0) < (constraints.length?.max ?? 1)],
[Controls.uppercase, !constraints.uppercase?.readonly],
[Controls.lowercase, !constraints.lowercase?.readonly],
[Controls.number, !constraints.number?.readonly],
[Controls.special, !constraints.special?.readonly],
[Controls.minNumber, constraints.minNumber.min < constraints.minNumber.max],
[Controls.minSpecial, constraints.minSpecial.min < constraints.minSpecial.max],
[
Controls.minNumber,
(constraints.minNumber?.min ?? 0) < (constraints.minNumber?.max ?? 1),
],
[
Controls.minSpecial,
(constraints.minSpecial?.min ?? 0) < (constraints.minSpecial?.max ?? 1),
],
] as [keyof typeof Controls, boolean][];
for (const [control, enabled] of toggles) {
@@ -171,7 +177,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
let lastMinNumber = 1;
this.numbers.valueChanges
.pipe(
filter((checked) => !(checked && this.minNumber.value > 0)),
filter((checked) => !(checked && (this.minNumber.value ?? 0) > 0)),
map((checked) => (checked ? lastMinNumber : 0)),
takeUntil(this.destroyed$),
)
@@ -179,8 +185,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
this.minNumber.valueChanges
.pipe(
map((value) => [value, value > 0] as const),
tap(([value, checkNumbers]) => (lastMinNumber = checkNumbers ? value : lastMinNumber)),
map((value) => [value, (value ?? 0) > 0] as const),
tap(
([value, checkNumbers]) =>
(lastMinNumber = checkNumbers && value ? value : lastMinNumber),
),
takeUntil(this.destroyed$),
)
.subscribe(([, checkNumbers]) => this.numbers.setValue(checkNumbers, { emitEvent: false }));
@@ -188,7 +197,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
let lastMinSpecial = 1;
this.special.valueChanges
.pipe(
filter((checked) => !(checked && this.minSpecial.value > 0)),
filter((checked) => !(checked && (this.minSpecial.value ?? 0) > 0)),
map((checked) => (checked ? lastMinSpecial : 0)),
takeUntil(this.destroyed$),
)
@@ -196,8 +205,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
this.minSpecial.valueChanges
.pipe(
map((value) => [value, value > 0] as const),
tap(([value, checkSpecial]) => (lastMinSpecial = checkSpecial ? value : lastMinSpecial)),
map((value) => [value, (value ?? 0) > 0] as const),
tap(
([value, checkSpecial]) =>
(lastMinSpecial = checkSpecial && value ? value : lastMinSpecial),
),
takeUntil(this.destroyed$),
)
.subscribe(([, checkSpecial]) => this.special.setValue(checkSpecial, { emitEvent: false }));
@@ -229,7 +241,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
}
/** display binding for enterprise policy notice */
protected policyInEffect: boolean;
protected policyInEffect: boolean = false;
private lengthBoundariesHint = new ReplaySubject<string>(1);
@@ -238,9 +250,9 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) {
if (enabled) {
this.settings.get(setting).enable({ emitEvent: false });
this.settings.get(setting)?.enable({ emitEvent: false });
} else {
this.settings.get(setting).disable({ emitEvent: false });
this.settings.get(setting)?.disable({ emitEvent: false });
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
Component,
EventEmitter,
@@ -13,10 +11,10 @@ import {
import { FormBuilder } from "@angular/forms";
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import {
CredentialGeneratorService,
Generators,
BuiltIn,
SubaddressGenerationOptions,
} from "@bitwarden/generator-core";
@@ -27,20 +25,20 @@ import {
})
export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy {
/** Instantiates the component
* @param accountService queries user availability
* @param generatorService settings and policy logic
* @param formBuilder reactive form controls
*/
constructor(
private formBuilder: FormBuilder,
private generatorService: CredentialGeneratorService,
private accountService: AccountService,
) {}
/** Binds the component to a specific user's settings.
* @remarks this is initialized to null but since it's a required input it'll
* never have that value in practice.
*/
@Input({ required: true })
account: Account;
account: Account = null!;
protected account$ = new ReplaySubject<Account>(1);
@@ -53,18 +51,18 @@ export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy
/** Emits settings updates and completes if the settings become unavailable.
* @remarks this does not emit the initial settings. If you would like
* to receive live settings updates including the initial update,
* use `CredentialGeneratorService.settings$(...)` instead.
* use `CredentialGeneratorService.settings(...)` instead.
*/
@Output()
readonly onUpdated = new EventEmitter<SubaddressGenerationOptions>();
/** The template's control bindings */
protected settings = this.formBuilder.group({
subaddressEmail: [Generators.subaddress.settings.initial.subaddressEmail],
subaddressEmail: [""],
});
async ngOnInit() {
const settings = await this.generatorService.settings(Generators.subaddress, {
const settings = await this.generatorService.settings(BuiltIn.plusAddress, {
account$: this.account$,
});
@@ -78,7 +76,7 @@ export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy
this.saveSettings
.pipe(
withLatestFrom(this.settings.valueChanges),
map(([, settings]) => settings),
map(([, settings]) => settings as SubaddressGenerationOptions),
takeUntil(this.destroyed$),
)
.subscribe(settings);

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LiveAnnouncer } from "@angular/cdk/a11y";
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
@@ -25,7 +23,6 @@ import {
map,
ReplaySubject,
Subject,
switchMap,
takeUntil,
withLatestFrom,
} from "rxjs";
@@ -33,7 +30,7 @@ import {
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { VendorId } from "@bitwarden/common/tools/extension";
import {
SemanticLogger,
disabledSemanticLoggerProvider,
@@ -43,21 +40,20 @@ import { UserId } from "@bitwarden/common/types/guid";
import { ToastService, Option } from "@bitwarden/components";
import {
AlgorithmInfo,
CredentialAlgorithm,
CredentialCategories,
CredentialGeneratorService,
GenerateRequest,
GeneratedCredential,
Generators,
getForwarderConfiguration,
isForwarderExtensionId,
isEmailAlgorithm,
isForwarderIntegration,
isSameAlgorithm,
isUsernameAlgorithm,
toCredentialGeneratorConfiguration,
isSameAlgorithm,
CredentialAlgorithm,
AlgorithmMetadata,
} from "@bitwarden/generator-core";
import { GeneratorHistoryService } from "@bitwarden/generator-history";
import { toAlgorithmInfo, translate } from "./util";
// constants used to identify navigation selections that are not
// generator algorithms
const FORWARDER = "forwarder";
@@ -92,7 +88,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
* the form binds to the active user
*/
@Input()
account: Account | null;
account: Account | null = null;
protected account$ = new ReplaySubject<Account>(1);
@@ -109,7 +105,11 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
async ngOnChanges(changes: SimpleChanges) {
const account = changes?.account;
if (account?.previousValue?.id !== account?.currentValue?.id) {
if (
account &&
account.currentValue.id &&
account.previousValue.id !== account.currentValue.id
) {
this.log.debug(
{
previousUserId: account?.previousValue?.id as UserId,
@@ -117,7 +117,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
},
"account input change detected",
);
this.account$.next(this.account);
this.account$.next(account.currentValue.id);
}
}
@@ -133,18 +133,18 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
/** emits algorithm info when the selected algorithm changes */
@Output()
readonly onAlgorithm = new EventEmitter<AlgorithmInfo>();
readonly onAlgorithm = new EventEmitter<AlgorithmInfo | null>();
/** Removes bottom margin from internal elements */
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
/** Tracks the selected generation algorithm */
protected username = this.formBuilder.group({
nav: [null as string],
nav: [null as string | null],
});
protected forwarder = this.formBuilder.group({
nav: [null as string],
nav: [null as string | null],
});
async ngOnInit() {
@@ -153,23 +153,27 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
});
if (!this.account) {
this.account = await firstValueFrom(this.accountService.activeAccount$);
this.log.info(
{ userId: this.account.id },
"account not specified; using active account settings",
);
this.account$.next(this.account);
const account = await firstValueFrom(this.accountService.activeAccount$);
if (!account) {
this.log.panic("active account cannot be `null`.");
}
this.log.info({ userId: account.id }, "account not specified; using active account settings");
this.account$.next(account);
}
this.generatorService
.algorithms$(["email", "username"], { account$: this.account$ })
combineLatest([
this.generatorService.algorithms$("email", { account$: this.account$ }),
this.generatorService.algorithms$("username", { account$: this.account$ }),
])
.pipe(
map((algorithms) => algorithms.flat()),
map((algorithms) => {
const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id));
const usernames = algorithms.filter((a) => !isForwarderExtensionId(a.id));
const usernameOptions = this.toOptions(usernames);
usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwardedEmail") });
const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id));
const forwarders = algorithms.filter((a) => isForwarderExtensionId(a.id));
const forwarderOptions = this.toOptions(forwarders);
forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") });
@@ -182,9 +186,15 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
this.forwarderOptions$.next(forwarders);
});
this.algorithm$
this.maybeAlgorithm$
.pipe(
map((a) => a?.description),
map((a) => {
if (a && a.i18nKeys.description) {
return translate(a.i18nKeys.description, this.i18nService);
} else {
return "";
}
}),
takeUntil(this.destroyed),
)
.subscribe((hint) => {
@@ -196,10 +206,12 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
});
// wire up the generator
this.algorithm$
this.generatorService
.generate$({
on$: this.generate$,
account$: this.account$,
})
.pipe(
filter((algorithm) => !!algorithm),
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
catchError((error: unknown, generator) => {
if (typeof error === "string") {
this.toastService.showToast({
@@ -214,11 +226,14 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
// continue with origin stream
return generator;
}),
withLatestFrom(this.account$, this.algorithm$),
withLatestFrom(this.account$, this.maybeAlgorithm$),
takeUntil(this.destroyed),
)
.subscribe(([generated, account, algorithm]) => {
this.log.debug({ source: generated.source }, "credential generated");
this.log.debug(
{ source: generated.source ?? null, algorithm: algorithm?.id ?? null },
"credential generated",
);
this.generatorHistoryService
.track(account.id, generated.credential, generated.category, generated.generationDate)
@@ -229,8 +244,8 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
if (generated.source === this.USER_REQUEST) {
this.announce(algorithm.onGeneratedMessage);
if (algorithm && generated.source === this.USER_REQUEST) {
this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService));
}
this.onGenerated.next(generated);
@@ -247,24 +262,30 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
this.username.valueChanges
.pipe(
map(
(username): CascadeValue =>
username.nav === FORWARDER
? { nav: username.nav }
: { nav: username.nav, algorithm: JSON.parse(username.nav) },
),
map((username): CascadeValue => {
if (username.nav === FORWARDER) {
return { nav: username.nav };
} else if (username.nav) {
return { nav: username.nav, algorithm: JSON.parse(username.nav) };
} else {
this.log.panic(username, "unknown navigation value.");
}
}),
takeUntil(this.destroyed),
)
.subscribe(activeIdentifier$);
this.forwarder.valueChanges
.pipe(
map(
(forwarder): CascadeValue =>
forwarder.nav === NONE_SELECTED
? { nav: forwarder.nav }
: { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) },
),
map((forwarder): CascadeValue => {
if (forwarder.nav === NONE_SELECTED) {
return { nav: forwarder.nav };
} else if (forwarder.nav) {
return { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) };
} else {
this.log.panic(forwarder, "unknown navigation value.");
}
}),
takeUntil(this.destroyed),
)
.subscribe(activeForwarder$);
@@ -275,7 +296,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
map(([username, forwarder]) => {
const showForwarder = !username.algorithm;
const forwarderId =
showForwarder && isForwarderIntegration(forwarder.algorithm)
showForwarder && forwarder.algorithm && isForwarderExtensionId(forwarder.algorithm)
? forwarder.algorithm.forwarder
: null;
return [showForwarder, forwarderId] as const;
@@ -305,54 +326,56 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
return null;
}
}),
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
distinctUntilChanged((prev, next) => {
if (prev === null || next === null) {
return false;
} else {
return isSameAlgorithm(prev.id, next.id);
}
}),
takeUntil(this.destroyed),
)
.subscribe((algorithm) => {
this.log.debug(algorithm, "algorithm selected");
this.log.debug({ algorithm: algorithm?.id ?? null }, "algorithm selected");
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
this.algorithm$.next(algorithm);
this.onAlgorithm.next(algorithm);
this.maybeAlgorithm$.next(algorithm);
if (algorithm) {
this.onAlgorithm.next(toAlgorithmInfo(algorithm, this.i18nService));
} else {
this.onAlgorithm.next(null);
}
});
});
// assume the last-visible generator algorithm is the user's preferred one
const preferences = await this.generatorService.preferences({ account$: this.account$ });
this.algorithm$
.pipe(
filter((algorithm) => !!algorithm),
withLatestFrom(preferences),
takeUntil(this.destroyed),
)
.pipe(withLatestFrom(preferences), takeUntil(this.destroyed))
.subscribe(([algorithm, preference]) => {
if (isEmailAlgorithm(algorithm.id)) {
this.log.info(
{ algorithm, category: CredentialCategories.email },
"algorithm preferences updated",
);
preference.email.algorithm = algorithm.id;
preference.email.updated = new Date();
} else if (isUsernameAlgorithm(algorithm.id)) {
this.log.info(
{ algorithm, category: CredentialCategories.username },
"algorithm preferences updated",
);
preference.username.algorithm = algorithm.id;
preference.username.updated = new Date();
} else {
return;
}
this.log.info(
{ algorithm: algorithm.id, type: algorithm.type },
"algorithm preferences updated",
);
preferences.next(preference);
});
preferences
.pipe(
map(({ email, username }) => {
const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null;
const forwarderPref = isForwarderExtensionId(email.algorithm) ? email : null;
const usernamePref = email.updated > username.updated ? email : username;
// inject drilldown flags
@@ -367,7 +390,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
selection: { nav: userNav },
active: {
nav: userNav,
algorithm: forwarderPref ? null : usernamePref.algorithm,
algorithm: forwarderPref ? undefined : usernamePref.algorithm,
},
},
forwarder: {
@@ -395,17 +418,16 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
// automatically regenerate when the algorithm switches if the algorithm
// allows it; otherwise set a placeholder
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
this.zone.run(() => {
if (!a || a.onlyOnRequest) {
this.log.debug("autogeneration disabled; clearing generated credential");
this.value$.next("-");
} else {
if (a?.capabilities?.autogenerate) {
this.log.debug("autogeneration enabled");
this.generate("autogenerate").catch((e: unknown) => {
this.log.error(e as object, "a failure occurred during autogeneration");
});
} else {
this.log.debug("autogeneration disabled; clearing generated credential");
this.value$.next("-");
}
});
});
@@ -413,34 +435,6 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
this.log.debug("component initialized");
}
private typeToGenerator$(algorithm: CredentialAlgorithm) {
const dependencies = {
on$: this.generate$,
account$: this.account$,
};
this.log.debug({ algorithm }, "constructing generation stream");
switch (algorithm) {
case "catchall":
return this.generatorService.generate$(Generators.catchall, dependencies);
case "subaddress":
return this.generatorService.generate$(Generators.subaddress, dependencies);
case "username":
return this.generatorService.generate$(Generators.username, dependencies);
}
if (isForwarderIntegration(algorithm)) {
const forwarder = getForwarderConfiguration(algorithm.forwarder);
const configuration = toCredentialGeneratorConfiguration(forwarder);
return this.generatorService.generate$(configuration, dependencies);
}
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
}
private announce(message: string) {
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
}
@@ -449,7 +443,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
protected typeOptions$ = new BehaviorSubject<Option<string>[]>([]);
/** Tracks the currently selected forwarder. */
protected forwarderId$ = new BehaviorSubject<IntegrationId>(null);
protected forwarderId$ = new BehaviorSubject<VendorId | null>(null);
/** Lists the credential types supported by the component. */
protected forwarderOptions$ = new BehaviorSubject<Option<string>[]>([]);
@@ -457,8 +451,13 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
/** Tracks forwarder control visibility */
protected showForwarder$ = new BehaviorSubject<boolean>(false);
/** tracks the currently selected credential type */
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
/** tracks the currently selected algorithm; emits `null` when no algorithm selected */
protected maybeAlgorithm$ = new ReplaySubject<AlgorithmMetadata | null>(1);
/** tracks the last valid algorithm selection */
protected algorithm$ = this.maybeAlgorithm$.pipe(
filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm),
);
/** Emits hint key for the currently selected credential type */
protected credentialTypeHint$ = new ReplaySubject<string>(1);
@@ -469,7 +468,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
/** Emits when a new credential is requested */
private readonly generate$ = new Subject<GenerateRequest>();
protected showAlgorithm$ = this.algorithm$.pipe(
protected showAlgorithm$ = this.maybeAlgorithm$.pipe(
combineLatestWith(this.showForwarder$),
map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)),
);
@@ -478,24 +477,21 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
* Emits the copy button aria-label respective of the selected credential type
*/
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ copy }) => copy),
map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)),
);
/**
* Emits the generate button aria-label respective of the selected credential type
*/
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ generate }) => generate),
map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)),
);
/**
* Emits the copy credential toast respective of the selected credential type
*/
protected credentialTypeLabel$ = this.algorithm$.pipe(
filter((algorithm) => !!algorithm),
map(({ credentialType }) => credentialType),
map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)),
);
/** Identifies generator requests that were requested by the user */
@@ -506,15 +502,20 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
* origin in the debugger.
*/
protected async generate(source: string) {
const request = { source, website: this.website };
const algorithm = await firstValueFrom(this.algorithm$);
const request: GenerateRequest = { source, algorithm: algorithm.id };
if (this.website) {
request.website = this.website;
}
this.log.debug(request, "generation requested");
this.generate$.next(request);
}
private toOptions(algorithms: AlgorithmInfo[]) {
private toOptions(algorithms: AlgorithmMetadata[]) {
const options: Option<string>[] = algorithms.map((algorithm) => ({
value: JSON.stringify(algorithm.id),
label: algorithm.name,
label: translate(algorithm.i18nKeys.name, this.i18nService),
}));
return options;

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
Component,
EventEmitter,
@@ -17,7 +15,7 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import {
CredentialGeneratorService,
EffUsernameGenerationOptions,
Generators,
BuiltIn,
} from "@bitwarden/generator-core";
/** Options group for usernames */
@@ -27,7 +25,6 @@ import {
})
export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
/** Instantiates the component
* @param accountService queries user availability
* @param generatorService settings and policy logic
* @param formBuilder reactive form controls
*/
@@ -37,9 +34,11 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
) {}
/** Binds the component to a specific user's settings.
* @remarks this is initialized to null but since it's a required input it'll
* never have that value in practice.
*/
@Input({ required: true })
account: Account;
account: Account = null!;
protected account$ = new ReplaySubject<Account>(1);
@@ -52,19 +51,19 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
/** Emits settings updates and completes if the settings become unavailable.
* @remarks this does not emit the initial settings. If you would like
* to receive live settings updates including the initial update,
* use `CredentialGeneratorService.settings$(...)` instead.
* use `CredentialGeneratorService.settings(...)` instead.
*/
@Output()
readonly onUpdated = new EventEmitter<EffUsernameGenerationOptions>();
/** The template's control bindings */
protected settings = this.formBuilder.group({
wordCapitalize: [Generators.username.settings.initial.wordCapitalize],
wordIncludeNumber: [Generators.username.settings.initial.wordIncludeNumber],
wordCapitalize: [false],
wordIncludeNumber: [false],
});
async ngOnInit() {
const settings = await this.generatorService.settings(Generators.username, {
const settings = await this.generatorService.settings(BuiltIn.effWordList, {
account$: this.account$,
});
@@ -78,7 +77,7 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
this.saveSettings
.pipe(
withLatestFrom(this.settings.valueChanges),
map(([, settings]) => settings),
map(([, settings]) => settings as EffUsernameGenerationOptions),
takeUntil(this.destroyed$),
)
.subscribe(settings);

View File

@@ -1,17 +1,16 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ValidatorFn, Validators } from "@angular/forms";
import { distinctUntilChanged, map, pairwise, pipe, skipWhile, startWith, takeWhile } from "rxjs";
import { AnyConstraint, Constraints } from "@bitwarden/common/tools/types";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { I18nKeyOrLiteral } from "@bitwarden/common/tools/types";
import { isI18nKey } from "@bitwarden/common/tools/util";
import { UserId } from "@bitwarden/common/types/guid";
import { CredentialGeneratorConfiguration } from "@bitwarden/generator-core";
import { AlgorithmInfo, AlgorithmMetadata } from "@bitwarden/generator-core";
export function completeOnAccountSwitch() {
return pipe(
map(({ id }: { id: UserId | null }) => id),
skipWhile((id) => !id),
startWith(null as UserId),
startWith(null as UserId | null),
pairwise(),
takeWhile(([prev, next]) => (prev ?? next) === next),
map(([_, id]) => id),
@@ -19,53 +18,27 @@ export function completeOnAccountSwitch() {
);
}
export function toValidators<Policy, Settings>(
target: keyof Settings,
configuration: CredentialGeneratorConfiguration<Settings, Policy>,
policy?: Constraints<Settings>,
) {
const validators: Array<ValidatorFn> = [];
export function toAlgorithmInfo(metadata: AlgorithmMetadata, i18n: I18nService) {
const info: AlgorithmInfo = {
id: metadata.id,
type: metadata.type,
name: translate(metadata.i18nKeys.name, i18n),
generate: translate(metadata.i18nKeys.generateCredential, i18n),
onGeneratedMessage: translate(metadata.i18nKeys.credentialGenerated, i18n),
credentialType: translate(metadata.i18nKeys.credentialType, i18n),
copy: translate(metadata.i18nKeys.copyCredential, i18n),
useGeneratedValue: translate(metadata.i18nKeys.useCredential, i18n),
onlyOnRequest: !metadata.capabilities.autogenerate,
request: metadata.capabilities.fields,
};
// widen the types to avoid typecheck issues
const config: AnyConstraint = configuration.settings.constraints[target];
const runtime: AnyConstraint = policy[target];
const required = getConstraint("required", config, runtime) ?? false;
if (required) {
validators.push(Validators.required);
if (metadata.i18nKeys.description) {
info.description = translate(metadata.i18nKeys.description, i18n);
}
const maxLength = getConstraint("maxLength", config, runtime);
if (maxLength !== undefined) {
validators.push(Validators.maxLength(maxLength));
}
const minLength = getConstraint("minLength", config, runtime);
if (minLength !== undefined) {
validators.push(Validators.minLength(config.minLength));
}
const min = getConstraint("min", config, runtime);
if (min !== undefined) {
validators.push(Validators.min(min));
}
const max = getConstraint("max", config, runtime);
if (max !== undefined) {
validators.push(Validators.max(max));
}
return validators;
return info;
}
function getConstraint<Key extends keyof AnyConstraint>(
key: Key,
config: AnyConstraint,
policy?: AnyConstraint,
) {
if (policy && key in policy) {
return policy[key] ?? config[key];
} else if (config && key in config) {
return config[key];
}
export function translate(key: I18nKeyOrLiteral, i18n: I18nService) {
return isI18nKey(key) ? i18n.t(key) : key.literal;
}

View File

@@ -0,0 +1,95 @@
import { Observable } from "rxjs";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies";
import { VendorId } from "@bitwarden/common/tools/extension";
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
import {
CredentialAlgorithm,
GeneratorMetadata,
GeneratorProfile,
CredentialType,
} from "../metadata";
import { AlgorithmMetadata } from "../metadata/algorithm-metadata";
import {
CredentialPreference,
ForwarderOptions,
GeneratedCredential,
GenerateRequest,
} from "../types";
import { GeneratorConstraints } from "../types/generator-constraints";
/** Generates credentials used in identity and/or authentication flows.
* @remarks typically this is for use outside of Bitwarden.
*/
export abstract class CredentialGeneratorService {
/** Generates a stream of credentials
* @param configuration determines which generator's settings are loaded
* @param dependencies.on$ Required. A new credential is emitted when this emits.
*/
abstract generate$: (
dependencies: OnDependency<GenerateRequest> & BoundDependency<"account", Account>,
) => Observable<GeneratedCredential>;
/** Emits metadata concerning the provided generation algorithms
* @param category the category or categories of interest
* @param dependences.account$ algorithms are filtered to only
* those matching the provided account's policy.
* @returns An observable that emits algorithm metadata.
*/
abstract algorithms$: (
type: CredentialType,
dependencies: BoundDependency<"account", Account>,
) => Observable<AlgorithmMetadata[]>;
/** Lists metadata for the algorithms in a credential category
* @param type the category or categories of interest
* @returns A list containing the requested metadata.
*/
abstract algorithms: (type: CredentialType | CredentialType[]) => AlgorithmMetadata[];
/** Look up the metadata for a specific generator algorithm
* @param id identifies the algorithm
* @returns the requested metadata, or `null` if the metadata wasn't found.
*/
abstract algorithm: (id: CredentialAlgorithm) => AlgorithmMetadata;
/** Look up the forwarder metadata for a vendor. */
abstract forwarder: (id: VendorId) => GeneratorMetadata<ForwarderOptions>;
/** Get a subject bound to credential generator preferences.
* @param dependencies.account$ identifies the account to which the preferences are bound
* @returns a subject bound to the user's preferences
* @remarks Preferences determine which algorithms are used when generating a
* credential from a credential category (e.g. `PassX` or `Username`). Preferences
* should not be used to hold navigation history. Use @bitwarden/generator-navigation
* instead.
*/
abstract preferences: (
dependencies: BoundDependency<"account", Account>,
) => UserStateSubject<CredentialPreference>;
/** Get a subject bound to a specific user's settings
* @param configuration determines which generator's settings are loaded
* @param dependencies.account$ identifies the account to which the settings are bound
* @returns a subject bound to the requested user's generator settings
* @remarks the subject enforces policy for the settings
*/
abstract settings: <Settings extends object>(
metadata: Readonly<GeneratorMetadata<Settings>>,
dependencies: BoundDependency<"account", Account>,
profile?: GeneratorProfile,
) => UserStateSubject<Settings>;
/** Get the policy constraints for the provided configuration
* @param dependencies.account$ determines which user's policy is loaded
* @returns an observable that emits the policy once `dependencies.account$`
* and the policy become available.
*/
abstract policy$: <Settings>(
metadata: Readonly<GeneratorMetadata<Settings>>,
dependencies: BoundDependency<"account", Account>,
profile?: GeneratorProfile,
) => Observable<GeneratorConstraints<Settings>>;
}

View File

@@ -9,6 +9,7 @@ import { PolicyEvaluator } from "./policy-evaluator.abstraction";
/** Generates credentials used for user authentication
* @typeParam Options the credential generation configuration
* @typeParam Policy the policy enforced by the generator
* @deprecated Use `CredentialGeneratorService` instead.
*/
export abstract class GeneratorService<Options, Policy> {
/** An observable monitoring the options saved to disk.

View File

@@ -1,3 +1,4 @@
export { CredentialGeneratorService } from "./credential-generator-service.abstraction";
export { GeneratorService } from "./generator.service.abstraction";
export { GeneratorStrategy } from "./generator-strategy.abstraction";
export { PolicyEvaluator } from "./policy-evaluator.abstraction";

View File

@@ -1,8 +0,0 @@
import { EmailDomainOptions, SelfHostedApiOptions } from "../types";
export const DefaultAddyIoOptions: SelfHostedApiOptions & EmailDomainOptions = Object.freeze({
website: null,
baseUrl: "https://app.addy.io",
token: "",
domain: "",
});

View File

@@ -1,18 +0,0 @@
import { CredentialPreference } from "../types";
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "./generator-types";
export const DefaultCredentialPreferences: CredentialPreference = Object.freeze({
email: Object.freeze({
algorithm: EmailAlgorithms[0],
updated: new Date(0),
}),
password: Object.freeze({
algorithm: PasswordAlgorithms[0],
updated: new Date(0),
}),
username: Object.freeze({
algorithm: UsernameAlgorithms[0],
updated: new Date(0),
}),
});

View File

@@ -1,6 +0,0 @@
import { ApiOptions } from "../types";
export const DefaultDuckDuckGoOptions: ApiOptions = Object.freeze({
website: null,
token: "",
});

View File

@@ -1,8 +0,0 @@
import { ApiOptions, EmailPrefixOptions } from "../types";
export const DefaultFastmailOptions: ApiOptions & EmailPrefixOptions = Object.freeze({
website: "",
domain: "",
prefix: "",
token: "",
});

View File

@@ -1,6 +0,0 @@
import { ApiOptions } from "../types";
export const DefaultFirefoxRelayOptions: ApiOptions = Object.freeze({
website: null,
token: "",
});

View File

@@ -1,7 +0,0 @@
import { ApiOptions, EmailDomainOptions } from "../types";
export const DefaultForwardEmailOptions: ApiOptions & EmailDomainOptions = Object.freeze({
website: null,
token: "",
domain: "",
});

View File

@@ -1,7 +0,0 @@
import { SelfHostedApiOptions } from "../types";
export const DefaultSimpleLoginOptions: SelfHostedApiOptions = Object.freeze({
website: null,
baseUrl: "https://app.simplelogin.io",
token: "",
});

View File

@@ -1,15 +0,0 @@
/** Types of passwords that may be generated by the credential generator */
export const PasswordAlgorithms = Object.freeze(["password", "passphrase"] as const);
/** Types of usernames that may be generated by the credential generator */
export const UsernameAlgorithms = Object.freeze(["username"] as const);
/** Types of email addresses that may be generated by the credential generator */
export const EmailAlgorithms = Object.freeze(["catchall", "subaddress"] as const);
/** All types of credentials that may be generated by the credential generator */
export const CredentialAlgorithms = Object.freeze([
...PasswordAlgorithms,
...UsernameAlgorithms,
...EmailAlgorithms,
] as const);

View File

@@ -1,422 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
import {
EmailRandomizer,
ForwarderConfiguration,
PasswordRandomizer,
UsernameRandomizer,
} from "../engine";
import { Forwarder } from "../engine/forwarder";
import {
DefaultPolicyEvaluator,
DynamicPasswordPolicyConstraints,
PassphraseGeneratorOptionsEvaluator,
passphraseLeastPrivilege,
PassphrasePolicyConstraints,
PasswordGeneratorOptionsEvaluator,
passwordLeastPrivilege,
} from "../policies";
import { CatchallConstraints } from "../policies/catchall-constraints";
import { SubaddressConstraints } from "../policies/subaddress-constraints";
import {
CatchallGenerationOptions,
CredentialGenerator,
CredentialGeneratorConfiguration,
EffUsernameGenerationOptions,
GeneratorDependencyProvider,
NoPolicy,
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
PasswordGenerationOptions,
PasswordGeneratorPolicy,
SubaddressGenerationOptions,
} from "../types";
import { DefaultCatchallOptions } from "./default-catchall-options";
import { DefaultEffUsernameOptions } from "./default-eff-username-options";
import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries";
import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generation-options";
import { DefaultPasswordBoundaries } from "./default-password-boundaries";
import { DefaultPasswordGenerationOptions } from "./default-password-generation-options";
import { DefaultSubaddressOptions } from "./default-subaddress-generator-options";
const PASSPHRASE: CredentialGeneratorConfiguration<
PassphraseGenerationOptions,
PassphraseGeneratorPolicy
> = Object.freeze({
id: "passphrase",
category: "password",
nameKey: "passphrase",
generateKey: "generatePassphrase",
onGeneratedMessageKey: "passphraseGenerated",
credentialTypeKey: "passphrase",
copyKey: "copyPassphrase",
useGeneratedValueKey: "useThisPassword",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PassphraseGenerationOptions> {
return new PasswordRandomizer(dependencies.randomizer);
},
},
settings: {
initial: DefaultPassphraseGenerationOptions,
constraints: {
numWords: {
min: DefaultPassphraseBoundaries.numWords.min,
max: DefaultPassphraseBoundaries.numWords.max,
recommendation: DefaultPassphraseGenerationOptions.numWords,
},
wordSeparator: { maxLength: 1 },
},
account: {
key: "passphraseGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<PassphraseGenerationOptions>([
"numWords",
"wordSeparator",
"capitalize",
"includeNumber",
]),
state: GENERATOR_DISK,
initial: DefaultPassphraseGenerationOptions,
options: {
deserializer: (value) => value,
clearOn: ["logout"],
},
} satisfies ObjectKey<PassphraseGenerationOptions>,
},
policy: {
type: PolicyType.PasswordGenerator,
disabledValue: Object.freeze({
minNumberWords: 0,
capitalize: false,
includeNumber: false,
}),
combine: passphraseLeastPrivilege,
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
toConstraints: (policy) =>
new PassphrasePolicyConstraints(policy, PASSPHRASE.settings.constraints),
},
});
const PASSWORD: CredentialGeneratorConfiguration<
PasswordGenerationOptions,
PasswordGeneratorPolicy
> = Object.freeze({
id: "password",
category: "password",
nameKey: "password",
generateKey: "generatePassword",
onGeneratedMessageKey: "passwordGenerated",
credentialTypeKey: "password",
copyKey: "copyPassword",
useGeneratedValueKey: "useThisPassword",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PasswordGenerationOptions> {
return new PasswordRandomizer(dependencies.randomizer);
},
},
settings: {
initial: DefaultPasswordGenerationOptions,
constraints: {
length: {
min: DefaultPasswordBoundaries.length.min,
max: DefaultPasswordBoundaries.length.max,
recommendation: DefaultPasswordGenerationOptions.length,
},
minNumber: {
min: DefaultPasswordBoundaries.minDigits.min,
max: DefaultPasswordBoundaries.minDigits.max,
},
minSpecial: {
min: DefaultPasswordBoundaries.minSpecialCharacters.min,
max: DefaultPasswordBoundaries.minSpecialCharacters.max,
},
},
account: {
key: "passwordGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<PasswordGenerationOptions>([
"length",
"ambiguous",
"uppercase",
"minUppercase",
"lowercase",
"minLowercase",
"number",
"minNumber",
"special",
"minSpecial",
]),
state: GENERATOR_DISK,
initial: DefaultPasswordGenerationOptions,
options: {
deserializer: (value) => value,
clearOn: ["logout"],
},
} satisfies ObjectKey<PasswordGenerationOptions>,
},
policy: {
type: PolicyType.PasswordGenerator,
disabledValue: Object.freeze({
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
}),
combine: passwordLeastPrivilege,
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
toConstraints: (policy) =>
new DynamicPasswordPolicyConstraints(policy, PASSWORD.settings.constraints),
},
});
const USERNAME: CredentialGeneratorConfiguration<EffUsernameGenerationOptions, NoPolicy> =
Object.freeze({
id: "username",
category: "username",
nameKey: "randomWord",
generateKey: "generateUsername",
onGeneratedMessageKey: "usernameGenerated",
credentialTypeKey: "username",
copyKey: "copyUsername",
useGeneratedValueKey: "useThisUsername",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<EffUsernameGenerationOptions> {
return new UsernameRandomizer(dependencies.randomizer);
},
},
settings: {
initial: DefaultEffUsernameOptions,
constraints: {},
account: {
key: "effUsernameGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<EffUsernameGenerationOptions>([
"wordCapitalize",
"wordIncludeNumber",
]),
state: GENERATOR_DISK,
initial: DefaultEffUsernameOptions,
options: {
deserializer: (value) => value,
clearOn: ["logout"],
},
} satisfies ObjectKey<EffUsernameGenerationOptions>,
},
policy: {
type: PolicyType.PasswordGenerator,
disabledValue: {},
combine(_acc: NoPolicy, _policy: Policy) {
return {};
},
createEvaluator(_policy: NoPolicy) {
return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>();
},
toConstraints(_policy: NoPolicy) {
return new IdentityConstraint<EffUsernameGenerationOptions>();
},
},
});
const CATCHALL: CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPolicy> =
Object.freeze({
id: "catchall",
category: "email",
nameKey: "catchallEmail",
descriptionKey: "catchallEmailDesc",
generateKey: "generateEmail",
onGeneratedMessageKey: "emailGenerated",
credentialTypeKey: "email",
copyKey: "copyEmail",
useGeneratedValueKey: "useThisEmail",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<CatchallGenerationOptions> {
return new EmailRandomizer(dependencies.randomizer);
},
},
settings: {
initial: DefaultCatchallOptions,
constraints: { catchallDomain: { minLength: 1 } },
account: {
key: "catchallGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<CatchallGenerationOptions>([
"catchallType",
"catchallDomain",
]),
state: GENERATOR_DISK,
initial: {
catchallType: "random",
catchallDomain: "",
},
options: {
deserializer: (value) => value,
clearOn: ["logout"],
},
} satisfies ObjectKey<CatchallGenerationOptions>,
},
policy: {
type: PolicyType.PasswordGenerator,
disabledValue: {},
combine(_acc: NoPolicy, _policy: Policy) {
return {};
},
createEvaluator(_policy: NoPolicy) {
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
},
toConstraints(_policy: NoPolicy, email: string) {
return new CatchallConstraints(email);
},
},
});
const SUBADDRESS: CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy> =
Object.freeze({
id: "subaddress",
category: "email",
nameKey: "plusAddressedEmail",
descriptionKey: "plusAddressedEmailDesc",
generateKey: "generateEmail",
onGeneratedMessageKey: "emailGenerated",
credentialTypeKey: "email",
copyKey: "copyEmail",
useGeneratedValueKey: "useThisEmail",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<SubaddressGenerationOptions> {
return new EmailRandomizer(dependencies.randomizer);
},
},
settings: {
initial: DefaultSubaddressOptions,
constraints: {},
account: {
key: "subaddressGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<SubaddressGenerationOptions>([
"subaddressType",
"subaddressEmail",
]),
state: GENERATOR_DISK,
initial: {
subaddressType: "random",
subaddressEmail: "",
},
options: {
deserializer: (value) => value,
clearOn: ["logout"],
},
} satisfies ObjectKey<SubaddressGenerationOptions>,
},
policy: {
type: PolicyType.PasswordGenerator,
disabledValue: {},
combine(_acc: NoPolicy, _policy: Policy) {
return {};
},
createEvaluator(_policy: NoPolicy) {
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
},
toConstraints(_policy: NoPolicy, email: string) {
return new SubaddressConstraints(email);
},
},
});
export function toCredentialGeneratorConfiguration<Settings extends ApiSettings = ApiSettings>(
configuration: ForwarderConfiguration<Settings>,
) {
const forwarder = Object.freeze({
id: { forwarder: configuration.id },
category: "email",
nameKey: configuration.name,
descriptionKey: "forwardedEmailDesc",
generateKey: "generateEmail",
onGeneratedMessageKey: "emailGenerated",
credentialTypeKey: "email",
copyKey: "copyEmail",
useGeneratedValueKey: "useThisEmail",
onlyOnRequest: true,
request: configuration.forwarder.request,
engine: {
create(dependencies: GeneratorDependencyProvider) {
// FIXME: figure out why `configuration` fails to typecheck
const config: any = configuration;
return new Forwarder(config, dependencies.client, dependencies.i18nService);
},
},
settings: {
initial: configuration.forwarder.defaultSettings,
constraints: configuration.forwarder.settingsConstraints,
account: configuration.forwarder.local.settings,
},
policy: {
type: PolicyType.PasswordGenerator,
disabledValue: {},
combine(_acc: NoPolicy, _policy: Policy) {
return {};
},
createEvaluator(_policy: NoPolicy) {
return new DefaultPolicyEvaluator<Settings>();
},
toConstraints(_policy: NoPolicy) {
return new IdentityConstraint<Settings>();
},
},
} satisfies CredentialGeneratorConfiguration<Settings, NoPolicy>);
return forwarder;
}
/** Generator configurations */
export const Generators = Object.freeze({
/** Passphrase generator configuration */
passphrase: PASSPHRASE,
/** Password generator configuration */
password: PASSWORD,
/** Username generator configuration */
username: USERNAME,
/** Catchall email generator configuration */
catchall: CATCHALL,
/** Email subaddress generator configuration */
subaddress: SUBADDRESS,
});

View File

@@ -1,20 +1,8 @@
export * from "./generators";
export * from "./default-addy-io-options";
export * from "./default-catchall-options";
export * from "./default-duck-duck-go-options";
export * from "./default-fastmail-options";
export * from "./default-forward-email-options";
export * from "./default-passphrase-boundaries";
export * from "./default-password-boundaries";
export * from "./default-eff-username-options";
export * from "./default-firefox-relay-options";
export * from "./default-passphrase-generation-options";
export * from "./default-password-generation-options";
export * from "./default-credential-preferences";
export * from "./default-subaddress-generator-options";
export * from "./default-simple-login-options";
export * from "./forwarders";
export * from "./integrations";
export * from "./policies";
export * from "./username-digits";
export * from "./generator-types";

View File

@@ -1,23 +0,0 @@
import {
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
PasswordGenerationOptions,
PasswordGeneratorPolicy,
PolicyConfiguration,
} from "../types";
import { Generators } from "./generators";
/** Policy configurations
* @deprecated use Generator.*.policy instead
*/
export const Policies = Object.freeze({
Passphrase: Generators.passphrase.policy,
Password: Generators.password.policy,
} satisfies {
/** Passphrase policy configuration */
Passphrase: PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions>;
/** Password policy configuration */
Password: PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions>;
});

View File

@@ -1,4 +0,0 @@
export const UsernameDigits = Object.freeze({
enabled: 4,
disabled: 0,
});

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
import { Type } from "../metadata";
import {
CatchallGenerationOptions,
CredentialGenerator,
@@ -128,7 +129,7 @@ export class EmailRandomizer
return new GeneratedCredential(
email,
"catchall",
Type.email,
Date.now(),
request.source,
request.website,
@@ -138,7 +139,7 @@ export class EmailRandomizer
return new GeneratedCredential(
email,
"subaddress",
Type.email,
Date.now(),
request.source,
request.website,

View File

@@ -8,6 +8,7 @@ import {
} from "@bitwarden/common/tools/integration/rpc";
import { GenerationRequest } from "@bitwarden/common/tools/types";
import { Type } from "../metadata";
import { CredentialGenerator, GeneratedCredential } from "../types";
import { AccountRequest, ForwarderConfiguration } from "./forwarder-configuration";
@@ -40,9 +41,8 @@ export class Forwarder implements CredentialGenerator<ApiSettings> {
const create = this.createForwardingAddress(this.configuration, settings);
const result = await this.client.fetchJson(create, requestOptions);
const id = { forwarder: this.configuration.id };
return new GeneratedCredential(result, id, Date.now());
return new GeneratedCredential(result, Type.email, Date.now());
}
private createContext<Settings>(

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
import { Type } from "../metadata";
import {
CredentialGenerator,
GenerateRequest,
@@ -86,7 +87,7 @@ export class PasswordRandomizer
return new GeneratedCredential(
password,
"password",
Type.password,
Date.now(),
request.source,
request.website,
@@ -97,7 +98,7 @@ export class PasswordRandomizer
return new GeneratedCredential(
passphrase,
"passphrase",
Type.password,
Date.now(),
request.source,
request.website,

View File

@@ -3,13 +3,33 @@ export * from "./abstractions";
export * from "./data";
export { createRandomizer } from "./factories";
export * from "./types";
export { CredentialGeneratorService } from "./services";
export { DefaultCredentialGeneratorService } from "./services";
export {
CredentialType,
CredentialAlgorithm,
PasswordAlgorithm,
Algorithm,
BuiltIn,
Type,
Profile,
GeneratorMetadata,
AlgorithmMetadata,
AlgorithmsByType,
} from "./metadata";
export {
isForwarderExtensionId,
isEmailAlgorithm,
isUsernameAlgorithm,
isPasswordAlgorithm,
isSameAlgorithm,
} from "./metadata/util";
// These internal interfacess are exposed for use by other generator modules
// They are unstable and may change arbitrarily
export * as engine from "./engine";
export * as integration from "./integration";
export * as policies from "./policies";
export * as providers from "./providers";
export * as rx from "./rx";
export * as services from "./services";
export * as strategies from "./strategies";

View File

@@ -1,6 +1,6 @@
import { CredentialAlgorithm, CredentialType } from "./type";
import { I18nKeyOrLiteral } from "@bitwarden/common/tools/types";
type I18nKeyOrLiteral = string | { literal: string };
import { CredentialAlgorithm, CredentialType } from "./type";
/** Credential generator metadata common across credential generators */
export type AlgorithmMetadata = {
@@ -14,7 +14,7 @@ export type AlgorithmMetadata = {
id: CredentialAlgorithm;
/** The kind of credential generated by this configuration */
category: CredentialType;
type: CredentialType;
/** Used to order credential algorithms for display purposes.
* Items with lesser weights appear before entries with greater
@@ -23,6 +23,10 @@ export type AlgorithmMetadata = {
weight: number;
/** Localization keys */
// FIXME: in practice, keys like `credentialGenerated` all align
// with credential types and contain duplicate keys. Extract
// them into a "credential type metadata" type and integrate
// that type with the algorithm metadata instead.
i18nKeys: {
/** descriptive name of the algorithm */
name: I18nKeyOrLiteral;

View File

@@ -4,17 +4,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
import { EmailRandomizer } from "../../engine";
import { CatchallConstraints } from "../../policies/catchall-constraints";
import {
CatchallGenerationOptions,
CredentialGenerator,
GeneratorDependencyProvider,
} from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { CatchallGenerationOptions, CredentialGenerator } from "../../types";
import { Algorithm, Type, Profile } from "../data";
import { GeneratorMetadata } from "../generator-metadata";
const catchall: GeneratorMetadata<CatchallGenerationOptions> = deepFreeze({
id: Algorithm.catchall,
category: Type.email,
type: Type.email,
weight: 210,
i18nKeys: {
name: "catchallEmail",

View File

@@ -1,19 +1,14 @@
import { ExtensionMetadata, ExtensionStorageKey } from "@bitwarden/common/tools/extension/type";
import { SelfHostedApiSettings } from "@bitwarden/common/tools/integration/rpc";
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
import { getForwarderConfiguration } from "../../data";
import { EmailDomainSettings, EmailPrefixSettings } from "../../engine";
import { Forwarder } from "../../engine/forwarder";
import { GeneratorDependencyProvider } from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { ForwarderOptions } from "../../types";
import { Profile, Type } from "../data";
import { GeneratorMetadata } from "../generator-metadata";
import { ForwarderProfileMetadata } from "../profile-metadata";
// These options are used by all forwarders; each forwarder uses a different set,
// as defined by `GeneratorMetadata<T>.capabilities.fields`.
type ForwarderOptions = Partial<EmailDomainSettings & EmailPrefixSettings & SelfHostedApiSettings>;
// update the extension metadata
export function toForwarderMetadata(
extension: ExtensionMetadata,
@@ -28,7 +23,7 @@ export function toForwarderMetadata(
const generator: GeneratorMetadata<ForwarderOptions> = {
id: { forwarder: extension.product.vendor.id },
category: Type.email,
type: Type.email,
weight: 300,
i18nKeys: {
name,

View File

@@ -4,17 +4,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
import { EmailRandomizer } from "../../engine";
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
import {
CredentialGenerator,
GeneratorDependencyProvider,
SubaddressGenerationOptions,
} from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, SubaddressGenerationOptions } from "../../types";
import { Algorithm, Profile, Type } from "../data";
import { GeneratorMetadata } from "../generator-metadata";
const plusAddress: GeneratorMetadata<SubaddressGenerationOptions> = deepFreeze({
id: Algorithm.plusAddress,
category: Type.email,
type: Type.email,
weight: 200,
i18nKeys: {
name: "plusAddressedEmail",

View File

@@ -1,4 +1,5 @@
import { CredentialGenerator, GeneratorDependencyProvider } from "../types";
import { GeneratorDependencyProvider } from "../providers";
import { CredentialGenerator } from "../types";
import { AlgorithmMetadata } from "./algorithm-metadata";
import { Profile } from "./data";

View File

@@ -3,7 +3,20 @@ import {
AlgorithmsByType as AlgorithmsByTypeData,
Type as TypeData,
} from "./data";
import catchall from "./email/catchall";
import plusAddress from "./email/plus-address";
import passphrase from "./password/eff-word-list";
import password from "./password/random-password";
import { CredentialType, CredentialAlgorithm } from "./type";
import effWordList from "./username/eff-word-list";
export const BuiltIn = {
catchall,
plusAddress,
passphrase,
password,
effWordList,
};
// `CredentialAlgorithm` is defined in terms of `ABT`; supplying
// type information in the barrel file breaks a circular dependency.
@@ -19,7 +32,8 @@ export const Types: ReadonlyArray<CredentialType> = Object.freeze(Object.values(
export { Profile, Type, Algorithm } from "./data";
export { toForwarderMetadata } from "./email/forwarder";
export { AlgorithmMetadata } from "./algorithm-metadata";
export { GeneratorMetadata } from "./generator-metadata";
export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-metadata";
export { GeneratorProfile, CredentialAlgorithm, CredentialType } from "./type";
export { GeneratorProfile, CredentialAlgorithm, PasswordAlgorithm, CredentialType } from "./type";
export { isForwarderProfile, toVendorId, isForwarderExtensionId } from "./util";

View File

@@ -5,17 +5,14 @@ import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
import { PasswordRandomizer } from "../../engine";
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies";
import {
CredentialGenerator,
GeneratorDependencyProvider,
PassphraseGenerationOptions,
} from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, PassphraseGenerationOptions } from "../../types";
import { Algorithm, Profile, Type } from "../data";
import { GeneratorMetadata } from "../generator-metadata";
const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
id: Algorithm.passphrase,
category: Type.password,
type: Type.password,
weight: 110,
i18nKeys: {
name: "passphrase",

View File

@@ -5,17 +5,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
import { PasswordRandomizer } from "../../engine";
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
import {
CredentialGenerator,
GeneratorDependencyProvider,
PasswordGeneratorSettings,
} from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, PasswordGeneratorSettings } from "../../types";
import { Algorithm, Profile, Type } from "../data";
import { GeneratorMetadata } from "../generator-metadata";
const password: GeneratorMetadata<PasswordGeneratorSettings> = deepFreeze({
id: Algorithm.password,
category: Type.password,
type: Type.password,
weight: 100,
i18nKeys: {
name: "password",

View File

@@ -4,17 +4,14 @@ import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state
import { deepFreeze } from "@bitwarden/common/tools/util";
import { UsernameRandomizer } from "../../engine";
import {
CredentialGenerator,
EffUsernameGenerationOptions,
GeneratorDependencyProvider,
} from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, EffUsernameGenerationOptions } from "../../types";
import { Algorithm, Profile, Type } from "../data";
import { GeneratorMetadata } from "../generator-metadata";
const effWordList: GeneratorMetadata<EffUsernameGenerationOptions> = deepFreeze({
id: Algorithm.username,
category: Type.username,
type: Type.username,
weight: 400,
i18nKeys: {
name: "randomWord",

View File

@@ -2,15 +2,15 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyId } from "@bitwarden/common/types/guid";
import { CredentialAlgorithms, PasswordAlgorithms } from "../data";
import { Algorithm, Algorithms, AlgorithmsByType } from "../metadata";
import { availableAlgorithms } from "./available-algorithms-policy";
describe("availableAlgorithmsPolicy", () => {
describe("availableAlgorithms_vNextPolicy", () => {
it("returns all algorithms", () => {
const result = availableAlgorithms([]);
for (const expected of CredentialAlgorithms) {
for (const expected of Algorithms) {
expect(result).toContain(expected);
}
});
@@ -30,7 +30,7 @@ describe("availableAlgorithmsPolicy", () => {
expect(result).toContain(override);
for (const expected of PasswordAlgorithms.filter((a) => a !== override)) {
for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== override)) {
expect(result).not.toContain(expected);
}
});
@@ -50,7 +50,7 @@ describe("availableAlgorithmsPolicy", () => {
expect(result).toContain(override);
for (const expected of PasswordAlgorithms.filter((a) => a !== override)) {
for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== override)) {
expect(result).not.toContain(expected);
}
});
@@ -79,7 +79,7 @@ describe("availableAlgorithmsPolicy", () => {
expect(result).toContain("password");
for (const expected of PasswordAlgorithms.filter((a) => a !== "password")) {
for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== "password")) {
expect(result).not.toContain(expected);
}
});
@@ -97,7 +97,7 @@ describe("availableAlgorithmsPolicy", () => {
const result = availableAlgorithms([policy]);
for (const expected of CredentialAlgorithms) {
for (const expected of Algorithms) {
expect(result).toContain(expected);
}
});
@@ -115,7 +115,7 @@ describe("availableAlgorithmsPolicy", () => {
const result = availableAlgorithms([policy]);
for (const expected of CredentialAlgorithms) {
for (const expected of Algorithms) {
expect(result).toContain(expected);
}
});
@@ -133,7 +133,7 @@ describe("availableAlgorithmsPolicy", () => {
const result = availableAlgorithms([policy]);
for (const expected of CredentialAlgorithms) {
for (const expected of Algorithms) {
expect(result).toContain(expected);
}
});

View File

@@ -1,57 +1,30 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PolicyType } from "@bitwarden/common/admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import {
CredentialAlgorithm as LegacyAlgorithm,
EmailAlgorithms,
PasswordAlgorithms,
UsernameAlgorithms,
} from "..";
import { CredentialAlgorithm } from "../metadata";
import { AlgorithmsByType, CredentialAlgorithm, Type } from "../metadata";
/** Reduces policies to a set of available algorithms
* @param policies the policies to reduce
* @returns the resulting `AlgorithmAvailabilityPolicy`
*/
export function availableAlgorithms(policies: Policy[]): LegacyAlgorithm[] {
export function availableAlgorithms(policies: Policy[]): CredentialAlgorithm[] {
const overridePassword = policies
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
.reduce(
(type, policy) => (type === "password" ? type : (policy.data.overridePasswordType ?? type)),
null as LegacyAlgorithm,
null as CredentialAlgorithm | null,
);
const policy: LegacyAlgorithm[] = [...EmailAlgorithms, ...UsernameAlgorithms];
const policy: CredentialAlgorithm[] = [
...AlgorithmsByType[Type.email],
...AlgorithmsByType[Type.username],
];
if (overridePassword) {
policy.push(overridePassword);
} else {
policy.push(...PasswordAlgorithms);
}
return policy;
}
/** Reduces policies to a set of available algorithms
* @param policies the policies to reduce
* @returns the resulting `AlgorithmAvailabilityPolicy`
*/
export function availableAlgorithms_vNext(policies: Policy[]): CredentialAlgorithm[] {
const overridePassword = policies
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
.reduce(
(type, policy) => (type === "password" ? type : (policy.data.overridePasswordType ?? type)),
null as CredentialAlgorithm,
);
const policy: CredentialAlgorithm[] = [...EmailAlgorithms, ...UsernameAlgorithms];
if (overridePassword) {
policy.push(overridePassword);
} else {
policy.push(...PasswordAlgorithms);
policy.push(...AlgorithmsByType[Type.password]);
}
return policy;

View File

@@ -1,15 +1,25 @@
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
import { Generators } from "../data";
import { BuiltIn, Profile } from "../metadata";
import { PasswordGeneratorSettings } from "../types";
import { AtLeastOne, Zero } from "./constraints";
import { DynamicPasswordPolicyConstraints } from "./dynamic-password-policy-constraints";
const accoutSettings = Generators.password.settings.account as ObjectKey<PasswordGeneratorSettings>;
const defaultOptions = accoutSettings.initial;
const disabledPolicy = Generators.password.policy.disabledValue;
const someConstraints = Generators.password.settings.constraints;
// non-null assertions used because these are always-defined constants
const accoutSettings = BuiltIn.password.profiles[Profile.account]!
.storage as ObjectKey<PasswordGeneratorSettings>;
const defaultOptions = accoutSettings.initial!;
const disabledPolicy = {
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
};
const someConstraints = BuiltIn.password.profiles[Profile.account]!.constraints.default;
describe("DynamicPasswordPolicyConstraints", () => {
describe("constructor", () => {
@@ -33,8 +43,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
expect(constraints.policyInEffect).toBeTruthy();
expect(constraints.lowercase.readonly).toEqual(true);
expect(constraints.lowercase.requiredValue).toEqual(true);
expect(constraints.lowercase?.readonly).toEqual(true);
expect(constraints.lowercase?.requiredValue).toEqual(true);
expect(constraints.minLowercase).toEqual({ min: 1 });
});
@@ -43,8 +53,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
expect(constraints.policyInEffect).toBeTruthy();
expect(constraints.uppercase.readonly).toEqual(true);
expect(constraints.uppercase.requiredValue).toEqual(true);
expect(constraints.uppercase?.readonly).toEqual(true);
expect(constraints.uppercase?.requiredValue).toEqual(true);
expect(constraints.minUppercase).toEqual({ min: 1 });
});
@@ -53,8 +63,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
expect(constraints.policyInEffect).toBeTruthy();
expect(constraints.number.readonly).toEqual(true);
expect(constraints.number.requiredValue).toEqual(true);
expect(constraints.number?.readonly).toEqual(true);
expect(constraints.number?.requiredValue).toEqual(true);
expect(constraints.minNumber).toEqual({ min: 1, max: 9 });
});
@@ -63,8 +73,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
expect(constraints.policyInEffect).toBeTruthy();
expect(constraints.special.readonly).toEqual(true);
expect(constraints.special.requiredValue).toEqual(true);
expect(constraints.special?.readonly).toEqual(true);
expect(constraints.special?.requiredValue).toEqual(true);
expect(constraints.minSpecial).toEqual({ min: 1, max: 9 });
});
@@ -73,8 +83,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
expect(constraints.policyInEffect).toBeTruthy();
expect(constraints.number.readonly).toEqual(true);
expect(constraints.number.requiredValue).toEqual(true);
expect(constraints.number?.readonly).toEqual(true);
expect(constraints.number?.requiredValue).toEqual(true);
expect(constraints.minNumber).toEqual({ min: 2, max: 9 });
});
@@ -83,8 +93,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
expect(constraints.policyInEffect).toBeTruthy();
expect(constraints.special.readonly).toEqual(true);
expect(constraints.special.requiredValue).toEqual(true);
expect(constraints.special?.readonly).toEqual(true);
expect(constraints.special?.requiredValue).toEqual(true);
expect(constraints.minSpecial).toEqual({ min: 2, max: 9 });
});
@@ -140,7 +150,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
const dynamic = new DynamicPasswordPolicyConstraints(
{
...disabledPolicy,
useLowercase,
// the `undefined` case is testing behavior when the type system is bypassed
useLowercase: useLowercase!,
},
someConstraints,
);
@@ -185,7 +196,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
const dynamic = new DynamicPasswordPolicyConstraints(
{
...disabledPolicy,
useUppercase,
// the `undefined` case is testing behavior when the type system is bypassed
useUppercase: useUppercase!,
},
someConstraints,
);

View File

@@ -1,12 +1,32 @@
import { Policies, DefaultPassphraseBoundaries } from "../data";
import { PassphraseGenerationOptions } from "../types";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { deepFreeze } from "@bitwarden/common/tools/util";
import { DefaultPassphraseBoundaries } from "../data";
import {
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
PolicyConfiguration,
} from "../types";
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
const Passphrase: PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions> =
deepFreeze({
type: PolicyType.PasswordGenerator,
disabledValue: Object.freeze({
minNumberWords: 0,
capitalize: false,
includeNumber: false,
}),
combine: passphraseLeastPrivilege,
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
});
describe("Password generator options builder", () => {
describe("constructor()", () => {
it("should set the policy object to a copy of the input policy", () => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.minNumberWords = 10; // arbitrary change for deep equality check
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -16,7 +36,7 @@ describe("Password generator options builder", () => {
});
it("should set default boundaries when a default policy is used", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
expect(builder.numWords).toEqual(DefaultPassphraseBoundaries.numWords);
@@ -25,7 +45,7 @@ describe("Password generator options builder", () => {
it.each([1, 2])(
"should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)",
(minNumberWords) => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.minNumberWords = minNumberWords;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -37,7 +57,7 @@ describe("Password generator options builder", () => {
it.each([8, 12, 18])(
"should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words",
(minNumberWords) => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.minNumberWords = minNumberWords;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -50,7 +70,7 @@ describe("Password generator options builder", () => {
it.each([150, 300, 9000])(
"should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries",
(minNumberWords) => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.minNumberWords = minNumberWords;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -63,14 +83,14 @@ describe("Password generator options builder", () => {
describe("policyInEffect", () => {
it("should return false when the policy has no effect", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(false);
});
it("should return true when the policy has a numWords greater than the default boundary", () => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.minNumberWords = DefaultPassphraseBoundaries.numWords.min + 1;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -78,7 +98,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has capitalize enabled", () => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.capitalize = true;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -86,7 +106,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has includeNumber enabled", () => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.includeNumber = true;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -98,7 +118,7 @@ describe("Password generator options builder", () => {
// All tests should freeze the options to ensure they are not modified
it("should set `capitalize` to `false` when the policy does not override it", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({});
@@ -108,7 +128,7 @@ describe("Password generator options builder", () => {
});
it("should set `capitalize` to `true` when the policy overrides it", () => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.capitalize = true;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ capitalize: false });
@@ -119,7 +139,7 @@ describe("Password generator options builder", () => {
});
it("should set `includeNumber` to false when the policy does not override it", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({});
@@ -129,7 +149,7 @@ describe("Password generator options builder", () => {
});
it("should set `includeNumber` to true when the policy overrides it", () => {
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
const policy: any = Object.assign({}, Passphrase.disabledValue);
policy.includeNumber = true;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ includeNumber: false });
@@ -140,7 +160,7 @@ describe("Password generator options builder", () => {
});
it("should set `numWords` to the minimum value when it isn't supplied", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({});
@@ -154,7 +174,7 @@ describe("Password generator options builder", () => {
(numWords) => {
expect(numWords).toBeLessThan(DefaultPassphraseBoundaries.numWords.min);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ numWords });
@@ -170,7 +190,7 @@ describe("Password generator options builder", () => {
expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min);
expect(numWords).toBeLessThanOrEqual(DefaultPassphraseBoundaries.numWords.max);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ numWords });
@@ -185,7 +205,7 @@ describe("Password generator options builder", () => {
(numWords) => {
expect(numWords).toBeGreaterThan(DefaultPassphraseBoundaries.numWords.max);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ numWords });
@@ -196,7 +216,7 @@ describe("Password generator options builder", () => {
);
it("should preserve unknown properties", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",
@@ -214,7 +234,7 @@ describe("Password generator options builder", () => {
// All tests should freeze the options to ensure they are not modified
it("should return the input options without altering them", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ wordSeparator: "%" });
@@ -224,7 +244,7 @@ describe("Password generator options builder", () => {
});
it("should set `wordSeparator` to '-' when it isn't supplied and there is no policy override", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({});
@@ -234,7 +254,7 @@ describe("Password generator options builder", () => {
});
it("should leave `wordSeparator` as the empty string '' when it is the empty string", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ wordSeparator: "" });
@@ -244,7 +264,7 @@ describe("Password generator options builder", () => {
});
it("should preserve unknown properties", () => {
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const policy = Object.assign({}, Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",

View File

@@ -4,8 +4,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyId } from "@bitwarden/common/types/guid";
import { Policies } from "../data";
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
function createPolicy(
@@ -22,21 +20,27 @@ function createPolicy(
});
}
const disabledValue = Object.freeze({
minNumberWords: 0,
capitalize: false,
includeNumber: false,
});
describe("passphraseLeastPrivilege", () => {
it("should return the accumulator when the policy type does not apply", () => {
const policy = createPolicy({}, PolicyType.RequireSso);
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
const result = passphraseLeastPrivilege(disabledValue, policy);
expect(result).toEqual(Policies.Passphrase.disabledValue);
expect(result).toEqual(disabledValue);
});
it("should return the accumulator when the policy is not enabled", () => {
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
const result = passphraseLeastPrivilege(disabledValue, policy);
expect(result).toEqual(Policies.Passphrase.disabledValue);
expect(result).toEqual(disabledValue);
});
it.each([
@@ -46,8 +50,8 @@ describe("passphraseLeastPrivilege", () => {
])("should take the %p from the policy", (input, value) => {
const policy = createPolicy({ [input]: value });
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
const result = passphraseLeastPrivilege(disabledValue, policy);
expect(result).toEqual({ ...Policies.Passphrase.disabledValue, [input]: value });
expect(result).toEqual({ ...disabledValue, [input]: value });
});
});

View File

@@ -1,4 +1,4 @@
import { Generators } from "../data";
import { BuiltIn, Profile } from "../metadata";
import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints";
@@ -9,8 +9,12 @@ const SomeSettings = {
wordSeparator: "-",
};
const disabledPolicy = Generators.passphrase.policy.disabledValue;
const someConstraints = Generators.passphrase.settings.constraints;
const disabledPolicy = {
minNumberWords: 0,
capitalize: false,
includeNumber: false,
};
const someConstraints = BuiltIn.passphrase.profiles[Profile.account]!.constraints.default;
describe("PassphrasePolicyConstraints", () => {
describe("constructor", () => {
@@ -61,7 +65,7 @@ describe("PassphrasePolicyConstraints", () => {
expect(constraints.policyInEffect).toBeTruthy();
expect(constraints.numWords).toMatchObject({
min: 10,
max: someConstraints.numWords.max,
max: someConstraints.numWords?.max,
});
});
});
@@ -84,8 +88,8 @@ describe("PassphrasePolicyConstraints", () => {
});
it.each([
[1, someConstraints.numWords.min, 3, someConstraints.numWords.max],
[21, someConstraints.numWords.min, 20, someConstraints.numWords.max],
[1, someConstraints.numWords?.min, 3, someConstraints.numWords?.max],
[21, someConstraints.numWords?.min, 20, someConstraints.numWords?.max],
])(
`fits numWords (=%p) within the default bounds (%p <= %p <= %p)`,
(value, _, expected, __) => {
@@ -98,8 +102,8 @@ describe("PassphrasePolicyConstraints", () => {
);
it.each([
[1, 6, 6, someConstraints.numWords.max],
[21, 20, 20, someConstraints.numWords.max],
[1, 6, 6, someConstraints.numWords?.max],
[21, 20, 20, someConstraints.numWords?.max],
])(
"fits numWords (=%p) within the policy bounds (%p <= %p <= %p)",
(value, minNumberWords, expected, _) => {

View File

@@ -1,14 +1,34 @@
import { DefaultPasswordBoundaries, Policies } from "../data";
import { PasswordGenerationOptions } from "../types";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { deepFreeze } from "@bitwarden/common/tools/util";
import { DefaultPasswordBoundaries } from "../data";
import { PasswordGenerationOptions, PasswordGeneratorPolicy, PolicyConfiguration } from "../types";
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
import { passwordLeastPrivilege } from "./password-least-privilege";
const Password: PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions> =
deepFreeze({
type: PolicyType.PasswordGenerator,
disabledValue: {
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
},
combine: passwordLeastPrivilege,
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
});
describe("Password generator options builder", () => {
const defaultOptions = Object.freeze({ minLength: 0 });
describe("constructor()", () => {
it("should set the policy object to a copy of the input policy", () => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.minLength = 10; // arbitrary change for deep equality check
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -18,7 +38,7 @@ describe("Password generator options builder", () => {
});
it("should set default boundaries when a default policy is used", () => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -32,7 +52,7 @@ describe("Password generator options builder", () => {
(minLength) => {
expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min);
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.minLength = minLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -47,7 +67,7 @@ describe("Password generator options builder", () => {
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min);
expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max);
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.minLength = expectedLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -62,7 +82,7 @@ describe("Password generator options builder", () => {
(expectedLength) => {
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max);
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.minLength = expectedLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -78,7 +98,7 @@ describe("Password generator options builder", () => {
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min);
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max);
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.numberCount = expectedMinDigits;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -93,7 +113,7 @@ describe("Password generator options builder", () => {
(expectedMinDigits) => {
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max);
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.numberCount = expectedMinDigits;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -113,7 +133,7 @@ describe("Password generator options builder", () => {
DefaultPasswordBoundaries.minSpecialCharacters.max,
);
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.specialCount = expectedSpecialCharacters;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -132,7 +152,7 @@ describe("Password generator options builder", () => {
DefaultPasswordBoundaries.minSpecialCharacters.max,
);
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.specialCount = expectedSpecialCharacters;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -151,7 +171,7 @@ describe("Password generator options builder", () => {
(expectedLength, numberCount, specialCount) => {
expect(expectedLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.numberCount = numberCount;
policy.specialCount = specialCount;
@@ -164,14 +184,14 @@ describe("Password generator options builder", () => {
describe("policyInEffect", () => {
it("should return false when the policy has no effect", () => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(false);
});
it("should return true when the policy has a minlength greater than the default boundary", () => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.minLength = DefaultPasswordBoundaries.length.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -179,7 +199,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has a number count greater than the default boundary", () => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.numberCount = DefaultPasswordBoundaries.minDigits.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -187,7 +207,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has a special character count greater than the default boundary", () => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.specialCount = DefaultPasswordBoundaries.minSpecialCharacters.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -195,7 +215,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has uppercase enabled", () => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useUppercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -203,7 +223,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has lowercase enabled", () => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useLowercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -211,7 +231,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has numbers enabled", () => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useNumbers = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -219,7 +239,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has special characters enabled", () => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useSpecial = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -237,7 +257,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'",
(expectedUppercase, uppercase) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useUppercase = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, uppercase });
@@ -251,7 +271,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
(uppercase) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useUppercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, uppercase });
@@ -269,7 +289,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'",
(expectedLowercase, lowercase) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useLowercase = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, lowercase });
@@ -283,7 +303,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
(lowercase) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useLowercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, lowercase });
@@ -301,7 +321,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'",
(expectedNumber, number) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useNumbers = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number });
@@ -315,7 +335,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
(number) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useNumbers = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number });
@@ -333,7 +353,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'",
(expectedSpecial, special) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useSpecial = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special });
@@ -347,7 +367,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
(special) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.useSpecial = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special });
@@ -361,7 +381,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
(length) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeLessThan(builder.length.min);
@@ -376,7 +396,7 @@ describe("Password generator options builder", () => {
it.each([5, 10, 50, 100, 128])(
"should not change `options.length` (= %i) when it is within the boundaries",
(length) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeGreaterThanOrEqual(builder.length.min);
expect(length).toBeLessThanOrEqual(builder.length.max);
@@ -392,7 +412,7 @@ describe("Password generator options builder", () => {
it.each([129, 500, 9000])(
"should set `options.length` (= %i) to the maximum length when it is exceeded",
(length) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeGreaterThan(builder.length.max);
@@ -414,7 +434,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0",
(expectedNumber, minNumber) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, minNumber });
@@ -425,7 +445,7 @@ describe("Password generator options builder", () => {
);
it("should set `options.minNumber` to the minimum value when `options.number` is true", () => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number: true });
@@ -435,7 +455,7 @@ describe("Password generator options builder", () => {
});
it("should set `options.minNumber` to 0 when `options.number` is false", () => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number: false });
@@ -447,7 +467,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
(minNumber) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.numberCount = 5; // arbitrary value greater than minNumber
expect(minNumber).toBeLessThan(policy.numberCount);
@@ -463,7 +483,7 @@ describe("Password generator options builder", () => {
it.each([1, 3, 5, 7, 9])(
"should not change `options.minNumber` (= %i) when it is within the boundaries",
(minNumber) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
@@ -479,7 +499,7 @@ describe("Password generator options builder", () => {
it.each([10, 20, 400])(
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
(minNumber) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minNumber).toBeGreaterThan(builder.minDigits.max);
@@ -501,7 +521,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0",
(expectedSpecial, minSpecial) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, minSpecial });
@@ -512,7 +532,7 @@ describe("Password generator options builder", () => {
);
it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special: true });
@@ -522,7 +542,7 @@ describe("Password generator options builder", () => {
});
it("should set `options.minSpecial` to 0 when `options.special` is false", () => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special: false });
@@ -534,7 +554,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
(minSpecial) => {
const policy: any = Object.assign({}, Policies.Password.disabledValue);
const policy: any = Object.assign({}, Password.disabledValue);
policy.specialCount = 5; // arbitrary value greater than minSpecial
expect(minSpecial).toBeLessThan(policy.specialCount);
@@ -550,7 +570,7 @@ describe("Password generator options builder", () => {
it.each([1, 3, 5, 7, 9])(
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
(minSpecial) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
@@ -566,7 +586,7 @@ describe("Password generator options builder", () => {
it.each([10, 20, 400])(
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
(minSpecial) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
@@ -579,7 +599,7 @@ describe("Password generator options builder", () => {
);
it("should preserve unknown properties", () => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",
@@ -602,7 +622,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
(expectedMinLowercase, lowercase) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ lowercase, ...defaultOptions });
@@ -618,7 +638,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minUppercase === %i` when `options.uppercase` is %s",
(expectedMinUppercase, uppercase) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ uppercase, ...defaultOptions });
@@ -634,7 +654,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set",
(expectedMinNumber, number) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ number, ...defaultOptions });
@@ -652,7 +672,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set",
(expectedNumber, minNumber) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ minNumber, ...defaultOptions });
@@ -668,7 +688,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set",
(special, expectedMinSpecial) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ special, ...defaultOptions });
@@ -686,7 +706,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set",
(minSpecial, expectedSpecial) => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ minSpecial, ...defaultOptions });
@@ -707,7 +727,7 @@ describe("Password generator options builder", () => {
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
expect(sumOfMinimums).toBeLessThan(DefaultPasswordBoundaries.length.min);
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
minLowercase,
@@ -732,7 +752,7 @@ describe("Password generator options builder", () => {
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
minLowercase,
@@ -749,7 +769,7 @@ describe("Password generator options builder", () => {
);
it("should preserve unknown properties", () => {
const policy = Object.assign({}, Policies.Password.disabledValue);
const policy = Object.assign({}, Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",

View File

@@ -4,8 +4,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyId } from "@bitwarden/common/types/guid";
import { Policies } from "../data";
import { passwordLeastPrivilege } from "./password-least-privilege";
function createPolicy(
@@ -22,21 +20,31 @@ function createPolicy(
});
}
const disabledValue = Object.freeze({
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
});
describe("passwordLeastPrivilege", () => {
it("should return the accumulator when the policy type does not apply", () => {
const policy = createPolicy({}, PolicyType.RequireSso);
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
const result = passwordLeastPrivilege(disabledValue, policy);
expect(result).toEqual(Policies.Password.disabledValue);
expect(result).toEqual(disabledValue);
});
it("should return the accumulator when the policy is not enabled", () => {
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
const result = passwordLeastPrivilege(disabledValue, policy);
expect(result).toEqual(Policies.Password.disabledValue);
expect(result).toEqual(disabledValue);
});
it.each([
@@ -50,8 +58,8 @@ describe("passwordLeastPrivilege", () => {
])("should take the %p from the policy", (input, value, expected) => {
const policy = createPolicy({ [input]: value });
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
const result = passwordLeastPrivilege(disabledValue, policy);
expect(result).toEqual({ ...Policies.Password.disabledValue, [expected]: value });
expect(result).toEqual({ ...disabledValue, [expected]: value });
});
});

View File

@@ -0,0 +1,14 @@
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
import { GeneratorDependencyProvider } from "./generator-dependency-provider";
import { GeneratorMetadataProvider } from "./generator-metadata-provider";
import { GeneratorProfileProvider } from "./generator-profile-provider";
// FIXME: find a better way to manage common dependencies than smashing them all
// together into a mega-type.
export type CredentialGeneratorProviders = {
readonly userState: UserStateSubjectDependencyProvider;
readonly generator: GeneratorDependencyProvider;
readonly profile: GeneratorProfileProvider;
readonly metadata: GeneratorMetadataProvider;
};

View File

@@ -0,0 +1,83 @@
import { AlgorithmsByType, Type } from "../metadata";
import { CredentialPreference } from "../types";
import { PREFERENCES } from "./credential-preferences";
const SomeCredentialPreferences: CredentialPreference = Object.freeze({
email: Object.freeze({
algorithm: AlgorithmsByType[Type.email][0],
updated: new Date(0),
}),
password: Object.freeze({
algorithm: AlgorithmsByType[Type.password][0],
updated: new Date(0),
}),
username: Object.freeze({
algorithm: AlgorithmsByType[Type.username][0],
updated: new Date(0),
}),
});
describe("PREFERENCES", () => {
describe("deserializer", () => {
it.each([[null], [undefined]])("creates new preferences (= %p)", (value) => {
// this case tests what happens when the type system is bypassed
const result = PREFERENCES.deserializer(value!);
expect(result).toEqual(SomeCredentialPreferences);
});
it("fills missing password preferences", () => {
const input: any = { ...SomeCredentialPreferences };
delete input.password;
const result = PREFERENCES.deserializer(input);
expect(result).toEqual(SomeCredentialPreferences);
});
it("fills missing email preferences", () => {
const input: any = { ...SomeCredentialPreferences };
delete input.email;
const result = PREFERENCES.deserializer(input);
expect(result).toEqual(SomeCredentialPreferences);
});
it("fills missing username preferences", () => {
const input: any = { ...SomeCredentialPreferences };
delete input.username;
const result = PREFERENCES.deserializer(input);
expect(result).toEqual(SomeCredentialPreferences);
});
it("converts string fields to Dates", () => {
const input: any = structuredClone(SomeCredentialPreferences);
input.email.updated = "1970-01-01T00:00:00.100Z";
input.password.updated = "1970-01-01T00:00:00.200Z";
input.username.updated = "1970-01-01T00:00:00.300Z";
const result = PREFERENCES.deserializer(input);
expect(result?.email.updated).toEqual(new Date(100));
expect(result?.password.updated).toEqual(new Date(200));
expect(result?.username.updated).toEqual(new Date(300));
});
it("converts number fields to Dates", () => {
const input: any = structuredClone(SomeCredentialPreferences);
input.email.updated = 100;
input.password.updated = 200;
input.username.updated = 300;
const result = PREFERENCES.deserializer(input);
expect(result?.email.updated).toEqual(new Date(100));
expect(result?.password.updated).toEqual(new Date(200));
expect(result?.username.updated).toEqual(new Date(300));
});
});
});

View File

@@ -0,0 +1,28 @@
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { AlgorithmsByType, CredentialType } from "../metadata";
import { CredentialPreference } from "../types";
/** plaintext password generation options */
export const PREFERENCES = new UserKeyDefinition<CredentialPreference>(
GENERATOR_DISK,
"credentialPreferences",
{
deserializer: (value) => {
const result = (value as any) ?? {};
for (const key in AlgorithmsByType) {
const type = key as CredentialType;
if (result[type]) {
result[type].updated = new Date(result[type].updated);
} else {
const [algorithm] = AlgorithmsByType[type];
result[type] = { algorithm, updated: new Date() };
}
}
return result;
},
clearOn: ["logout"],
},
);

View File

@@ -0,0 +1,12 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
import { Randomizer } from "../abstractions";
export type GeneratorDependencyProvider = {
randomizer: Randomizer;
client: RestClient;
// FIXME: introduce `I18nKeyOrLiteral` into forwarder
// structures and remove this dependency
i18nService: I18nService;
};

View File

@@ -5,6 +5,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { FakeAccountService, FakeStateProvider } from "@bitwarden/common/spec";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
import {
@@ -23,7 +24,6 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat
import { deepFreeze } from "@bitwarden/common/tools/util";
import { UserId } from "@bitwarden/common/types/guid";
import { FakeAccountService, FakeStateProvider } from "../../../../../common/spec";
import { Algorithm, AlgorithmsByType, CredentialAlgorithm, Type, Types } from "../metadata";
import catchall from "../metadata/email/catchall";
import plusAddress from "../metadata/email/plus-address";

View File

@@ -29,7 +29,7 @@ import {
Algorithms,
Types,
} from "../metadata";
import { availableAlgorithms_vNext } from "../policies/available-algorithms-policy";
import { availableAlgorithms } from "../policies/available-algorithms-policy";
import { CredentialPreference } from "../types";
import {
AlgorithmRequest,
@@ -146,7 +146,7 @@ export class GeneratorMetadataProvider {
const available$ = id$.pipe(
switchMap((id) => {
const policies$ = this.application.policy.getAll$(PolicyType.PasswordGenerator, id).pipe(
map((p) => availableAlgorithms_vNext(p).filter((a) => this._metadata.has(a))),
map((p) => availableAlgorithms(p).filter((a) => this._metadata.has(a))),
map((p) => new Set(p)),
// complete policy emissions otherwise `switchMap` holds `available$` open indefinitely
takeUntil(anyComplete(id$)),

View File

@@ -6,6 +6,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { FakeStateProvider, FakeAccountService, awaitAsync } from "@bitwarden/common/spec";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
@@ -15,7 +16,6 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat
import { StateConstraints } from "@bitwarden/common/tools/types";
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
import { CoreProfileMetadata, ProfileContext } from "../metadata/profile-metadata";
import { GeneratorConstraints } from "../types";

View File

@@ -0,0 +1,4 @@
export { CredentialGeneratorProviders } from "./credential-generator-providers";
export { GeneratorMetadataProvider } from "./generator-metadata-provider";
export { GeneratorProfileProvider } from "./generator-profile-provider";
export { GeneratorDependencyProvider } from "./generator-dependency-provider";

View File

@@ -18,20 +18,6 @@ export function mapPolicyToEvaluator<Policy, Evaluator>(
);
}
/** Maps an administrative console policy to constraints using the provided configuration.
* @param configuration the configuration that constructs the constraints.
*/
export function mapPolicyToConstraints<Policy, Evaluator>(
configuration: PolicyConfiguration<Policy, Evaluator>,
email: string,
) {
return pipe(
reduceCollection(configuration.combine, configuration.disabledValue),
distinctIfShallowMatch(),
map((policy) => configuration.toConstraints(policy, email)),
);
}
/** Constructs a method that maps a policy to the default (no-op) policy. */
export function newDefaultEvaluator<Target>() {
return () => {

View File

@@ -1,294 +1,180 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { concatMap, distinctUntilChanged, map, Observable, switchMap, takeUntil } from "rxjs";
import { Simplify } from "type-fest";
import {
concatMap,
distinctUntilChanged,
filter,
map,
of,
shareReplay,
switchMap,
takeUntil,
tap,
} from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies";
import { IntegrationMetadata } from "@bitwarden/common/tools/integration";
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
import { VendorId } from "@bitwarden/common/tools/extension";
import { SemanticLogger } from "@bitwarden/common/tools/log";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { anyComplete, withLatestReady } from "@bitwarden/common/tools/rx";
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
import { Randomizer } from "../abstractions";
import {
Generators,
getForwarderConfiguration,
Integrations,
toCredentialGeneratorConfiguration,
} from "../data";
import { availableAlgorithms } from "../policies/available-algorithms-policy";
import { mapPolicyToConstraints } from "../rx";
import { CredentialGeneratorService } from "../abstractions";
import {
CredentialAlgorithm,
CredentialCategories,
CredentialCategory,
AlgorithmInfo,
CredentialPreference,
isForwarderIntegration,
ForwarderIntegration,
GenerateRequest,
} from "../types";
import {
CredentialGeneratorConfiguration as Configuration,
CredentialGeneratorInfo,
GeneratorDependencyProvider,
} from "../types/credential-generator-configuration";
import { GeneratorConstraints } from "../types/generator-constraints";
Profile,
GeneratorMetadata,
GeneratorProfile,
isForwarderProfile,
toVendorId,
CredentialType,
} from "../metadata";
import { CredentialGeneratorProviders } from "../providers";
import { GenerateRequest } from "../types";
import { isAlgorithmRequest, isTypeRequest } from "../types/metadata-request";
import { PREFERENCES } from "./credential-preferences";
type Generate$Dependencies = Simplify<
OnDependency<GenerateRequest> & BoundDependency<"account", Account>
>;
export class CredentialGeneratorService {
export class DefaultCredentialGeneratorService implements CredentialGeneratorService {
/** Instantiate the `DefaultCredentialGeneratorService`.
* @param provide application services required by the credential generator.
* @param system low-level services required by the credential generator.
*/
constructor(
private readonly randomizer: Randomizer,
private readonly policyService: PolicyService,
private readonly apiService: ApiService,
private readonly i18nService: I18nService,
private readonly providers: UserStateSubjectDependencyProvider,
) {}
private getDependencyProvider(): GeneratorDependencyProvider {
return {
client: new RestClient(this.apiService, this.i18nService),
i18nService: this.i18nService,
randomizer: this.randomizer,
};
private readonly provide: CredentialGeneratorProviders,
private readonly system: SystemServiceProvider,
) {
this.log = system.log({ type: "CredentialGeneratorService" });
}
// FIXME: the rxjs methods of this service can be a lot more resilient if
// `Subjects` are introduced where sharing occurs
private readonly log: SemanticLogger;
/** Generates a stream of credentials
* @param configuration determines which generator's settings are loaded
* @param dependencies.on$ Required. A new credential is emitted when this emits.
*/
generate$<Settings extends object, Policy>(
configuration: Readonly<Configuration<Settings, Policy>>,
dependencies: Generate$Dependencies,
) {
const engine = configuration.engine.create(this.getDependencyProvider());
const settings$ = this.settings$(configuration, dependencies);
generate$(dependencies: OnDependency<GenerateRequest> & BoundDependency<"account", Account>) {
// `on$` is partitioned into several streams so that the generator
// engine and settings refresh only when their respective inputs change
const on$ = dependencies.on$.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const account$ = dependencies.account$.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
// load algorithm metadata
const algorithm$ = on$.pipe(
switchMap((requested) => {
if (isAlgorithmRequest(requested)) {
return of(requested.algorithm);
} else if (isTypeRequest(requested)) {
return this.provide.metadata.preference$(requested.type, { account$ });
} else {
this.log.panic(requested, "algorithm or category required");
}
}),
filter((algorithm): algorithm is CredentialAlgorithm => !!algorithm),
map((algorithm) => this.provide.metadata.metadata(algorithm)),
distinctUntilChanged((previous, current) => previous.id === current.id),
);
// load the active profile's algorithm settings
const settings$ = on$.pipe(
map((request) => request.profile ?? Profile.account),
distinctUntilChanged(),
withLatestReady(algorithm$),
switchMap(([profile, meta]) => this.settings(meta, { account$ }, profile)),
);
// load the algorithm's engine
const engine$ = algorithm$.pipe(
tap((meta) => this.log.info({ algorithm: meta.id }, "engine selected")),
map((meta) => meta.engine.create(this.provide.generator)),
);
// generation proper
const generate$ = dependencies.on$.pipe(
const generate$ = on$.pipe(
withLatestReady(engine$),
withLatestReady(settings$),
concatMap(([request, settings]) => engine.generate(request, settings)),
concatMap(([[request, engine], settings]) => engine.generate(request, settings)),
takeUntil(anyComplete([settings$])),
);
return generate$;
}
/** Emits metadata concerning the provided generation algorithms
* @param category the category or categories of interest
* @param dependences.account$ algorithms are filtered to only
* those matching the provided account's policy.
* @returns An observable that emits algorithm metadata.
*/
algorithms$(
category: CredentialCategory,
dependencies: BoundDependency<"account", Account>,
): Observable<AlgorithmInfo[]>;
algorithms$(
category: CredentialCategory[],
dependencies: BoundDependency<"account", Account>,
): Observable<AlgorithmInfo[]>;
algorithms$(
category: CredentialCategory | CredentialCategory[],
algorithms$(type: CredentialType, dependencies: BoundDependency<"account", Account>) {
return this.provide.metadata
.algorithms$({ type }, dependencies)
.pipe(map((algorithms) => algorithms.map((a) => this.algorithm(a))));
}
algorithms(type: CredentialType | CredentialType[]) {
const types: CredentialType[] = Array.isArray(type) ? type : [type];
const algorithms = types
.flatMap((type) => this.provide.metadata.algorithms({ type }))
.map((algorithm) => this.algorithm(algorithm));
return algorithms;
}
algorithm(id: CredentialAlgorithm) {
const metadata = this.provide.metadata.metadata(id);
if (!metadata) {
this.log.panic({ algorithm: id }, "invalid credential algorithm");
}
return metadata;
}
forwarder(id: VendorId) {
const metadata = this.provide.metadata.metadata({ forwarder: id });
if (!metadata) {
this.log.panic({ algorithm: id }, "invalid vendor");
}
return metadata;
}
preferences(dependencies: BoundDependency<"account", Account>) {
return this.provide.metadata.preferences(dependencies);
}
settings<Settings extends object>(
metadata: Readonly<GeneratorMetadata<Settings>>,
dependencies: BoundDependency<"account", Account>,
profile: GeneratorProfile = Profile.account,
) {
// any cast required here because TypeScript fails to bind `category`
// to the union-typed overload of `algorithms`.
const algorithms = this.algorithms(category as any);
const activeProfile = metadata.profiles[profile];
if (!activeProfile) {
this.log.panic(
{ algorithm: metadata.id, profile },
"failed to load settings; profile metadata not found",
);
}
// apply policy
const algorithms$ = dependencies.account$.pipe(
distinctUntilChanged(),
switchMap((account) => {
const policies$ = this.policyService.getAll$(PolicyType.PasswordGenerator, account.id).pipe(
map((p) => new Set(availableAlgorithms(p))),
// complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely
takeUntil(anyComplete(dependencies.account$)),
let settings: UserStateSubject<Settings>;
if (isForwarderProfile(activeProfile)) {
const vendor = toVendorId(metadata.id);
if (!vendor) {
this.log.panic(
{ algorithm: metadata.id, profile },
"failed to load extension profile; vendor not specified",
);
return policies$;
}),
map((available) => {
const filtered = algorithms.filter(
(c) => isForwarderIntegration(c.id) || available.has(c.id),
);
return filtered;
}),
);
return algorithms$;
}
/** Lists metadata for the algorithms in a credential category
* @param category the category or categories of interest
* @returns A list containing the requested metadata.
*/
algorithms(category: CredentialCategory): AlgorithmInfo[];
algorithms(category: CredentialCategory[]): AlgorithmInfo[];
algorithms(category: CredentialCategory | CredentialCategory[]): AlgorithmInfo[] {
const categories: CredentialCategory[] = Array.isArray(category) ? category : [category];
const algorithms = categories
.flatMap((c) => CredentialCategories[c] as CredentialAlgorithm[])
.map((id) => this.algorithm(id))
.filter((info) => info !== null);
const forwarders = Object.keys(Integrations)
.map((key: keyof typeof Integrations) => {
const forwarder: ForwarderIntegration = { forwarder: Integrations[key].id };
return this.algorithm(forwarder);
})
.filter((forwarder) => categories.includes(forwarder.category));
return algorithms.concat(forwarders);
}
/** Look up the metadata for a specific generator algorithm
* @param id identifies the algorithm
* @returns the requested metadata, or `null` if the metadata wasn't found.
*/
algorithm(id: CredentialAlgorithm): AlgorithmInfo {
let generator: CredentialGeneratorInfo = null;
let integration: IntegrationMetadata = null;
if (isForwarderIntegration(id)) {
const forwarderConfig = getForwarderConfiguration(id.forwarder);
integration = forwarderConfig;
if (forwarderConfig) {
generator = toCredentialGeneratorConfiguration(forwarderConfig);
}
this.log.info({ profile, vendor, site: activeProfile.site }, "loading extension profile");
settings = this.system.extension.settings(activeProfile, vendor, dependencies);
} else {
generator = Generators[id];
this.log.info({ profile, algorithm: metadata.id }, "loading generator profile");
settings = this.provide.profile.settings(activeProfile, dependencies);
}
if (!generator) {
throw new Error(`Invalid credential algorithm: ${JSON.stringify(id)}`);
}
const info: AlgorithmInfo = {
id: generator.id,
category: generator.category,
name: integration ? integration.name : this.i18nService.t(generator.nameKey),
generate: this.i18nService.t(generator.generateKey),
onGeneratedMessage: this.i18nService.t(generator.onGeneratedMessageKey),
credentialType: this.i18nService.t(generator.credentialTypeKey),
copy: this.i18nService.t(generator.copyKey),
useGeneratedValue: this.i18nService.t(generator.useGeneratedValueKey),
onlyOnRequest: generator.onlyOnRequest,
request: generator.request,
};
if (generator.descriptionKey) {
info.description = this.i18nService.t(generator.descriptionKey);
}
return info;
return settings;
}
/** Get the settings for the provided configuration
* @param configuration determines which generator's settings are loaded
* @param dependencies.account$ identifies the account to which the settings are bound.
* @returns an observable that emits settings
* @remarks the observable enforces policies on the settings
*/
settings$<Settings extends object, Policy>(
configuration: Configuration<Settings, Policy>,
policy$<Settings>(
metadata: Readonly<GeneratorMetadata<Settings>>,
dependencies: BoundDependency<"account", Account>,
profile: GeneratorProfile = Profile.account,
) {
const constraints$ = this.policy$(configuration, dependencies);
const activeProfile = metadata.profiles[profile];
if (!activeProfile) {
this.log.panic(
{ algorithm: metadata.id, profile },
"failed to load policy; profile metadata not found",
);
}
const settings = new UserStateSubject(configuration.settings.account, this.providers, {
constraints$,
account$: dependencies.account$,
});
const settings$ = settings.pipe(
map((settings) => settings ?? structuredClone(configuration.settings.initial)),
);
return settings$;
}
/** Get a subject bound to credential generator preferences.
* @param dependencies.account$ identifies the account to which the preferences are bound
* @returns a subject bound to the user's preferences
* @remarks Preferences determine which algorithms are used when generating a
* credential from a credential category (e.g. `PassX` or `Username`). Preferences
* should not be used to hold navigation history. Use @bitwarden/generator-navigation
* instead.
*/
preferences(
dependencies: BoundDependency<"account", Account>,
): UserStateSubject<CredentialPreference> {
// FIXME: enforce policy
const subject = new UserStateSubject(PREFERENCES, this.providers, dependencies);
return subject;
}
/** Get a subject bound to a specific user's settings
* @param configuration determines which generator's settings are loaded
* @param dependencies.account$ identifies the account to which the settings are bound
* @returns a subject bound to the requested user's generator settings
* @remarks the subject enforces policy for the settings
*/
settings<Settings extends object, Policy>(
configuration: Readonly<Configuration<Settings, Policy>>,
dependencies: BoundDependency<"account", Account>,
) {
const constraints$ = this.policy$(configuration, dependencies);
const subject = new UserStateSubject(configuration.settings.account, this.providers, {
constraints$,
account$: dependencies.account$,
});
return subject;
}
/** Get the policy constraints for the provided configuration
* @param dependencies.account$ determines which user's policy is loaded
* @returns an observable that emits the policy once `dependencies.account$`
* and the policy become available.
*/
policy$<Settings, Policy>(
configuration: Configuration<Settings, Policy>,
dependencies: BoundDependency<"account", Account>,
): Observable<GeneratorConstraints<Settings>> {
const constraints$ = dependencies.account$.pipe(
map((account) => {
if (account.emailVerified) {
return { userId: account.id, email: account.email };
}
return { userId: account.id, email: null };
}),
switchMap(({ userId, email }) => {
// complete policy emissions otherwise `switchMap` holds `policies$` open indefinitely
const policies$ = this.policyService
.getAll$(configuration.policy.type, userId)
.pipe(
mapPolicyToConstraints(configuration.policy, email),
takeUntil(anyComplete(dependencies.account$)),
);
return policies$;
}),
);
return constraints$;
return this.provide.profile.constraints$(activeProfile, dependencies);
}
}

View File

@@ -1,53 +0,0 @@
import { DefaultCredentialPreferences } from "../data";
import { PREFERENCES } from "./credential-preferences";
describe("PREFERENCES", () => {
describe("deserializer", () => {
it.each([[null], [undefined]])("creates new preferences (= %p)", (value) => {
const result = PREFERENCES.deserializer(value);
expect(result).toEqual(DefaultCredentialPreferences);
});
it("fills missing password preferences", () => {
const input = { ...DefaultCredentialPreferences };
delete input.password;
const result = PREFERENCES.deserializer(input as any);
expect(result).toEqual(DefaultCredentialPreferences);
});
it("fills missing email preferences", () => {
const input = { ...DefaultCredentialPreferences };
delete input.email;
const result = PREFERENCES.deserializer(input as any);
expect(result).toEqual(DefaultCredentialPreferences);
});
it("fills missing username preferences", () => {
const input = { ...DefaultCredentialPreferences };
delete input.username;
const result = PREFERENCES.deserializer(input as any);
expect(result).toEqual(DefaultCredentialPreferences);
});
it("converts updated fields to Dates", () => {
const input = structuredClone(DefaultCredentialPreferences);
input.email.updated = "1970-01-01T00:00:00.100Z" as any;
input.password.updated = "1970-01-01T00:00:00.200Z" as any;
input.username.updated = "1970-01-01T00:00:00.300Z" as any;
const result = PREFERENCES.deserializer(input as any);
expect(result.email.updated).toEqual(new Date(100));
expect(result.password.updated).toEqual(new Date(200));
expect(result.username.updated).toEqual(new Date(300));
});
});
});

View File

@@ -1,30 +0,0 @@
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { DefaultCredentialPreferences } from "../data";
import { CredentialPreference } from "../types";
/** plaintext password generation options */
export const PREFERENCES = new UserKeyDefinition<CredentialPreference>(
GENERATOR_DISK,
"credentialPreferences",
{
deserializer: (value) => {
const result = (value as any) ?? {};
for (const key in DefaultCredentialPreferences) {
// bind `key` to `category` to transmute the type
const category: keyof typeof DefaultCredentialPreferences = key as any;
const preference = result[category] ?? { ...DefaultCredentialPreferences[category] };
if (typeof preference.updated === "string") {
preference.updated = new Date(preference.updated);
}
result[category] = preference;
}
return result;
},
clearOn: ["logout"],
},
);

View File

@@ -1,2 +1,2 @@
export { DefaultGeneratorService } from "./default-generator.service";
export { CredentialGeneratorService } from "./credential-generator.service";
export { DefaultCredentialGeneratorService } from "./credential-generator.service";

View File

@@ -2,7 +2,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { StateProvider } from "@bitwarden/common/platform/state";
import { GeneratorStrategy } from "../abstractions";
import { DefaultEffUsernameOptions, UsernameDigits } from "../data";
import { DefaultEffUsernameOptions } from "../data";
import { UsernameRandomizer } from "../engine";
import { newDefaultEvaluator } from "../rx";
import { EffUsernameGenerationOptions, NoPolicy } from "../types";
@@ -10,6 +10,11 @@ import { observe$PerUserId, sharedStateByUserId } from "../util";
import { EFF_USERNAME_SETTINGS } from "./storage";
const UsernameDigits = Object.freeze({
enabled: 4,
disabled: 0,
});
/** Strategy for creating usernames from the EFF wordlist */
export class EffUsernameGeneratorStrategy
implements GeneratorStrategy<EffUsernameGenerationOptions, NoPolicy>

View File

@@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
import { DefaultPassphraseGenerationOptions } from "../data";
import { PasswordRandomizer } from "../engine";
import { PassphraseGeneratorOptionsEvaluator } from "../policies";
@@ -20,7 +20,7 @@ const SomeUser = "some user" as UserId;
describe("Passphrase generation strategy", () => {
describe("toEvaluator()", () => {
it("should map to the policy evaluator", async () => {
const strategy = new PassphraseGeneratorStrategy(null, null);
const strategy = new PassphraseGeneratorStrategy(null!, null!);
const policy = mock<Policy>({
type: PolicyType.PasswordGenerator,
data: {
@@ -44,13 +44,18 @@ describe("Passphrase generation strategy", () => {
it.each([[[]], [null], [undefined]])(
"should map `%p` to a disabled password policy evaluator",
async (policies) => {
const strategy = new PassphraseGeneratorStrategy(null, null);
const strategy = new PassphraseGeneratorStrategy(null!, null!);
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
// this case tests when the type system is subverted
const evaluator$ = of(policies!).pipe(strategy.toEvaluator());
const evaluator = await firstValueFrom(evaluator$);
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
expect(evaluator.policy).toMatchObject(Policies.Passphrase.disabledValue);
expect(evaluator.policy).toMatchObject({
minNumberWords: 0,
capitalize: false,
includeNumber: false,
});
},
);
});
@@ -58,7 +63,7 @@ describe("Passphrase generation strategy", () => {
describe("durableState", () => {
it("should use password settings key", () => {
const provider = mock<StateProvider>();
const strategy = new PassphraseGeneratorStrategy(null, provider);
const strategy = new PassphraseGeneratorStrategy(null!, provider);
strategy.durableState(SomeUser);
@@ -68,7 +73,7 @@ describe("Passphrase generation strategy", () => {
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new PassphraseGeneratorStrategy(null, null);
const strategy = new PassphraseGeneratorStrategy(null!, null!);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
@@ -78,7 +83,7 @@ describe("Passphrase generation strategy", () => {
describe("policy", () => {
it("should use password generator policy", () => {
const strategy = new PassphraseGeneratorStrategy(null, null);
const strategy = new PassphraseGeneratorStrategy(null!, null!);
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
});
@@ -95,7 +100,7 @@ describe("Passphrase generation strategy", () => {
});
it("should map options", async () => {
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
numWords: 6,
@@ -114,7 +119,7 @@ describe("Passphrase generation strategy", () => {
});
it("should default numWords", async () => {
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
capitalize: true,
@@ -132,7 +137,7 @@ describe("Passphrase generation strategy", () => {
});
it("should default capitalize", async () => {
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
numWords: 6,
@@ -150,7 +155,7 @@ describe("Passphrase generation strategy", () => {
});
it("should default includeNumber", async () => {
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
numWords: 6,
@@ -168,7 +173,7 @@ describe("Passphrase generation strategy", () => {
});
it("should default wordSeparator", async () => {
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
numWords: 6,

View File

@@ -4,8 +4,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { StateProvider } from "@bitwarden/common/platform/state";
import { GeneratorStrategy } from "../abstractions";
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
import { DefaultPassphraseGenerationOptions } from "../data";
import { PasswordRandomizer } from "../engine";
import { PassphraseGeneratorOptionsEvaluator, passphraseLeastPrivilege } from "../policies";
import { mapPolicyToEvaluator } from "../rx";
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
import { observe$PerUserId, optionsToEffWordListRequest, sharedStateByUserId } from "../util";
@@ -30,7 +31,16 @@ export class PassphraseGeneratorStrategy
defaults$ = observe$PerUserId(() => DefaultPassphraseGenerationOptions);
readonly policy = PolicyType.PasswordGenerator;
toEvaluator() {
return mapPolicyToEvaluator(Policies.Passphrase);
return mapPolicyToEvaluator({
type: PolicyType.PasswordGenerator,
disabledValue: Object.freeze({
minNumberWords: 0,
capitalize: false,
includeNumber: false,
}),
combine: passphraseLeastPrivilege,
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
});
}
// algorithm

View File

@@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultPasswordGenerationOptions, Policies } from "../data";
import { DefaultPasswordGenerationOptions } from "../data";
import { PasswordRandomizer } from "../engine";
import { PasswordGeneratorOptionsEvaluator } from "../policies";
@@ -20,7 +20,7 @@ const SomeUser = "some user" as UserId;
describe("Password generation strategy", () => {
describe("toEvaluator()", () => {
it("should map to a password policy evaluator", async () => {
const strategy = new PasswordGeneratorStrategy(null, null);
const strategy = new PasswordGeneratorStrategy(null!, null!);
const policy = mock<Policy>({
type: PolicyType.PasswordGenerator,
data: {
@@ -52,13 +52,22 @@ describe("Password generation strategy", () => {
it.each([[[]], [null], [undefined]])(
"should map `%p` to a disabled password policy evaluator",
async (policies) => {
const strategy = new PasswordGeneratorStrategy(null, null);
const strategy = new PasswordGeneratorStrategy(null!, null!);
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
// this case tests when the type system is subverted
const evaluator$ = of(policies!).pipe(strategy.toEvaluator());
const evaluator = await firstValueFrom(evaluator$);
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
expect(evaluator.policy).toMatchObject(Policies.Password.disabledValue);
expect(evaluator.policy).toMatchObject({
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
});
},
);
});
@@ -66,7 +75,7 @@ describe("Password generation strategy", () => {
describe("durableState", () => {
it("should use password settings key", () => {
const provider = mock<StateProvider>();
const strategy = new PasswordGeneratorStrategy(null, provider);
const strategy = new PasswordGeneratorStrategy(null!, provider);
strategy.durableState(SomeUser);
@@ -76,7 +85,7 @@ describe("Password generation strategy", () => {
describe("defaults$", () => {
it("should return the default subaddress options", async () => {
const strategy = new PasswordGeneratorStrategy(null, null);
const strategy = new PasswordGeneratorStrategy(null!, null!);
const result = await firstValueFrom(strategy.defaults$(SomeUser));
@@ -86,7 +95,7 @@ describe("Password generation strategy", () => {
describe("policy", () => {
it("should use password generator policy", () => {
const strategy = new PasswordGeneratorStrategy(null, null);
const strategy = new PasswordGeneratorStrategy(null!, null!);
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
});
@@ -103,7 +112,7 @@ describe("Password generation strategy", () => {
});
it("should map options", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 20,
@@ -130,7 +139,7 @@ describe("Password generation strategy", () => {
});
it("should disable uppercase", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 3,
@@ -157,7 +166,7 @@ describe("Password generation strategy", () => {
});
it("should disable lowercase", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 3,
@@ -184,7 +193,7 @@ describe("Password generation strategy", () => {
});
it("should disable digits", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 3,
@@ -211,7 +220,7 @@ describe("Password generation strategy", () => {
});
it("should disable special", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 3,
@@ -238,7 +247,7 @@ describe("Password generation strategy", () => {
});
it("should override length with minimums", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 20,
@@ -265,7 +274,7 @@ describe("Password generation strategy", () => {
});
it("should default uppercase", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 2,
@@ -291,7 +300,7 @@ describe("Password generation strategy", () => {
});
it("should default lowercase", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 0,
@@ -317,7 +326,7 @@ describe("Password generation strategy", () => {
});
it("should default number", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 0,
@@ -343,7 +352,7 @@ describe("Password generation strategy", () => {
});
it("should default special", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 0,
@@ -369,7 +378,7 @@ describe("Password generation strategy", () => {
});
it("should default minUppercase", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 0,
@@ -395,7 +404,7 @@ describe("Password generation strategy", () => {
});
it("should default minLowercase", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 0,
@@ -421,7 +430,7 @@ describe("Password generation strategy", () => {
});
it("should default minNumber", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 0,
@@ -447,7 +456,7 @@ describe("Password generation strategy", () => {
});
it("should default minSpecial", async () => {
const strategy = new PasswordGeneratorStrategy(randomizer, null);
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
const result = await strategy.generate({
length: 0,

View File

@@ -2,8 +2,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { StateProvider } from "@bitwarden/common/platform/state";
import { GeneratorStrategy } from "../abstractions";
import { Policies, DefaultPasswordGenerationOptions } from "../data";
import { DefaultPasswordGenerationOptions } from "../data";
import { PasswordRandomizer } from "../engine";
import { PasswordGeneratorOptionsEvaluator, passwordLeastPrivilege } from "../policies";
import { mapPolicyToEvaluator } from "../rx";
import { PasswordGenerationOptions, PasswordGeneratorPolicy } from "../types";
import { observe$PerUserId, optionsToRandomAsciiRequest, sharedStateByUserId } from "../util";
@@ -27,7 +28,20 @@ export class PasswordGeneratorStrategy
defaults$ = observe$PerUserId(() => DefaultPasswordGenerationOptions);
readonly policy = PolicyType.PasswordGenerator;
toEvaluator() {
return mapPolicyToEvaluator(Policies.Password);
return mapPolicyToEvaluator({
type: PolicyType.PasswordGenerator,
disabledValue: {
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
},
combine: passwordLeastPrivilege,
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
});
}
// algorithm

View File

@@ -0,0 +1,48 @@
import { CredentialAlgorithm, CredentialType } from "../metadata";
export type AlgorithmInfo = {
/** Uniquely identifies the credential configuration
* @example
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
* // to pattern test whether the credential describes a forwarder algorithm
* const meta : CredentialGeneratorInfo = // ...
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
*/
id: CredentialAlgorithm;
/** The kind of credential generated by this configuration */
type: CredentialType;
/** Localized algorithm name */
name: string;
/* Localized generate button label */
generate: string;
/** Localized "credential generated" informational message */
onGeneratedMessage: string;
/* Localized copy button label */
copy: string;
/* Localized dialog button label */
useGeneratedValue: string;
/* Localized generated value label */
credentialType: string;
/** Localized algorithm description */
description?: string;
/** When true, credential generation must be explicitly requested.
* @remarks this property is useful when credential generation
* carries side effects, such as configuring a service external
* to Bitwarden.
*/
onlyOnRequest: boolean;
/** Well-known fields to display on the options panel or collect from the environment.
* @remarks: at present, this is only used by forwarders
*/
request: readonly string[];
};

View File

@@ -1,153 +0,0 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
import { Constraints } from "@bitwarden/common/tools/types";
import { Randomizer } from "../abstractions";
import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from "../types";
import { CredentialGenerator } from "./credential-generator";
export type GeneratorDependencyProvider = {
randomizer: Randomizer;
client: RestClient;
i18nService: I18nService;
};
export type AlgorithmInfo = {
/** Uniquely identifies the credential configuration
* @example
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
* // to pattern test whether the credential describes a forwarder algorithm
* const meta : CredentialGeneratorInfo = // ...
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
*/
id: CredentialAlgorithm;
/** The kind of credential generated by this configuration */
category: CredentialCategory;
/** Localized algorithm name */
name: string;
/* Localized generate button label */
generate: string;
/** Localized "credential generated" informational message */
onGeneratedMessage: string;
/* Localized copy button label */
copy: string;
/* Localized dialog button label */
useGeneratedValue: string;
/* Localized generated value label */
credentialType: string;
/** Localized algorithm description */
description?: string;
/** When true, credential generation must be explicitly requested.
* @remarks this property is useful when credential generation
* carries side effects, such as configuring a service external
* to Bitwarden.
*/
onlyOnRequest: boolean;
/** Well-known fields to display on the options panel or collect from the environment.
* @remarks: at present, this is only used by forwarders
*/
request: readonly string[];
};
/** Credential generator metadata common across credential generators */
export type CredentialGeneratorInfo = {
/** Uniquely identifies the credential configuration
* @example
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
* // to pattern test whether the credential describes a forwarder algorithm
* const meta : CredentialGeneratorInfo = // ...
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
*/
id: CredentialAlgorithm;
/** The kind of credential generated by this configuration */
category: CredentialCategory;
/** Localization key for the credential name */
nameKey: string;
/** Localization key for the credential description*/
descriptionKey?: string;
/** Localization key for the generate command label */
generateKey: string;
/** Localization key for the copy button label */
copyKey: string;
/** Localization key for the "credential generated" informational message */
onGeneratedMessageKey: string;
/** Localized "use generated credential" button label */
useGeneratedValueKey: string;
/** Localization key for describing the kind of credential generated
* by this generator.
*/
credentialTypeKey: string;
/** When true, credential generation must be explicitly requested.
* @remarks this property is useful when credential generation
* carries side effects, such as configuring a service external
* to Bitwarden.
*/
onlyOnRequest: boolean;
/** Well-known fields to display on the options panel or collect from the environment.
* @remarks: at present, this is only used by forwarders
*/
request: readonly string[];
};
/** Credential generator metadata that relies upon typed setting and policy definitions.
* @example
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
* // to pattern test whether the credential describes a forwarder algorithm
* const meta : CredentialGeneratorInfo = // ...
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
*/
export type CredentialGeneratorConfiguration<Settings, Policy> = CredentialGeneratorInfo & {
/** An algorithm that generates credentials when ran. */
engine: {
/** Factory for the generator
*/
// FIXME: note that this erases the engine's type so that credentials are
// generated uniformly. This property needs to be maintained for
// the credential generator, but engine configurations should return
// the underlying type. `create` may be able to do double-duty w/ an
// engine definition if `CredentialGenerator` can be made covariant.
create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator<Settings>;
};
/** Defines the stored parameters for credential generation */
settings: {
/** value used when an account's settings haven't been initialized
* @deprecated use `ObjectKey.initial` for your desired storage property instead
*/
initial: Readonly<Partial<Settings>>;
/** Application-global constraints that apply to account settings */
constraints: Constraints<Settings>;
/** storage location for account-global settings */
account: UserKeyDefinition<Settings> | ObjectKey<Settings>;
/** storage location for *plaintext* settings imports */
import?: UserKeyDefinition<Settings> | ObjectKey<Settings, Record<string, never>, Settings>;
};
/** defines how to construct policy for this settings instance */
policy: PolicyConfiguration<Policy, Settings>;
};

View File

@@ -0,0 +1,10 @@
import { CredentialAlgorithm, CredentialType } from "../metadata";
/** The kind of credential to generate using a compound configuration. */
// FIXME: extend the preferences to include a preferred forwarder
export type CredentialPreference = {
[Key in CredentialType]: {
algorithm: CredentialAlgorithm;
updated: Date;
};
};

View File

@@ -13,18 +13,6 @@ import { EmailDomainSettings, EmailPrefixSettings } from "../engine";
*/
export type ForwarderId = IntegrationId;
/** Metadata format for email forwarding services. */
export type ForwarderMetadata = {
/** The unique identifier for the forwarder. */
id: ForwarderId;
/** The name of the service the forwarder queries. */
name: string;
/** Whether the forwarder is valid for self-hosted instances of Bitwarden. */
validForSelfHosted: boolean;
};
/** Options common to all forwarder APIs */
export type ApiOptions = ApiSettings & IntegrationRequest;
@@ -36,3 +24,9 @@ export type EmailDomainOptions = EmailDomainSettings;
/** Api configuration for forwarders that support custom email parts. */
export type EmailPrefixOptions = EmailDomainSettings & EmailPrefixSettings;
// These options are used by all forwarders; each forwarder uses a different set,
// as defined by `GeneratorMetadata<T>.capabilities.fields`.
export type ForwarderOptions = Partial<
EmailDomainSettings & EmailPrefixSettings & SelfHostedApiSettings
>;

View File

@@ -1,6 +1,15 @@
import { RequireExactlyOne } from "type-fest";
import { CredentialType, GeneratorProfile, CredentialAlgorithm } from "../metadata";
/** Contextual information about the application state when a generator is invoked.
*/
export type GenerateRequest = {
export type GenerateRequest = RequireExactlyOne<
{ type: CredentialType; algorithm: CredentialAlgorithm },
"type" | "algorithm"
> & {
profile?: GeneratorProfile;
/** Traces the origin of the generation request. This parameter is
* copied to the generated credential.
*

View File

@@ -1,40 +1,42 @@
import { CredentialAlgorithm, GeneratedCredential } from ".";
import { Type } from "../metadata";
import { GeneratedCredential } from "./generated-credential";
describe("GeneratedCredential", () => {
describe("constructor", () => {
it("assigns credential", () => {
const result = new GeneratedCredential("example", "passphrase", new Date(100));
const result = new GeneratedCredential("example", Type.password, new Date(100));
expect(result.credential).toEqual("example");
});
it("assigns category", () => {
const result = new GeneratedCredential("example", "passphrase", new Date(100));
const result = new GeneratedCredential("example", Type.password, new Date(100));
expect(result.category).toEqual("passphrase");
});
it("passes through date parameters", () => {
const result = new GeneratedCredential("example", "password", new Date(100));
const result = new GeneratedCredential("example", Type.password, new Date(100));
expect(result.generationDate).toEqual(new Date(100));
});
it("converts numeric dates to Dates", () => {
const result = new GeneratedCredential("example", "password", 100);
const result = new GeneratedCredential("example", Type.password, 100);
expect(result.generationDate).toEqual(new Date(100));
});
});
it("toJSON converts from a credential into a JSON object", () => {
const credential = new GeneratedCredential("example", "password", new Date(100));
const credential = new GeneratedCredential("example", Type.password, new Date(100));
const result = credential.toJSON();
expect(result).toEqual({
credential: "example",
category: "password" as CredentialAlgorithm,
category: Type.password,
generationDate: 100,
});
});
@@ -42,7 +44,7 @@ describe("GeneratedCredential", () => {
it("fromJSON converts Json objects into credentials", () => {
const jsonValue = {
credential: "example",
category: "password" as CredentialAlgorithm,
category: Type.password,
generationDate: 100,
};

View File

@@ -1,6 +1,6 @@
import { Jsonify } from "type-fest";
import { CredentialAlgorithm } from "./generator-type";
import { CredentialType } from "../metadata";
/** A credential generation result */
export class GeneratedCredential {
@@ -16,7 +16,7 @@ export class GeneratedCredential {
*/
constructor(
readonly credential: string,
readonly category: CredentialAlgorithm,
readonly category: CredentialType,
generationDate: Date | number,
readonly source?: string,
readonly website?: string,

View File

@@ -1,83 +0,0 @@
import { VendorId } from "@bitwarden/common/tools/extension";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types";
import { AlgorithmsByType, CredentialType } from "../metadata";
/** A type of password that may be generated by the credential generator. */
export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number];
/** A type of username that may be generated by the credential generator. */
export type UsernameAlgorithm = (typeof UsernameAlgorithms)[number];
/** A type of email address that may be generated by the credential generator. */
export type EmailAlgorithm = (typeof EmailAlgorithms)[number];
export type ForwarderIntegration = { forwarder: IntegrationId & VendorId };
/** Returns true when the input algorithm is a forwarder integration. */
export function isForwarderIntegration(
algorithm: CredentialAlgorithm,
): algorithm is ForwarderIntegration {
return algorithm && typeof algorithm === "object" && "forwarder" in algorithm;
}
export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) {
if (lhs === rhs) {
return true;
} else if (isForwarderIntegration(lhs) && isForwarderIntegration(rhs)) {
return lhs.forwarder === rhs.forwarder;
} else {
return false;
}
}
/** A type of credential that may be generated by the credential generator. */
export type CredentialAlgorithm =
| PasswordAlgorithm
| UsernameAlgorithm
| EmailAlgorithm
| ForwarderIntegration;
/** Compound credential types supported by the credential generator. */
export const CredentialCategories = Object.freeze({
/** Lists algorithms in the "password" credential category */
password: PasswordAlgorithms as Readonly<PasswordAlgorithm[]>,
/** Lists algorithms in the "username" credential category */
username: UsernameAlgorithms as Readonly<UsernameAlgorithm[]>,
/** Lists algorithms in the "email" credential category */
email: EmailAlgorithms as Readonly<(EmailAlgorithm | ForwarderIntegration)[]>,
});
/** Returns true when the input algorithm is a password algorithm. */
export function isPasswordAlgorithm(
algorithm: CredentialAlgorithm,
): algorithm is PasswordAlgorithm {
return PasswordAlgorithms.includes(algorithm as any);
}
/** Returns true when the input algorithm is a username algorithm. */
export function isUsernameAlgorithm(
algorithm: CredentialAlgorithm,
): algorithm is UsernameAlgorithm {
return UsernameAlgorithms.includes(algorithm as any);
}
/** Returns true when the input algorithm is an email algorithm. */
export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm {
return EmailAlgorithms.includes(algorithm as any) || isForwarderIntegration(algorithm);
}
/** A type of compound credential that may be generated by the credential generator. */
export type CredentialCategory = keyof typeof CredentialCategories;
/** The kind of credential to generate using a compound configuration. */
// FIXME: extend the preferences to include a preferred forwarder
export type CredentialPreference = {
[Key in CredentialType & CredentialCategory]: {
algorithm: CredentialAlgorithm & (typeof AlgorithmsByType)[Key][number];
updated: Date;
};
};

View File

@@ -1,16 +1,14 @@
import { EmailAlgorithm, PasswordAlgorithm, UsernameAlgorithm } from "./generator-type";
export * from "./boundary";
export * from "./catchall-generator-options";
export * from "./credential-generator";
export * from "./credential-generator-configuration";
export * from "./algorithm-info";
export * from "./eff-username-generator-options";
export * from "./forwarder-options";
export * from "./generate-request";
export * from "./generator-constraints";
export * from "./generated-credential";
export * from "./generator-options";
export * from "./generator-type";
export * from "./credential-preference";
export * from "./no-policy";
export * from "./passphrase-generation-options";
export * from "./passphrase-generator-policy";
@@ -19,13 +17,3 @@ export * from "./password-generator-policy";
export * from "./policy-configuration";
export * from "./subaddress-generator-options";
export * from "./word-options";
/** Provided for backwards compatibility only.
* @deprecated Use one of the Algorithm types instead.
*/
export type GeneratorType = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm;
/** Provided for backwards compatibility only.
* @deprecated Use one of the Algorithm types instead.
*/
export type PasswordType = PasswordAlgorithm;

View File

@@ -3,8 +3,6 @@ import { Policy as AdminPolicy } from "@bitwarden/common/admin-console/models/do
import { PolicyEvaluator } from "../abstractions";
import { GeneratorConstraints } from "./generator-constraints";
/** Determines how to construct a password generator policy */
export type PolicyConfiguration<Policy, Settings> = {
type: PolicyType;
@@ -22,15 +20,4 @@ export type PolicyConfiguration<Policy, Settings> = {
* Use `toConstraints` instead.
*/
createEvaluator: (policy: Policy) => PolicyEvaluator<Policy, Settings>;
/** Converts policy service data into actionable policy constraints.
*
* @param policy - the policy to map into policy constraints.
* @param email - the default email to extend.
*
* @remarks this version includes constraints needed for the reactive forms;
* it was introduced so that the constraints can be incrementally introduced
* as the new UI is built.
*/
toConstraints: (policy: Policy, email: string) => GeneratorConstraints<Settings>;
};

View File

@@ -11,7 +11,8 @@
"include": [
"src",
"../extensions/src/history/generator-history.abstraction.ts",
"../extensions/src/navigation/generator-navigation.service.abstraction.ts"
"../extensions/src/navigation/generator-navigation.service.abstraction.ts",
"../extensions/legacy/src/forwarders.ts"
],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,6 +1,6 @@
import { Jsonify } from "type-fest";
import { CredentialAlgorithm } from "@bitwarden/generator-core";
import { CredentialType } from "@bitwarden/generator-core";
/** A credential generation result */
export class GeneratedCredential {
@@ -14,7 +14,7 @@ export class GeneratedCredential {
*/
constructor(
readonly credential: string,
readonly category: CredentialAlgorithm,
readonly category: CredentialType,
generationDate: Date | number,
) {
if (typeof generationDate === "number") {

View File

@@ -3,7 +3,7 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
import { CredentialAlgorithm } from "@bitwarden/generator-core";
import { CredentialType } from "@bitwarden/generator-core";
import { GeneratedCredential } from "./generated-credential";
@@ -29,7 +29,7 @@ export abstract class GeneratorHistoryService {
track: (
userId: UserId,
credential: string,
category: CredentialAlgorithm,
category: CredentialType,
date?: Date,
) => Promise<GeneratedCredential | null>;

View File

@@ -9,7 +9,7 @@ import { BufferedState } from "@bitwarden/common/tools/state/buffered-state";
import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-packer";
import { SecretState } from "@bitwarden/common/tools/state/secret-state";
import { UserId } from "@bitwarden/common/types/guid";
import { CredentialAlgorithm } from "@bitwarden/generator-core";
import { CredentialType } from "@bitwarden/generator-core";
import { KeyService } from "@bitwarden/key-management";
import { GeneratedCredential } from "./generated-credential";
@@ -36,12 +36,7 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
private _credentialStates = new Map<UserId, SingleUserState<GeneratedCredential[]>>();
/** {@link GeneratorHistoryService.track} */
track = async (
userId: UserId,
credential: string,
category: CredentialAlgorithm,
date?: Date,
) => {
track = async (userId: UserId, credential: string, category: CredentialType, date?: Date) => {
const state = this.getCredentialState(userId);
let result: GeneratedCredential = null;

View File

@@ -1,4 +1,18 @@
import { ForwarderMetadata } from "../types";
import { IntegrationId } from "@bitwarden/common/tools/integration";
export type ForwarderId = IntegrationId;
/** Metadata format for email forwarding services. */
export type ForwarderMetadata = {
/** The unique identifier for the forwarder. */
id: ForwarderId;
/** The name of the service the forwarder queries. */
name: string;
/** Whether the forwarder is valid for self-hosted instances of Bitwarden. */
validForSelfHosted: boolean;
};
/** Metadata about an email forwarding service.
* @remarks This is used to populate the forwarder selection list

View File

@@ -7,7 +7,6 @@ import {
GeneratorService,
DefaultPassphraseGenerationOptions,
DefaultPasswordGenerationOptions,
Policies,
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
PasswordGenerationOptions,
@@ -38,12 +37,17 @@ const PasswordGeneratorOptionsEvaluator = policies.PasswordGeneratorOptionsEvalu
function createPassphraseGenerator(
options: PassphraseGenerationOptions = {},
policy: PassphraseGeneratorPolicy = Policies.Passphrase.disabledValue,
policy?: PassphraseGeneratorPolicy,
) {
let savedOptions = options;
const generator = mock<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({
evaluator$(id: UserId) {
const evaluator = new PassphraseGeneratorOptionsEvaluator(policy);
const active = policy ?? {
minNumberWords: 0,
capitalize: false,
includeNumber: false,
};
const evaluator = new PassphraseGeneratorOptionsEvaluator(active);
return of(evaluator);
},
options$(id: UserId) {
@@ -63,12 +67,21 @@ function createPassphraseGenerator(
function createPasswordGenerator(
options: PasswordGenerationOptions = {},
policy: PasswordGeneratorPolicy = Policies.Password.disabledValue,
policy?: PasswordGeneratorPolicy,
) {
let savedOptions = options;
const generator = mock<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({
evaluator$(id: UserId) {
const evaluator = new PasswordGeneratorOptionsEvaluator(policy);
const active = policy ?? {
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
};
const evaluator = new PasswordGeneratorOptionsEvaluator(active);
return of(evaluator);
},
options$(id: UserId) {
@@ -118,7 +131,13 @@ describe("LegacyPasswordGenerationService", () => {
describe("generatePassword", () => {
it("invokes the inner password generator to generate passwords", async () => {
const innerPassword = createPasswordGenerator();
const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null, null);
const generator = new LegacyPasswordGenerationService(
null!,
null!,
innerPassword,
null!,
null!,
);
const options = { type: "password" } as PasswordGeneratorOptions;
await generator.generatePassword(options);
@@ -129,11 +148,11 @@ describe("LegacyPasswordGenerationService", () => {
it("invokes the inner passphrase generator to generate passphrases", async () => {
const innerPassphrase = createPassphraseGenerator();
const generator = new LegacyPasswordGenerationService(
null,
null,
null,
null!,
null!,
null!,
innerPassphrase,
null,
null!,
);
const options = { type: "passphrase" } as PasswordGeneratorOptions;
@@ -147,11 +166,11 @@ describe("LegacyPasswordGenerationService", () => {
it("invokes the inner passphrase generator", async () => {
const innerPassphrase = createPassphraseGenerator();
const generator = new LegacyPasswordGenerationService(
null,
null,
null,
null!,
null!,
null!,
innerPassphrase,
null,
null!,
);
const options = {} as PasswordGeneratorOptions;
@@ -193,7 +212,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [result] = await generator.getOptions();
@@ -220,16 +239,16 @@ describe("LegacyPasswordGenerationService", () => {
});
it("sets default options when an inner service lacks a value", async () => {
const innerPassword = createPasswordGenerator(null);
const innerPassphrase = createPassphraseGenerator(null);
const navigation = createNavigationGenerator(null);
const innerPassword = createPasswordGenerator(null!);
const innerPassphrase = createPassphraseGenerator(null!);
const navigation = createNavigationGenerator(null!);
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [result] = await generator.getOptions();
@@ -277,7 +296,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [, policy] = await generator.getOptions();
@@ -323,7 +342,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
@@ -363,7 +382,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
@@ -409,7 +428,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
@@ -441,7 +460,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const options = {
type: "password" as const,
@@ -474,7 +493,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const options = {
type: "passphrase" as const,
@@ -504,7 +523,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const options = {
type: "passphrase" as const,
@@ -533,9 +552,9 @@ describe("LegacyPasswordGenerationService", () => {
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
null,
null,
null,
null!,
null!,
null!,
history,
);
@@ -552,9 +571,9 @@ describe("LegacyPasswordGenerationService", () => {
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
null,
null,
null,
null!,
null!,
null!,
history,
);

View File

@@ -14,13 +14,13 @@ import {
GeneratorService,
CatchallGenerationOptions,
EffUsernameGenerationOptions,
Forwarders,
SubaddressGenerationOptions,
UsernameGeneratorType,
ForwarderId,
} from "@bitwarden/generator-core";
import { GeneratorNavigationService, GeneratorNavigation } from "@bitwarden/generator-navigation";
import { Forwarders } from "./forwarders";
import { UsernameGeneratorOptions } from "./username-generation-options";
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PasswordAlgorithms, PolicyEvaluator } from "@bitwarden/generator-core";
import { AlgorithmsByType, PolicyEvaluator, Type } from "@bitwarden/generator-core";
import { DefaultGeneratorNavigation } from "./default-generator-navigation";
import { GeneratorNavigation } from "./generator-navigation";
@@ -19,7 +19,7 @@ export class GeneratorNavigationEvaluator
/** {@link PolicyEvaluator.policyInEffect} */
get policyInEffect(): boolean {
return PasswordAlgorithms.includes(this.policy?.overridePasswordType);
return AlgorithmsByType[Type.password].includes(this.policy?.overridePasswordType);
}
/** Apply policy to the input options.

View File

@@ -4,14 +4,14 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PasswordType } from "@bitwarden/generator-core";
import { PasswordAlgorithm } from "@bitwarden/generator-core";
/** Policy settings affecting password generator navigation */
export type GeneratorNavigationPolicy = {
/** The type of generator that should be shown by default when opening
* the password generator.
*/
overridePasswordType?: PasswordType;
overridePasswordType?: PasswordAlgorithm;
};
/** Reduces a policy into an accumulator by preferring the password generator

View File

@@ -1,4 +1,4 @@
import { GeneratorType, ForwarderId, UsernameGeneratorType } from "@bitwarden/generator-core";
import { ForwarderId, UsernameGeneratorType, CredentialAlgorithm } from "@bitwarden/generator-core";
/** Stores credential generator UI state. */
export type GeneratorNavigation = {
@@ -6,7 +6,7 @@ export type GeneratorNavigation = {
* @remarks The legacy generator only supports "password" and "passphrase".
* The componentized generator supports all values.
*/
type?: GeneratorType;
type?: CredentialAlgorithm;
/** When `type === "username"`, this stores the username algorithm. */
username?: UsernameGeneratorType;

View File

@@ -28,7 +28,7 @@ import {
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core";
import { CredentialGeneratorService, GenerateRequest, Type } from "@bitwarden/generator-core";
import { SendFormConfig } from "../../abstractions/send-form-config.service";
import { SendFormContainer } from "../../send-form-container";
@@ -122,12 +122,12 @@ export class SendOptionsComponent implements OnInit {
}
generatePassword = async () => {
const on$ = new BehaviorSubject<GenerateRequest>({ source: "send" });
const on$ = new BehaviorSubject<GenerateRequest>({ source: "send", type: Type.password });
const account$ = this.accountService.activeAccount$.pipe(
pin({ name: () => "send-options.component", distinct: (p, c) => p.id === c.id }),
);
const generatedCredential = await firstValueFrom(
this.generatorService.generate$(Generators.password, { on$, account$ }),
this.generatorService.generate$({ on$, account$ }),
);
this.sendOptionsForm.patchValue({