From 6d792314760e96388682f6a3d5215d73e1a9ff78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Wed, 7 Feb 2024 13:24:32 -0500 Subject: [PATCH] [PM-5610] add eff long word list generator (#7748) --- .../default-policy-evaluator.spec.ts | 43 ++++++++++ .../generator/default-policy-evaluator.ts | 27 +++++++ .../tools/generator/key-definition.spec.ts | 15 +--- .../src/tools/generator/key-definitions.ts | 16 +--- libs/common/src/tools/generator/no-policy.ts | 2 + .../eff-username-generator-options.ts | 11 +++ .../eff-username-generator-strategy.spec.ts | 80 +++++++++++++++++++ .../eff-username-generator-strategy.ts | 53 ++++++++++++ .../src/tools/generator/username/index.ts | 1 + .../username/username-generation-options.ts | 6 +- 10 files changed, 227 insertions(+), 27 deletions(-) create mode 100644 libs/common/src/tools/generator/default-policy-evaluator.spec.ts create mode 100644 libs/common/src/tools/generator/default-policy-evaluator.ts create mode 100644 libs/common/src/tools/generator/no-policy.ts create mode 100644 libs/common/src/tools/generator/username/eff-username-generator-options.ts create mode 100644 libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts create mode 100644 libs/common/src/tools/generator/username/eff-username-generator-strategy.ts diff --git a/libs/common/src/tools/generator/default-policy-evaluator.spec.ts b/libs/common/src/tools/generator/default-policy-evaluator.spec.ts new file mode 100644 index 0000000000..d5d5e81028 --- /dev/null +++ b/libs/common/src/tools/generator/default-policy-evaluator.spec.ts @@ -0,0 +1,43 @@ +import { DefaultPolicyEvaluator } from "./default-policy-evaluator"; + +describe("Password generator options builder", () => { + describe("policy", () => { + it("should return an empty object", () => { + const builder = new DefaultPolicyEvaluator(); + + expect(builder.policy).toEqual({}); + }); + }); + + describe("policyInEffect", () => { + it("should return false", () => { + const builder = new DefaultPolicyEvaluator(); + + expect(builder.policyInEffect).toEqual(false); + }); + }); + + describe("applyPolicy(options)", () => { + // All tests should freeze the options to ensure they are not modified + it("should return the input operations without altering them", () => { + const builder = new DefaultPolicyEvaluator(); + const options = Object.freeze({}); + + const sanitizedOptions = builder.applyPolicy(options); + + expect(sanitizedOptions).toEqual(options); + }); + }); + + describe("sanitize(options)", () => { + // All tests should freeze the options to ensure they are not modified + it("should return the input options without altering them", () => { + const builder = new DefaultPolicyEvaluator(); + const options = Object.freeze({}); + + const sanitizedOptions = builder.sanitize(options); + + expect(sanitizedOptions).toEqual(options); + }); + }); +}); diff --git a/libs/common/src/tools/generator/default-policy-evaluator.ts b/libs/common/src/tools/generator/default-policy-evaluator.ts new file mode 100644 index 0000000000..d77ea2bbbc --- /dev/null +++ b/libs/common/src/tools/generator/default-policy-evaluator.ts @@ -0,0 +1,27 @@ +import { PolicyEvaluator } from "./abstractions"; +import { NoPolicy } from "./no-policy"; + +/** A policy evaluator that does not apply any policy */ +export class DefaultPolicyEvaluator + implements PolicyEvaluator +{ + /** {@link PolicyEvaluator.policy} */ + get policy() { + return {}; + } + + /** {@link PolicyEvaluator.policyInEffect} */ + get policyInEffect() { + return false; + } + + /** {@link PolicyEvaluator.applyPolicy} */ + applyPolicy(options: PolicyTarget) { + return options; + } + + /** {@link PolicyEvaluator.sanitize} */ + sanitize(options: PolicyTarget) { + return options; + } +} diff --git a/libs/common/src/tools/generator/key-definition.spec.ts b/libs/common/src/tools/generator/key-definition.spec.ts index 10ff62d22b..3c092cf3a7 100644 --- a/libs/common/src/tools/generator/key-definition.spec.ts +++ b/libs/common/src/tools/generator/key-definition.spec.ts @@ -1,9 +1,8 @@ import { ENCRYPTED_HISTORY, - ENCRYPTED_USERNAME_SETTINGS, + EFF_USERNAME_SETTINGS, PASSPHRASE_SETTINGS, PASSWORD_SETTINGS, - PLAINTEXT_USERNAME_SETTINGS, } from "./key-definitions"; describe("Key definitions", () => { @@ -23,18 +22,10 @@ describe("Key definitions", () => { }); }); - describe("ENCRYPTED_USERNAME_SETTINGS", () => { + describe("BASIC_LATIN_SETTINGS", () => { it("should pass through deserialization", () => { const value = {}; - const result = ENCRYPTED_USERNAME_SETTINGS.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("PLAINTEXT_USERNAME_SETTINGS", () => { - it("should pass through deserialization", () => { - const value = {}; - const result = PLAINTEXT_USERNAME_SETTINGS.deserializer(value); + const result = EFF_USERNAME_SETTINGS.deserializer(value); expect(result).toBe(value); }); }); diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index f5856fc4ac..4c543761f1 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -1,8 +1,9 @@ -import { GENERATOR_DISK, GENERATOR_MEMORY, KeyDefinition } from "../../platform/state"; +import { GENERATOR_DISK, KeyDefinition } from "../../platform/state"; import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options"; import { GeneratedPasswordHistory } from "./password/generated-password-history"; import { PasswordGenerationOptions } from "./password/password-generation-options"; +import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options"; /** plaintext password generation options */ export const PASSWORD_SETTINGS = new KeyDefinition( @@ -23,18 +24,9 @@ export const PASSPHRASE_SETTINGS = new KeyDefinition( +export const EFF_USERNAME_SETTINGS = new KeyDefinition( GENERATOR_DISK, - "usernameGeneratorSettings", - { - deserializer: (value) => value, - }, -); - -/** plaintext username generation options */ -export const PLAINTEXT_USERNAME_SETTINGS = new KeyDefinition( - GENERATOR_MEMORY, - "usernameGeneratorSettings", + "effUsernameGeneratorSettings", { deserializer: (value) => value, }, diff --git a/libs/common/src/tools/generator/no-policy.ts b/libs/common/src/tools/generator/no-policy.ts new file mode 100644 index 0000000000..00ffc6098c --- /dev/null +++ b/libs/common/src/tools/generator/no-policy.ts @@ -0,0 +1,2 @@ +/** Type representing an absence of policy. */ +export type NoPolicy = Record; diff --git a/libs/common/src/tools/generator/username/eff-username-generator-options.ts b/libs/common/src/tools/generator/username/eff-username-generator-options.ts new file mode 100644 index 0000000000..868149c2fd --- /dev/null +++ b/libs/common/src/tools/generator/username/eff-username-generator-options.ts @@ -0,0 +1,11 @@ +/** Settings supported when generating an ASCII username */ +export type EffUsernameGenerationOptions = { + wordCapitalize?: boolean; + wordIncludeNumber?: boolean; +}; + +/** The default options for EFF long word generation. */ +export const DefaultEffUsernameOptions: Partial = Object.freeze({ + wordCapitalize: false, + wordIncludeNumber: false, +}); diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts new file mode 100644 index 0000000000..2433ae34f1 --- /dev/null +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts @@ -0,0 +1,80 @@ +import { mock } from "jest-mock-extended"; + +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { EFF_USERNAME_SETTINGS } from "../key-definitions"; + +import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; + +describe("EFF long word list generation strategy", () => { + describe("evaluator()", () => { + it("should throw if the policy type is incorrect", () => { + const strategy = new EffUsernameGeneratorStrategy(null); + const policy = mock({ + type: PolicyType.DisableSend, + }); + + expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); + }); + + it("should map to the policy evaluator", () => { + const strategy = new EffUsernameGeneratorStrategy(null); + const policy = mock({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, + }); + + const evaluator = strategy.evaluator(policy); + + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + expect(evaluator.policy).toMatchObject({}); + }); + }); + + describe("disk", () => { + it("should use password settings key", () => { + const legacy = mock(); + const strategy = new EffUsernameGeneratorStrategy(legacy); + + expect(strategy.disk).toBe(EFF_USERNAME_SETTINGS); + }); + }); + + describe("cache_ms", () => { + it("should be a positive non-zero number", () => { + const legacy = mock(); + const strategy = new EffUsernameGeneratorStrategy(legacy); + + expect(strategy.cache_ms).toBeGreaterThan(0); + }); + }); + + describe("policy", () => { + it("should use password generator policy", () => { + const legacy = mock(); + const strategy = new EffUsernameGeneratorStrategy(legacy); + + expect(strategy.policy).toBe(PolicyType.PasswordGenerator); + }); + }); + + describe("generate()", () => { + it("should call the legacy service with the given options", async () => { + const legacy = mock(); + const strategy = new EffUsernameGeneratorStrategy(legacy); + const options = { + wordCapitalize: false, + wordIncludeNumber: false, + }; + + await strategy.generate(options); + + expect(legacy.generateWord).toHaveBeenCalledWith(options); + }); + }); +}); diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts new file mode 100644 index 0000000000..fb878c16c9 --- /dev/null +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts @@ -0,0 +1,53 @@ +import { PolicyType } from "../../../admin-console/enums"; +import { Policy } from "../../../admin-console/models/domain/policy"; +import { GeneratorStrategy } from "../abstractions"; +import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { EFF_USERNAME_SETTINGS } from "../key-definitions"; +import { NoPolicy } from "../no-policy"; + +import { EffUsernameGenerationOptions } from "./eff-username-generator-options"; +import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; + +const ONE_MINUTE = 60 * 1000; + +/** Strategy for creating usernames from the EFF wordlist */ +export class EffUsernameGeneratorStrategy + implements GeneratorStrategy +{ + /** Instantiates the generation strategy + * @param usernameService generates a username from EFF word list + */ + constructor(private usernameService: UsernameGenerationServiceAbstraction) {} + + /** {@link GeneratorStrategy.disk} */ + get disk() { + return EFF_USERNAME_SETTINGS; + } + + /** {@link GeneratorStrategy.policy} */ + get policy() { + // Uses password generator since there aren't policies + // specific to usernames. + return PolicyType.PasswordGenerator; + } + + /** {@link GeneratorStrategy.cache_ms} */ + get cache_ms() { + return ONE_MINUTE; + } + + /** {@link GeneratorStrategy.evaluator} */ + evaluator(policy: Policy) { + if (policy.type !== this.policy) { + const details = `Expected: ${this.policy}. Received: ${policy.type}`; + throw Error("Mismatched policy type. " + details); + } + + return new DefaultPolicyEvaluator(); + } + + /** {@link GeneratorStrategy.generate} */ + generate(options: EffUsernameGenerationOptions) { + return this.usernameService.generateWord(options); + } +} diff --git a/libs/common/src/tools/generator/username/index.ts b/libs/common/src/tools/generator/username/index.ts index c4197b4344..97291d3900 100644 --- a/libs/common/src/tools/generator/username/index.ts +++ b/libs/common/src/tools/generator/username/index.ts @@ -1,3 +1,4 @@ +export { EffUsernameGeneratorStrategy } from "./eff-username-generator-strategy"; export { UsernameGeneratorOptions } from "./username-generation-options"; export { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; export { UsernameGenerationService } from "./username-generation.service"; diff --git a/libs/common/src/tools/generator/username/username-generation-options.ts b/libs/common/src/tools/generator/username/username-generation-options.ts index 276668de96..2cb1e8dfd6 100644 --- a/libs/common/src/tools/generator/username/username-generation-options.ts +++ b/libs/common/src/tools/generator/username/username-generation-options.ts @@ -1,7 +1,7 @@ -export type UsernameGeneratorOptions = { +import { EffUsernameGenerationOptions } from "./eff-username-generator-options"; + +export type UsernameGeneratorOptions = EffUsernameGenerationOptions & { type?: "word" | "subaddress" | "catchall" | "forwarded"; - wordCapitalize?: boolean; - wordIncludeNumber?: boolean; subaddressType?: "random" | "website-name"; subaddressEmail?: string; catchallType?: "random" | "website-name";