;
+};
// FIXME: once the modernization is complete, switch the type parameters
// in `PolicyEvaluator` and bake-in the constraints type.
type Evaluator = PolicyEvaluator & Constraints;
export class CredentialGeneratorService {
constructor(
+ private randomizer: Randomizer,
private stateProvider: StateProvider,
private policyService: PolicyService,
) {}
+ /** Generates a stream of credentials
+ * @param configuration determines which generator's settings are loaded
+ * @param dependencies.on$ when specified, a new credential is emitted when
+ * this emits. Otherwise, a new credential is emitted when the settings
+ * update.
+ */
+ generate$(
+ configuration: Readonly>,
+ dependencies?: Generate$Dependencies,
+ ) {
+ // instantiate the engine
+ const engine = configuration.engine.create(this.randomizer);
+
+ // stream blocks until all of these values are received
+ const website$ = dependencies?.website$ ?? new BehaviorSubject(null);
+ const request$ = website$.pipe(map((website) => ({ website })));
+ const settings$ = this.settings$(configuration, dependencies);
+
+ // monitor completion
+ const requestComplete$ = request$.pipe(ignoreElements(), endWith(true));
+ const settingsComplete$ = request$.pipe(ignoreElements(), endWith(true));
+ const complete$ = race(requestComplete$, settingsComplete$);
+
+ // generation proper
+ const generate$ = (dependencies?.on$ ?? settings$).pipe(
+ withLatestFrom(request$, settings$),
+ concatMap(([, request, settings]) => engine.generate(request, settings)),
+ takeUntil(complete$),
+ );
+
+ return generate$;
+ }
+
/** Get the settings for the provided configuration
* @param configuration determines which generator's settings are loaded
* @param dependencies.userId$ identifies the user to which the settings are bound.
@@ -82,7 +136,7 @@ export class CredentialGeneratorService {
* @remarks the subject enforces policy for the settings
*/
async settings(
- configuration: Configuration,
+ configuration: Readonly>,
dependencies: SingleUserDependency,
) {
const userId = await firstValueFrom(
diff --git a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts
index f9b346e02b..6591b179fc 100644
--- a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts
+++ b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts
@@ -17,7 +17,7 @@ import { PASSPHRASE_SETTINGS } from "./storage";
const SomeUser = "some user" as UserId;
-describe("Password generation strategy", () => {
+describe("Passphrase generation strategy", () => {
describe("toEvaluator()", () => {
it("should map to the policy evaluator", async () => {
const strategy = new PassphraseGeneratorStrategy(null, null);
diff --git a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.ts b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.ts
index fe2731f9dd..37d8b9e3fb 100644
--- a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.ts
+++ b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.ts
@@ -2,11 +2,11 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { StateProvider } from "@bitwarden/common/platform/state";
import { GeneratorStrategy } from "../abstractions";
-import { DefaultPassphraseBoundaries, DefaultPassphraseGenerationOptions, Policies } from "../data";
+import { DefaultPassphraseGenerationOptions, Policies } from "../data";
import { PasswordRandomizer } from "../engine";
import { mapPolicyToEvaluator } from "../rx";
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
-import { observe$PerUserId, sharedStateByUserId } from "../util";
+import { observe$PerUserId, optionsToEffWordListRequest, sharedStateByUserId } from "../util";
import { PASSPHRASE_SETTINGS } from "./storage";
@@ -33,13 +33,7 @@ export class PassphraseGeneratorStrategy
// algorithm
async generate(options: PassphraseGenerationOptions): Promise {
- const requestWords = options.numWords ?? DefaultPassphraseGenerationOptions.numWords;
- const request = {
- numberOfWords: Math.max(requestWords, DefaultPassphraseBoundaries.numWords.min),
- capitalize: options.capitalize ?? DefaultPassphraseGenerationOptions.capitalize,
- number: options.includeNumber ?? DefaultPassphraseGenerationOptions.includeNumber,
- separator: options.wordSeparator ?? DefaultPassphraseGenerationOptions.wordSeparator,
- };
+ const request = optionsToEffWordListRequest(options);
return this.randomizer.randomEffLongWords(request);
}
diff --git a/libs/tools/generator/core/src/strategies/password-generator-strategy.ts b/libs/tools/generator/core/src/strategies/password-generator-strategy.ts
index 9ed62490c0..9ff8a3d88b 100644
--- a/libs/tools/generator/core/src/strategies/password-generator-strategy.ts
+++ b/libs/tools/generator/core/src/strategies/password-generator-strategy.ts
@@ -6,7 +6,7 @@ import { Policies, DefaultPasswordGenerationOptions } from "../data";
import { PasswordRandomizer } from "../engine";
import { mapPolicyToEvaluator } from "../rx";
import { PasswordGenerationOptions, PasswordGeneratorPolicy } from "../types";
-import { observe$PerUserId, sharedStateByUserId, sum } from "../util";
+import { observe$PerUserId, optionsToRandomAsciiRequest, sharedStateByUserId } from "../util";
import { PASSWORD_SETTINGS } from "./storage";
@@ -32,62 +32,7 @@ export class PasswordGeneratorStrategy
// algorithm
async generate(options: PasswordGenerationOptions): Promise {
- // converts password generation option sets, which are defined by
- // an "enabled" and "quantity" parameter, to the password engine's
- // parameters, which represent disabled options as `undefined`
- // properties.
- function process(
- // values read from the options
- enabled: boolean,
- quantity: number,
- // value used if an option is missing
- defaultEnabled: boolean,
- defaultQuantity: number,
- ) {
- const isEnabled = enabled ?? defaultEnabled;
- const actualQuantity = quantity ?? defaultQuantity;
- const result = isEnabled ? actualQuantity : undefined;
-
- return result;
- }
-
- const request = {
- uppercase: process(
- options.uppercase,
- options.minUppercase,
- DefaultPasswordGenerationOptions.uppercase,
- DefaultPasswordGenerationOptions.minUppercase,
- ),
- lowercase: process(
- options.lowercase,
- options.minLowercase,
- DefaultPasswordGenerationOptions.lowercase,
- DefaultPasswordGenerationOptions.minLowercase,
- ),
- digits: process(
- options.number,
- options.minNumber,
- DefaultPasswordGenerationOptions.number,
- DefaultPasswordGenerationOptions.minNumber,
- ),
- special: process(
- options.special,
- options.minSpecial,
- DefaultPasswordGenerationOptions.special,
- DefaultPasswordGenerationOptions.minSpecial,
- ),
- ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous,
- all: 0,
- };
-
- // engine represents character sets as "include only"; you assert how many all
- // characters there can be rather than a total length. This conversion has
- // the character classes win, so that the result is always consistent with policy
- // minimums.
- const required = sum(request.uppercase, request.lowercase, request.digits, request.special);
- const remaining = (options.length ?? 0) - required;
- request.all = Math.max(remaining, 0);
-
+ const request = optionsToRandomAsciiRequest(options);
const result = await this.randomizer.randomAscii(request);
return result;
diff --git a/libs/tools/generator/core/src/types/credential-category.ts b/libs/tools/generator/core/src/types/credential-category.ts
new file mode 100644
index 0000000000..54c8c5ed8e
--- /dev/null
+++ b/libs/tools/generator/core/src/types/credential-category.ts
@@ -0,0 +1,5 @@
+/** Kinds of credentials that can be stored by the history service
+ * password - a secret consisting of arbitrary characters used to authenticate a user
+ * passphrase - a secret consisting of words used to authenticate a user
+ */
+export type CredentialCategory = "password" | "passphrase";
diff --git a/libs/tools/generator/core/src/types/credential-generator-configuration.ts b/libs/tools/generator/core/src/types/credential-generator-configuration.ts
index 2a8b07b0e8..80d977a73c 100644
--- a/libs/tools/generator/core/src/types/credential-generator-configuration.ts
+++ b/libs/tools/generator/core/src/types/credential-generator-configuration.ts
@@ -1,9 +1,29 @@
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import { Constraints } from "@bitwarden/common/tools/types";
+import { Randomizer } from "../abstractions";
import { PolicyConfiguration } from "../types";
+import { CredentialCategory } from "./credential-category";
+import { CredentialGenerator } from "./credential-generator";
+
export type CredentialGeneratorConfiguration = {
+ /** Category describing usage of the credential generated by this configuration
+ */
+ category: CredentialCategory;
+
+ /** 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: Randomizer) => CredentialGenerator;
+ };
+ /** Defines the stored parameters for credential generation */
settings: {
/** value used when an account's settings haven't been initialized */
initial: Readonly>;
diff --git a/libs/tools/generator/core/src/types/credential-generator.ts b/libs/tools/generator/core/src/types/credential-generator.ts
new file mode 100644
index 0000000000..c95ff25aff
--- /dev/null
+++ b/libs/tools/generator/core/src/types/credential-generator.ts
@@ -0,0 +1,12 @@
+import { GenerationRequest } from "@bitwarden/common/tools/types";
+
+import { GeneratedCredential } from "./generated-credential";
+
+/** An algorithm that generates credentials. */
+export type CredentialGenerator = {
+ /** Generates a credential
+ * @param request runtime parameters
+ * @param settings stored parameters
+ */
+ generate: (request: GenerationRequest, settings: Settings) => Promise;
+};
diff --git a/libs/tools/generator/core/src/types/generated-credential.spec.ts b/libs/tools/generator/core/src/types/generated-credential.spec.ts
new file mode 100644
index 0000000000..a687676576
--- /dev/null
+++ b/libs/tools/generator/core/src/types/generated-credential.spec.ts
@@ -0,0 +1,58 @@
+import { CredentialCategory, GeneratedCredential } from ".";
+
+describe("GeneratedCredential", () => {
+ describe("constructor", () => {
+ it("assigns credential", () => {
+ const result = new GeneratedCredential("example", "passphrase", new Date(100));
+
+ expect(result.credential).toEqual("example");
+ });
+
+ it("assigns category", () => {
+ const result = new GeneratedCredential("example", "passphrase", new Date(100));
+
+ expect(result.category).toEqual("passphrase");
+ });
+
+ it("passes through date parameters", () => {
+ const result = new GeneratedCredential("example", "password", new Date(100));
+
+ expect(result.generationDate).toEqual(new Date(100));
+ });
+
+ it("converts numeric dates to Dates", () => {
+ const result = new GeneratedCredential("example", "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 result = credential.toJSON();
+
+ expect(result).toEqual({
+ credential: "example",
+ category: "password" as CredentialCategory,
+ generationDate: 100,
+ });
+ });
+
+ it("fromJSON converts Json objects into credentials", () => {
+ const jsonValue = {
+ credential: "example",
+ category: "password" as CredentialCategory,
+ generationDate: 100,
+ };
+
+ const result = GeneratedCredential.fromJSON(jsonValue);
+
+ expect(result).toBeInstanceOf(GeneratedCredential);
+ expect(result).toEqual({
+ credential: "example",
+ category: "password",
+ generationDate: new Date(100),
+ });
+ });
+});
diff --git a/libs/tools/generator/core/src/types/generated-credential.ts b/libs/tools/generator/core/src/types/generated-credential.ts
new file mode 100644
index 0000000000..ff174b04a5
--- /dev/null
+++ b/libs/tools/generator/core/src/types/generated-credential.ts
@@ -0,0 +1,47 @@
+import { Jsonify } from "type-fest";
+
+import { CredentialCategory } from "./credential-category";
+
+/** A credential generation result */
+export class GeneratedCredential {
+ /**
+ * Instantiates a generated credential
+ * @param credential The value of the generated credential (e.g. a password)
+ * @param category The kind of credential
+ * @param generationDate The date that the credential was generated.
+ * Numeric values should are interpreted using {@link Date.valueOf}
+ * semantics.
+ */
+ constructor(
+ readonly credential: string,
+ readonly category: CredentialCategory,
+ generationDate: Date | number,
+ ) {
+ if (typeof generationDate === "number") {
+ this.generationDate = new Date(generationDate);
+ } else {
+ this.generationDate = generationDate;
+ }
+ }
+
+ /** The date that the credential was generated */
+ generationDate: Date;
+
+ /** Constructs a credential from its `toJSON` representation */
+ static fromJSON(jsonValue: Jsonify) {
+ return new GeneratedCredential(
+ jsonValue.credential,
+ jsonValue.category,
+ jsonValue.generationDate,
+ );
+ }
+
+ /** Serializes a credential to a JSON-compatible object */
+ toJSON() {
+ return {
+ credential: this.credential,
+ category: this.category,
+ generationDate: this.generationDate.valueOf(),
+ };
+ }
+}
diff --git a/libs/tools/generator/core/src/types/index.ts b/libs/tools/generator/core/src/types/index.ts
index 786b15b9d1..229ac1c0c3 100644
--- a/libs/tools/generator/core/src/types/index.ts
+++ b/libs/tools/generator/core/src/types/index.ts
@@ -1,8 +1,11 @@
export * from "./boundary";
export * from "./catchall-generator-options";
+export * from "./credential-category";
+export * from "./credential-generator";
export * from "./credential-generator-configuration";
export * from "./eff-username-generator-options";
export * from "./forwarder-options";
+export * from "./generated-credential";
export * from "./generator-options";
export * from "./generator-type";
export * from "./no-policy";
diff --git a/libs/tools/generator/core/src/util.spec.ts b/libs/tools/generator/core/src/util.spec.ts
index 32bdc3ad3a..7ffd869535 100644
--- a/libs/tools/generator/core/src/util.spec.ts
+++ b/libs/tools/generator/core/src/util.spec.ts
@@ -1,4 +1,5 @@
-import { sum } from "./util";
+import { DefaultPassphraseGenerationOptions } from "./data";
+import { optionsToEffWordListRequest, optionsToRandomAsciiRequest, sum } from "./util";
describe("sum", () => {
it("returns 0 when the list is empty", () => {
@@ -15,3 +16,411 @@ describe("sum", () => {
expect(sum(1, 2, 3)).toBe(6);
});
});
+
+describe("optionsToRandomAsciiRequest", () => {
+ it("should map options", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 20,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: true,
+ special: true,
+ minUppercase: 1,
+ minLowercase: 2,
+ minNumber: 3,
+ minSpecial: 4,
+ });
+
+ expect(result).toEqual({
+ all: 10,
+ uppercase: 1,
+ lowercase: 2,
+ digits: 3,
+ special: 4,
+ ambiguous: true,
+ });
+ });
+
+ it("should disable uppercase", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 3,
+ ambiguous: true,
+ uppercase: false,
+ lowercase: true,
+ number: true,
+ special: true,
+ minUppercase: 1,
+ minLowercase: 1,
+ minNumber: 1,
+ minSpecial: 1,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: undefined,
+ lowercase: 1,
+ digits: 1,
+ special: 1,
+ ambiguous: true,
+ });
+ });
+
+ it("should disable lowercase", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 3,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: false,
+ number: true,
+ special: true,
+ minUppercase: 1,
+ minLowercase: 1,
+ minNumber: 1,
+ minSpecial: 1,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 1,
+ lowercase: undefined,
+ digits: 1,
+ special: 1,
+ ambiguous: true,
+ });
+ });
+
+ it("should disable digits", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 3,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: false,
+ special: true,
+ minUppercase: 1,
+ minLowercase: 1,
+ minNumber: 1,
+ minSpecial: 1,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 1,
+ lowercase: 1,
+ digits: undefined,
+ special: 1,
+ ambiguous: true,
+ });
+ });
+
+ it("should disable special", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 3,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: true,
+ special: false,
+ minUppercase: 1,
+ minLowercase: 1,
+ minNumber: 1,
+ minSpecial: 1,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 1,
+ lowercase: 1,
+ digits: 1,
+ special: undefined,
+ ambiguous: true,
+ });
+ });
+
+ it("should override length with minimums", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 20,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: true,
+ special: true,
+ minUppercase: 1,
+ minLowercase: 2,
+ minNumber: 3,
+ minSpecial: 4,
+ });
+
+ expect(result).toEqual({
+ all: 10,
+ uppercase: 1,
+ lowercase: 2,
+ digits: 3,
+ special: 4,
+ ambiguous: true,
+ });
+ });
+
+ it("should default uppercase", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 2,
+ ambiguous: true,
+ lowercase: true,
+ number: true,
+ special: true,
+ minUppercase: 2,
+ minLowercase: 0,
+ minNumber: 0,
+ minSpecial: 0,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 2,
+ lowercase: 0,
+ digits: 0,
+ special: 0,
+ ambiguous: true,
+ });
+ });
+
+ it("should default lowercase", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 0,
+ ambiguous: true,
+ uppercase: true,
+ number: true,
+ special: true,
+ minUppercase: 0,
+ minLowercase: 2,
+ minNumber: 0,
+ minSpecial: 0,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 0,
+ lowercase: 2,
+ digits: 0,
+ special: 0,
+ ambiguous: true,
+ });
+ });
+
+ it("should default number", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 0,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ special: true,
+ minUppercase: 0,
+ minLowercase: 0,
+ minNumber: 2,
+ minSpecial: 0,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 0,
+ lowercase: 0,
+ digits: 2,
+ special: 0,
+ ambiguous: true,
+ });
+ });
+
+ it("should default special", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 0,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: true,
+ minUppercase: 0,
+ minLowercase: 0,
+ minNumber: 0,
+ minSpecial: 0,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 0,
+ lowercase: 0,
+ digits: 0,
+ special: undefined,
+ ambiguous: true,
+ });
+ });
+
+ it("should default minUppercase", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 0,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: true,
+ special: true,
+ minLowercase: 0,
+ minNumber: 0,
+ minSpecial: 0,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 1,
+ lowercase: 0,
+ digits: 0,
+ special: 0,
+ ambiguous: true,
+ });
+ });
+
+ it("should default minLowercase", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 0,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: true,
+ special: true,
+ minUppercase: 0,
+ minNumber: 0,
+ minSpecial: 0,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 0,
+ lowercase: 1,
+ digits: 0,
+ special: 0,
+ ambiguous: true,
+ });
+ });
+
+ it("should default minNumber", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 0,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: true,
+ special: true,
+ minUppercase: 0,
+ minLowercase: 0,
+ minSpecial: 0,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 0,
+ lowercase: 0,
+ digits: 1,
+ special: 0,
+ ambiguous: true,
+ });
+ });
+
+ it("should default minSpecial", async () => {
+ const result = optionsToRandomAsciiRequest({
+ length: 0,
+ ambiguous: true,
+ uppercase: true,
+ lowercase: true,
+ number: true,
+ special: true,
+ minUppercase: 0,
+ minLowercase: 0,
+ minNumber: 0,
+ });
+
+ expect(result).toEqual({
+ all: 0,
+ uppercase: 0,
+ lowercase: 0,
+ digits: 0,
+ special: 0,
+ ambiguous: true,
+ });
+ });
+});
+
+describe("optionsToEffWordListRequest", () => {
+ it("should map options", async () => {
+ const result = optionsToEffWordListRequest({
+ numWords: 4,
+ capitalize: true,
+ includeNumber: true,
+ wordSeparator: "!",
+ });
+
+ expect(result).toEqual({
+ numberOfWords: 4,
+ capitalize: true,
+ number: true,
+ separator: "!",
+ });
+ });
+
+ it("should default numWords", async () => {
+ const result = optionsToEffWordListRequest({
+ capitalize: true,
+ includeNumber: true,
+ wordSeparator: "!",
+ });
+
+ expect(result).toEqual({
+ numberOfWords: DefaultPassphraseGenerationOptions.numWords,
+ capitalize: true,
+ number: true,
+ separator: "!",
+ });
+ });
+
+ it("should default capitalize", async () => {
+ const result = optionsToEffWordListRequest({
+ numWords: 4,
+ includeNumber: true,
+ wordSeparator: "!",
+ });
+
+ expect(result).toEqual({
+ numberOfWords: 4,
+ capitalize: DefaultPassphraseGenerationOptions.capitalize,
+ number: true,
+ separator: "!",
+ });
+ });
+
+ it("should default includeNumber", async () => {
+ const result = optionsToEffWordListRequest({
+ numWords: 4,
+ capitalize: true,
+ wordSeparator: "!",
+ });
+
+ expect(result).toEqual({
+ numberOfWords: 4,
+ capitalize: true,
+ number: DefaultPassphraseGenerationOptions.includeNumber,
+ separator: "!",
+ });
+ });
+
+ it("should default wordSeparator", async () => {
+ const result = optionsToEffWordListRequest({
+ numWords: 4,
+ capitalize: true,
+ includeNumber: true,
+ });
+
+ expect(result).toEqual({
+ numberOfWords: 4,
+ capitalize: true,
+ number: true,
+ separator: DefaultPassphraseGenerationOptions.wordSeparator,
+ });
+ });
+});
diff --git a/libs/tools/generator/core/src/util.ts b/libs/tools/generator/core/src/util.ts
index cca2c75834..21e901765e 100644
--- a/libs/tools/generator/core/src/util.ts
+++ b/libs/tools/generator/core/src/util.ts
@@ -7,6 +7,13 @@ import {
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
+import {
+ DefaultPassphraseBoundaries,
+ DefaultPassphraseGenerationOptions,
+ DefaultPasswordGenerationOptions,
+} from "./data";
+import { PassphraseGenerationOptions, PasswordGenerationOptions } from "./types";
+
/** construct a method that outputs a copy of `defaultValue` as an observable. */
export function observe$PerUserId(
create: () => Partial,
@@ -50,3 +57,79 @@ export function sharedStateByUserId(key: UserKeyDefinition, provid
/** returns the sum of items in the list. */
export const sum = (...items: number[]) =>
(items ?? []).reduce((sum: number, current: number) => sum + (current ?? 0), 0);
+
+/* converts password generation option sets, which are defined by
+ * an "enabled" and "quantity" parameter, to the password engine's
+ * parameters, which represent disabled options as `undefined`
+ * properties.
+ */
+export function optionsToRandomAsciiRequest(options: PasswordGenerationOptions) {
+ // helper for processing common option sets
+ function process(
+ // values read from the options
+ enabled: boolean,
+ quantity: number,
+ // value used if an option is missing
+ defaultEnabled: boolean,
+ defaultQuantity: number,
+ ) {
+ const isEnabled = enabled ?? defaultEnabled;
+ const actualQuantity = quantity ?? defaultQuantity;
+ const result = isEnabled ? actualQuantity : undefined;
+
+ return result;
+ }
+
+ const request = {
+ uppercase: process(
+ options.uppercase,
+ options.minUppercase,
+ DefaultPasswordGenerationOptions.uppercase,
+ DefaultPasswordGenerationOptions.minUppercase,
+ ),
+ lowercase: process(
+ options.lowercase,
+ options.minLowercase,
+ DefaultPasswordGenerationOptions.lowercase,
+ DefaultPasswordGenerationOptions.minLowercase,
+ ),
+ digits: process(
+ options.number,
+ options.minNumber,
+ DefaultPasswordGenerationOptions.number,
+ DefaultPasswordGenerationOptions.minNumber,
+ ),
+ special: process(
+ options.special,
+ options.minSpecial,
+ DefaultPasswordGenerationOptions.special,
+ DefaultPasswordGenerationOptions.minSpecial,
+ ),
+ ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous,
+ all: 0,
+ };
+
+ // engine represents character sets as "include only"; you assert how many all
+ // characters there can be rather than a total length. This conversion has
+ // the character classes win, so that the result is always consistent with policy
+ // minimums.
+ const required = sum(request.uppercase, request.lowercase, request.digits, request.special);
+ const remaining = (options.length ?? 0) - required;
+ request.all = Math.max(remaining, 0);
+
+ return request;
+}
+
+/* converts passphrase generation option sets to the eff word list request
+ */
+export function optionsToEffWordListRequest(options: PassphraseGenerationOptions) {
+ const requestWords = options.numWords ?? DefaultPassphraseGenerationOptions.numWords;
+ const request = {
+ numberOfWords: Math.max(requestWords, DefaultPassphraseBoundaries.numWords.min),
+ capitalize: options.capitalize ?? DefaultPassphraseGenerationOptions.capitalize,
+ number: options.includeNumber ?? DefaultPassphraseGenerationOptions.includeNumber,
+ separator: options.wordSeparator ?? DefaultPassphraseGenerationOptions.wordSeparator,
+ };
+
+ return request;
+}
diff --git a/libs/tools/send/send-ui/src/icons/no-send.icon.ts b/libs/tools/send/send-ui/src/icons/no-send.icon.ts
index e1442ad702..555d802460 100644
--- a/libs/tools/send/send-ui/src/icons/no-send.icon.ts
+++ b/libs/tools/send/send-ui/src/icons/no-send.icon.ts
@@ -2,12 +2,12 @@ import { svgIcon } from "@bitwarden/components";
export const NoSendsIcon = svgIcon`
`;
diff --git a/libs/tools/send/send-ui/src/icons/send-created.icon.ts b/libs/tools/send/send-ui/src/icons/send-created.icon.ts
index bb4bc2dd3b..099baebb9a 100644
--- a/libs/tools/send/send-ui/src/icons/send-created.icon.ts
+++ b/libs/tools/send/send-ui/src/icons/send-created.icon.ts
@@ -2,12 +2,12 @@ import { svgIcon } from "@bitwarden/components";
export const SendCreatedIcon = svgIcon`