diff --git a/libs/common/src/tools/generator/abstractions/randomizer.ts b/libs/common/src/tools/generator/abstractions/randomizer.ts new file mode 100644 index 0000000000..3322247759 --- /dev/null +++ b/libs/common/src/tools/generator/abstractions/randomizer.ts @@ -0,0 +1,39 @@ +import { WordOptions } from "../word-options"; + +/** Entropy source for credential generation. */ +export interface Randomizer { + /** picks a random entry from a list. + * @param list random entry source. This must have at least one entry. + * @returns a promise that resolves with a random entry from the list. + */ + pick(list: Array): Promise; + + /** picks a random word from a list. + * @param list random entry source. This must have at least one entry. + * @param options customizes the output word + * @returns a promise that resolves with a random word from the list. + */ + pickWord(list: Array, options?: WordOptions): Promise; + + /** Shuffles a list of items + * @param list random entry source. This must have at least two entries. + * @param options.copy shuffles a copy of the input when this is true. + * Defaults to true. + * @returns a promise that resolves with the randomized list. + */ + shuffle(items: Array): Promise>; + + /** Generates a string containing random lowercase ASCII characters and numbers. + * @param length the number of characters to generate + * @returns a promise that resolves with the randomized string. + */ + chars(length: number): Promise; + + /** Selects an integer value from a range by randomly choosing it from + * a uniform distribution. + * @param min the minimum value in the range, inclusive. + * @param max the minimum value in the range, inclusive. + * @returns a promise that resolves with the randomized string. + */ + uniform(min: number, max: number): Promise; +} diff --git a/libs/common/src/tools/generator/legacy-password-generation.service.ts b/libs/common/src/tools/generator/legacy-password-generation.service.ts index db4aa9d2a9..d69d4d2dc0 100644 --- a/libs/common/src/tools/generator/legacy-password-generation.service.ts +++ b/libs/common/src/tools/generator/legacy-password-generation.service.ts @@ -40,11 +40,11 @@ import { import { GeneratedPasswordHistory, PasswordGenerationOptions, - PasswordGenerationService, PasswordGeneratorOptions, PasswordGeneratorPolicy, PasswordGeneratorStrategy, } from "./password"; +import { CryptoServiceRandomizer } from "./random"; type MappedOptions = { generator: GeneratorNavigation; @@ -60,17 +60,15 @@ export function legacyPasswordGenerationServiceFactory( accountService: AccountService, stateProvider: StateProvider, ): PasswordGenerationServiceAbstraction { - // FIXME: Once the password generation service is replaced with this service - // in the clients, factor out the deprecated service in its entirety. - const deprecatedService = new PasswordGenerationService(cryptoService, null, null); + const randomizer = new CryptoServiceRandomizer(cryptoService); const passwords = new DefaultGeneratorService( - new PasswordGeneratorStrategy(deprecatedService, stateProvider), + new PasswordGeneratorStrategy(randomizer, stateProvider), policyService, ); const passphrases = new DefaultGeneratorService( - new PassphraseGeneratorStrategy(deprecatedService, stateProvider), + new PassphraseGeneratorStrategy(randomizer, stateProvider), policyService, ); diff --git a/libs/common/src/tools/generator/legacy-username-generation.service.ts b/libs/common/src/tools/generator/legacy-username-generation.service.ts index 61c19ee314..aaa6bc2c80 100644 --- a/libs/common/src/tools/generator/legacy-username-generation.service.ts +++ b/libs/common/src/tools/generator/legacy-username-generation.service.ts @@ -14,6 +14,7 @@ import { DefaultGeneratorService } from "./default-generator.service"; import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service"; import { GeneratorNavigation } from "./navigation/generator-navigation"; import { NoPolicy } from "./no-policy"; +import { CryptoServiceRandomizer } from "./random"; import { CatchallGeneratorStrategy, SubaddressGeneratorStrategy, @@ -37,7 +38,6 @@ import { } from "./username/options/forwarder-options"; import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; import { UsernameGeneratorOptions } from "./username/username-generation-options"; -import { UsernameGenerationService } from "./username/username-generation.service"; type MappedOptions = { generator: GeneratorNavigation; @@ -65,22 +65,20 @@ export function legacyUsernameGenerationServiceFactory( accountService: AccountService, stateProvider: StateProvider, ): UsernameGenerationServiceAbstraction { - // FIXME: Once the username generation service is replaced with this service - // in the clients, factor out the deprecated service in its entirety. - const deprecatedService = new UsernameGenerationService(cryptoService, null, null); + const randomizer = new CryptoServiceRandomizer(cryptoService); const effUsername = new DefaultGeneratorService( - new EffUsernameGeneratorStrategy(deprecatedService, stateProvider), + new EffUsernameGeneratorStrategy(randomizer, stateProvider), policyService, ); const subaddress = new DefaultGeneratorService( - new SubaddressGeneratorStrategy(deprecatedService, stateProvider), + new SubaddressGeneratorStrategy(randomizer, stateProvider), policyService, ); const catchall = new DefaultGeneratorService( - new CatchallGeneratorStrategy(deprecatedService, stateProvider), + new CatchallGeneratorStrategy(randomizer, stateProvider), policyService, ); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts index 6ad1bd90dd..429f81175a 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts @@ -11,7 +11,7 @@ import { PolicyType } from "../../../admin-console/enums"; import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; -import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; +import { Randomizer } from "../abstractions/randomizer"; import { PASSPHRASE_SETTINGS } from "../key-definitions"; import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy"; @@ -65,8 +65,8 @@ describe("Password generation strategy", () => { describe("durableState", () => { it("should use password settings key", () => { const provider = mock(); - const legacy = mock(); - const strategy = new PassphraseGeneratorStrategy(legacy, provider); + const randomizer = mock(); + const strategy = new PassphraseGeneratorStrategy(randomizer, provider); strategy.durableState(SomeUser); @@ -86,36 +86,14 @@ describe("Password generation strategy", () => { describe("policy", () => { it("should use password generator policy", () => { - const legacy = mock(); - const strategy = new PassphraseGeneratorStrategy(legacy, null); + const randomizer = mock(); + const strategy = new PassphraseGeneratorStrategy(randomizer, null); 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 PassphraseGeneratorStrategy(legacy, null); - const options = { - type: "passphrase", - minNumberWords: 1, - capitalize: true, - includeNumber: true, - }; - - await strategy.generate(options); - - expect(legacy.generatePassphrase).toHaveBeenCalledWith(options); - }); - - it("should set the generation type to passphrase", async () => { - const legacy = mock(); - const strategy = new PassphraseGeneratorStrategy(legacy, null); - - await strategy.generate({ type: "foo" } as any); - - expect(legacy.generatePassphrase).toHaveBeenCalledWith({ type: "passphrase" }); - }); + it.todo("should generate a password using the given options"); }); }); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts index c7b5ff8b78..3ed6a1219c 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts @@ -1,25 +1,20 @@ -import { BehaviorSubject, map, pipe } from "rxjs"; - import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; +import { EFFLongWordList } from "../../../platform/misc/wordlist"; import { StateProvider } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; +import { Randomizer } from "../abstractions/randomizer"; import { PASSPHRASE_SETTINGS } from "../key-definitions"; -import { distinctIfShallowMatch, reduceCollection } from "../rx-operators"; +import { Policies } from "../policies"; +import { mapPolicyToEvaluator } from "../rx-operators"; +import { clone$PerUserId, sharedStateByUserId } from "../util"; import { PassphraseGenerationOptions, DefaultPassphraseGenerationOptions, } from "./passphrase-generation-options"; -import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; -import { - DisabledPassphraseGeneratorPolicy, - PassphraseGeneratorPolicy, - leastPrivilege, -} from "./passphrase-generator-policy"; +import { PassphraseGeneratorPolicy } from "./passphrase-generator-policy"; -/** {@link GeneratorStrategy} */ +/** Generates passphrases composed of random words */ export class PassphraseGeneratorStrategy implements GeneratorStrategy { @@ -28,36 +23,48 @@ export class PassphraseGeneratorStrategy * @param stateProvider provides durable state */ constructor( - private legacy: PasswordGenerationServiceAbstraction, + private randomizer: Randomizer, private stateProvider: StateProvider, ) {} - /** {@link GeneratorStrategy.durableState} */ - durableState(id: UserId) { - return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS); - } - - /** Gets the default options. */ - defaults$(_: UserId) { - return new BehaviorSubject({ ...DefaultPassphraseGenerationOptions }).asObservable(); - } - - /** {@link GeneratorStrategy.policy} */ - get policy() { - return PolicyType.PasswordGenerator; - } - - /** {@link GeneratorStrategy.toEvaluator} */ + // configuration + durableState = sharedStateByUserId(PASSPHRASE_SETTINGS, this.stateProvider); + defaults$ = clone$PerUserId(DefaultPassphraseGenerationOptions); + readonly policy = PolicyType.PasswordGenerator; toEvaluator() { - return pipe( - reduceCollection(leastPrivilege, DisabledPassphraseGeneratorPolicy), - distinctIfShallowMatch(), - map((policy) => new PassphraseGeneratorOptionsEvaluator(policy)), - ); + return mapPolicyToEvaluator(Policies.Passphrase); } - /** {@link GeneratorStrategy.generate} */ - generate(options: PassphraseGenerationOptions): Promise { - return this.legacy.generatePassphrase({ ...options, type: "passphrase" }); + // algorithm + async generate(options: PassphraseGenerationOptions): Promise { + const o = { ...DefaultPassphraseGenerationOptions, ...options }; + if (o.numWords == null || o.numWords <= 2) { + o.numWords = DefaultPassphraseGenerationOptions.numWords; + } + if (o.capitalize == null) { + o.capitalize = false; + } + if (o.includeNumber == null) { + o.includeNumber = false; + } + + // select which word gets the number, if any + let luckyNumber = -1; + if (o.includeNumber) { + luckyNumber = await this.randomizer.uniform(0, o.numWords); + } + + // generate the passphrase + const wordList = new Array(o.numWords); + for (let i = 0; i < o.numWords; i++) { + const word = await this.randomizer.pickWord(EFFLongWordList, { + titleCase: o.capitalize, + number: i === luckyNumber, + }); + + wordList[i] = word; + } + + return wordList.join(o.wordSeparator); } } diff --git a/libs/common/src/tools/generator/password/index.ts b/libs/common/src/tools/generator/password/index.ts index e17ab8201c..7e16a2c442 100644 --- a/libs/common/src/tools/generator/password/index.ts +++ b/libs/common/src/tools/generator/password/index.ts @@ -7,5 +7,4 @@ export { PasswordGeneratorStrategy } from "./password-generator-strategy"; // legacy interfaces export { PasswordGeneratorOptions } from "./password-generator-options"; export { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; -export { PasswordGenerationService } from "./password-generation.service"; export { GeneratedPasswordHistory } from "./generated-password-history"; diff --git a/libs/common/src/tools/generator/password/password-generation.service.ts b/libs/common/src/tools/generator/password/password-generation.service.ts deleted file mode 100644 index e193b0fd33..0000000000 --- a/libs/common/src/tools/generator/password/password-generation.service.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { from } from "rxjs"; - -import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "../../../admin-console/enums"; -import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; -import { StateService } from "../../../platform/abstractions/state.service"; -import { EFFLongWordList } from "../../../platform/misc/wordlist"; -import { EncString } from "../../../platform/models/domain/enc-string"; -import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; -import { PassphraseGeneratorOptionsEvaluator } from "../passphrase/passphrase-generator-options-evaluator"; - -import { GeneratedPasswordHistory } from "./generated-password-history"; -import { PasswordGeneratorOptions } from "./password-generator-options"; -import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; - -const DefaultOptions: PasswordGeneratorOptions = { - length: 14, - minLength: 5, - ambiguous: false, - number: true, - minNumber: 1, - uppercase: true, - minUppercase: 0, - lowercase: true, - minLowercase: 0, - special: false, - minSpecial: 0, - type: "password", - numWords: 3, - wordSeparator: "-", - capitalize: false, - includeNumber: false, -}; - -const DefaultPolicy = new PasswordGeneratorPolicyOptions(); - -const MaxPasswordsInHistory = 100; - -export class PasswordGenerationService implements PasswordGenerationServiceAbstraction { - constructor( - private cryptoService: CryptoService, - private policyService: PolicyService, - private stateService: StateService, - ) {} - - async generatePassword(options: PasswordGeneratorOptions): Promise { - if ((options.type ?? DefaultOptions.type) === "passphrase") { - return this.generatePassphrase({ ...DefaultOptions, ...options }); - } - - const evaluator = new PasswordGeneratorOptionsEvaluator(DefaultPolicy); - const o = evaluator.sanitize({ ...DefaultOptions, ...options }); - - const positions: string[] = []; - if (o.lowercase && o.minLowercase > 0) { - for (let i = 0; i < o.minLowercase; i++) { - positions.push("l"); - } - } - if (o.uppercase && o.minUppercase > 0) { - for (let i = 0; i < o.minUppercase; i++) { - positions.push("u"); - } - } - if (o.number && o.minNumber > 0) { - for (let i = 0; i < o.minNumber; i++) { - positions.push("n"); - } - } - if (o.special && o.minSpecial > 0) { - for (let i = 0; i < o.minSpecial; i++) { - positions.push("s"); - } - } - while (positions.length < o.length) { - positions.push("a"); - } - - // shuffle - await this.shuffleArray(positions); - - // build out the char sets - let allCharSet = ""; - - let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz"; - if (o.ambiguous) { - lowercaseCharSet += "l"; - } - if (o.lowercase) { - allCharSet += lowercaseCharSet; - } - - let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ"; - if (o.ambiguous) { - uppercaseCharSet += "IO"; - } - if (o.uppercase) { - allCharSet += uppercaseCharSet; - } - - let numberCharSet = "23456789"; - if (o.ambiguous) { - numberCharSet += "01"; - } - if (o.number) { - allCharSet += numberCharSet; - } - - const specialCharSet = "!@#$%^&*"; - if (o.special) { - allCharSet += specialCharSet; - } - - let password = ""; - for (let i = 0; i < o.length; i++) { - let positionChars: string; - switch (positions[i]) { - case "l": - positionChars = lowercaseCharSet; - break; - case "u": - positionChars = uppercaseCharSet; - break; - case "n": - positionChars = numberCharSet; - break; - case "s": - positionChars = specialCharSet; - break; - case "a": - positionChars = allCharSet; - break; - default: - break; - } - - const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1); - password += positionChars.charAt(randomCharIndex); - } - - return password; - } - - async generatePassphrase(options: PasswordGeneratorOptions): Promise { - const evaluator = new PassphraseGeneratorOptionsEvaluator(DefaultPolicy); - const o = evaluator.sanitize({ ...DefaultOptions, ...options }); - - if (o.numWords == null || o.numWords <= 2) { - o.numWords = DefaultOptions.numWords; - } - if (o.capitalize == null) { - o.capitalize = false; - } - if (o.includeNumber == null) { - o.includeNumber = false; - } - - const listLength = EFFLongWordList.length - 1; - const wordList = new Array(o.numWords); - for (let i = 0; i < o.numWords; i++) { - const wordIndex = await this.cryptoService.randomNumber(0, listLength); - if (o.capitalize) { - wordList[i] = this.capitalize(EFFLongWordList[wordIndex]); - } else { - wordList[i] = EFFLongWordList[wordIndex]; - } - } - - if (o.includeNumber) { - await this.appendRandomNumberToRandomWord(wordList); - } - return wordList.join(o.wordSeparator); - } - - getOptions$() { - return from(this.getOptions()); - } - - async getOptions(): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> { - let options = await this.stateService.getPasswordGenerationOptions(); - if (options == null) { - options = Object.assign({}, DefaultOptions); - } else { - options = Object.assign({}, DefaultOptions, options); - } - await this.stateService.setPasswordGenerationOptions(options); - const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(options); - options = enforcedOptions[0]; - return [options, enforcedOptions[1]]; - } - - async enforcePasswordGeneratorPoliciesOnOptions( - options: PasswordGeneratorOptions, - ): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> { - let policy = await this.getPasswordGeneratorPolicyOptions(); - policy = policy ?? new PasswordGeneratorPolicyOptions(); - - // Force default type if password/passphrase selected via policy - if (policy.defaultType === "password" || policy.defaultType === "passphrase") { - options.type = policy.defaultType; - } - - const evaluator = - options.type == "password" - ? new PasswordGeneratorOptionsEvaluator(policy) - : new PassphraseGeneratorOptionsEvaluator(policy); - - // Ensure the options to pass the current rules - const withPolicy = evaluator.applyPolicy(options); - const sanitized = evaluator.sanitize(withPolicy); - - // callers assume this function updates the options parameter - const result = Object.assign(options, sanitized); - return [result, policy]; - } - - async getPasswordGeneratorPolicyOptions(): Promise { - const policies = await this.policyService?.getAll(PolicyType.PasswordGenerator); - let enforcedOptions: PasswordGeneratorPolicyOptions = null; - - if (policies == null || policies.length === 0) { - return enforcedOptions; - } - - policies.forEach((currentPolicy) => { - if (!currentPolicy.enabled || currentPolicy.data == null) { - return; - } - - if (enforcedOptions == null) { - enforcedOptions = new PasswordGeneratorPolicyOptions(); - } - - // Password wins in multi-org collisions - if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== "password") { - enforcedOptions.defaultType = currentPolicy.data.defaultType; - } - - if ( - currentPolicy.data.minLength != null && - currentPolicy.data.minLength > enforcedOptions.minLength - ) { - enforcedOptions.minLength = currentPolicy.data.minLength; - } - - if (currentPolicy.data.useUpper) { - enforcedOptions.useUppercase = true; - } - - if (currentPolicy.data.useLower) { - enforcedOptions.useLowercase = true; - } - - if (currentPolicy.data.useNumbers) { - enforcedOptions.useNumbers = true; - } - - if ( - currentPolicy.data.minNumbers != null && - currentPolicy.data.minNumbers > enforcedOptions.numberCount - ) { - enforcedOptions.numberCount = currentPolicy.data.minNumbers; - } - - if (currentPolicy.data.useSpecial) { - enforcedOptions.useSpecial = true; - } - - if ( - currentPolicy.data.minSpecial != null && - currentPolicy.data.minSpecial > enforcedOptions.specialCount - ) { - enforcedOptions.specialCount = currentPolicy.data.minSpecial; - } - - if ( - currentPolicy.data.minNumberWords != null && - currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords - ) { - enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords; - } - - if (currentPolicy.data.capitalize) { - enforcedOptions.capitalize = true; - } - - if (currentPolicy.data.includeNumber) { - enforcedOptions.includeNumber = true; - } - }); - - return enforcedOptions; - } - - async saveOptions(options: PasswordGeneratorOptions) { - await this.stateService.setPasswordGenerationOptions(options); - } - - async getHistory(): Promise { - const hasKey = await this.cryptoService.hasUserKey(); - if (!hasKey) { - return new Array(); - } - - if ((await this.stateService.getDecryptedPasswordGenerationHistory()) == null) { - const encrypted = await this.stateService.getEncryptedPasswordGenerationHistory(); - const decrypted = await this.decryptHistory(encrypted); - await this.stateService.setDecryptedPasswordGenerationHistory(decrypted); - } - - const passwordGenerationHistory = - await this.stateService.getDecryptedPasswordGenerationHistory(); - return passwordGenerationHistory != null - ? passwordGenerationHistory - : new Array(); - } - - async addHistory(password: string): Promise { - // Cannot add new history if no key is available - const hasKey = await this.cryptoService.hasUserKey(); - if (!hasKey) { - return; - } - - const currentHistory = await this.getHistory(); - - // Prevent duplicates - if (this.matchesPrevious(password, currentHistory)) { - return; - } - - currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now())); - - // Remove old items. - if (currentHistory.length > MaxPasswordsInHistory) { - currentHistory.pop(); - } - - const newHistory = await this.encryptHistory(currentHistory); - await this.stateService.setDecryptedPasswordGenerationHistory(currentHistory); - return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory); - } - - async clear(userId?: string): Promise { - await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId }); - await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId }); - return []; - } - - private capitalize(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - private async appendRandomNumberToRandomWord(wordList: string[]) { - if (wordList == null || wordList.length <= 0) { - return; - } - const index = await this.cryptoService.randomNumber(0, wordList.length - 1); - const num = await this.cryptoService.randomNumber(0, 9); - wordList[index] = wordList[index] + num; - } - - private async encryptHistory( - history: GeneratedPasswordHistory[], - ): Promise { - if (history == null || history.length === 0) { - return Promise.resolve([]); - } - - const promises = history.map(async (item) => { - const encrypted = await this.cryptoService.encrypt(item.password); - return new GeneratedPasswordHistory(encrypted.encryptedString, item.date); - }); - - return await Promise.all(promises); - } - - private async decryptHistory( - history: GeneratedPasswordHistory[], - ): Promise { - if (history == null || history.length === 0) { - return Promise.resolve([]); - } - - const promises = history.map(async (item) => { - const decrypted = await this.cryptoService.decryptToUtf8(new EncString(item.password)); - return new GeneratedPasswordHistory(decrypted, item.date); - }); - - return await Promise.all(promises); - } - - private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean { - if (history == null || history.length === 0) { - return false; - } - - return history[history.length - 1].password === password; - } - - // ref: https://stackoverflow.com/a/12646864/1090359 - private async shuffleArray(array: string[]) { - for (let i = array.length - 1; i > 0; i--) { - const j = await this.cryptoService.randomNumber(0, i); - [array[i], array[j]] = [array[j], array[i]]; - } - } -} diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts index a7509e8b43..668dd818e2 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts @@ -12,13 +12,13 @@ import { PolicyType } from "../../../admin-console/enums"; import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { Randomizer } from "../abstractions/randomizer"; import { PASSWORD_SETTINGS } from "../key-definitions"; import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy"; import { DefaultPasswordGenerationOptions, - PasswordGenerationServiceAbstraction, PasswordGeneratorOptionsEvaluator, PasswordGeneratorStrategy, } from "."; @@ -74,8 +74,8 @@ describe("Password generation strategy", () => { describe("durableState", () => { it("should use password settings key", () => { const provider = mock(); - const legacy = mock(); - const strategy = new PasswordGeneratorStrategy(legacy, provider); + const randomizer = mock(); + const strategy = new PasswordGeneratorStrategy(randomizer, provider); strategy.durableState(SomeUser); @@ -95,40 +95,14 @@ describe("Password generation strategy", () => { describe("policy", () => { it("should use password generator policy", () => { - const legacy = mock(); - const strategy = new PasswordGeneratorStrategy(legacy, null); + const randomizer = mock(); + const strategy = new PasswordGeneratorStrategy(randomizer, null); 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 PasswordGeneratorStrategy(legacy, null); - const options = { - type: "password", - minLength: 1, - useUppercase: true, - useLowercase: true, - useNumbers: true, - numberCount: 1, - useSpecial: true, - specialCount: 1, - }; - - await strategy.generate(options); - - expect(legacy.generatePassword).toHaveBeenCalledWith(options); - }); - - it("should set the generation type to password", async () => { - const legacy = mock(); - const strategy = new PasswordGeneratorStrategy(legacy, null); - - await strategy.generate({ type: "foo" } as any); - - expect(legacy.generatePassword).toHaveBeenCalledWith({ type: "password" }); - }); + it.todo("should generate a password using the given options"); }); }); diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.ts b/libs/common/src/tools/generator/password/password-generator-strategy.ts index 23828d7b59..075c331e06 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.ts @@ -1,25 +1,19 @@ -import { BehaviorSubject, map, pipe } from "rxjs"; - import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; +import { Randomizer } from "../abstractions/randomizer"; import { PASSWORD_SETTINGS } from "../key-definitions"; -import { distinctIfShallowMatch, reduceCollection } from "../rx-operators"; +import { Policies } from "../policies"; +import { mapPolicyToEvaluator } from "../rx-operators"; +import { clone$PerUserId, sharedStateByUserId } from "../util"; import { DefaultPasswordGenerationOptions, PasswordGenerationOptions, } from "./password-generation-options"; -import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; -import { - DisabledPasswordGeneratorPolicy, - PasswordGeneratorPolicy, - leastPrivilege, -} from "./password-generator-policy"; +import { PasswordGeneratorPolicy } from "./password-generator-policy"; -/** {@link GeneratorStrategy} */ +/** Generates passwords composed of random characters */ export class PasswordGeneratorStrategy implements GeneratorStrategy { @@ -27,36 +21,108 @@ export class PasswordGeneratorStrategy * @param legacy generates the password */ constructor( - private legacy: PasswordGenerationServiceAbstraction, + private randomizer: Randomizer, private stateProvider: StateProvider, ) {} - /** {@link GeneratorStrategy.durableState} */ - durableState(id: UserId) { - return this.stateProvider.getUser(id, PASSWORD_SETTINGS); - } - - /** Gets the default options. */ - defaults$(_: UserId) { - return new BehaviorSubject({ ...DefaultPasswordGenerationOptions }).asObservable(); - } - - /** {@link GeneratorStrategy.policy} */ - get policy() { - return PolicyType.PasswordGenerator; - } - - /** {@link GeneratorStrategy.toEvaluator} */ + // configuration + durableState = sharedStateByUserId(PASSWORD_SETTINGS, this.stateProvider); + defaults$ = clone$PerUserId(DefaultPasswordGenerationOptions); + readonly policy = PolicyType.PasswordGenerator; toEvaluator() { - return pipe( - reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy), - distinctIfShallowMatch(), - map((policy) => new PasswordGeneratorOptionsEvaluator(policy)), - ); + return mapPolicyToEvaluator(Policies.Password); } - /** {@link GeneratorStrategy.generate} */ - generate(options: PasswordGenerationOptions): Promise { - return this.legacy.generatePassword({ ...options, type: "password" }); + // algorithm + async generate(options: PasswordGenerationOptions): Promise { + const o = { ...DefaultPasswordGenerationOptions, ...options }; + let positions: string[] = []; + if (o.lowercase && o.minLowercase > 0) { + for (let i = 0; i < o.minLowercase; i++) { + positions.push("l"); + } + } + if (o.uppercase && o.minUppercase > 0) { + for (let i = 0; i < o.minUppercase; i++) { + positions.push("u"); + } + } + if (o.number && o.minNumber > 0) { + for (let i = 0; i < o.minNumber; i++) { + positions.push("n"); + } + } + if (o.special && o.minSpecial > 0) { + for (let i = 0; i < o.minSpecial; i++) { + positions.push("s"); + } + } + while (positions.length < o.length) { + positions.push("a"); + } + + // shuffle + positions = await this.randomizer.shuffle(positions); + + // build out the char sets + let allCharSet = ""; + + let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz"; + if (o.ambiguous) { + lowercaseCharSet += "l"; + } + if (o.lowercase) { + allCharSet += lowercaseCharSet; + } + + let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ"; + if (o.ambiguous) { + uppercaseCharSet += "IO"; + } + if (o.uppercase) { + allCharSet += uppercaseCharSet; + } + + let numberCharSet = "23456789"; + if (o.ambiguous) { + numberCharSet += "01"; + } + if (o.number) { + allCharSet += numberCharSet; + } + + const specialCharSet = "!@#$%^&*"; + if (o.special) { + allCharSet += specialCharSet; + } + + let password = ""; + for (let i = 0; i < o.length; i++) { + let positionChars: string; + switch (positions[i]) { + case "l": + positionChars = lowercaseCharSet; + break; + case "u": + positionChars = uppercaseCharSet; + break; + case "n": + positionChars = numberCharSet; + break; + case "s": + positionChars = specialCharSet; + break; + case "a": + positionChars = allCharSet; + break; + default: + break; + } + + const randomCharIndex = await this.randomizer.uniform(0, positionChars.length - 1); + password += positionChars.charAt(randomCharIndex); + } + + return password; } } diff --git a/libs/common/src/tools/generator/policies.ts b/libs/common/src/tools/generator/policies.ts new file mode 100644 index 0000000000..27521f0eeb --- /dev/null +++ b/libs/common/src/tools/generator/policies.ts @@ -0,0 +1,48 @@ +import { Policy as AdminPolicy } from "@bitwarden/common/admin-console/models/domain/policy"; + +import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorPolicy } from "./passphrase"; +import { + DisabledPassphraseGeneratorPolicy, + leastPrivilege as passphraseLeastPrivilege, +} from "./passphrase/passphrase-generator-policy"; +import { PasswordGeneratorOptionsEvaluator, PasswordGeneratorPolicy } from "./password"; +import { + DisabledPasswordGeneratorPolicy, + leastPrivilege as passwordLeastPrivilege, +} from "./password/password-generator-policy"; + +/** Determines how to construct a password generator policy */ +export type PolicyConfiguration = { + /** The value of the policy when it is not in effect. */ + disabledValue: Policy; + + /** Combines multiple policies set by the administrative console into + * a single policy. + */ + combine: (acc: Policy, policy: AdminPolicy) => Policy; + + /** Converts policy service data into an actionable policy. + */ + createEvaluator: (policy: Policy) => Evaluator; +}; + +const PASSPHRASE = Object.freeze({ + disabledValue: DisabledPassphraseGeneratorPolicy, + combine: passphraseLeastPrivilege, + createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy), +} as PolicyConfiguration); + +const PASSWORD = Object.freeze({ + disabledValue: DisabledPasswordGeneratorPolicy, + combine: passwordLeastPrivilege, + createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy), +} as PolicyConfiguration); + +/** Policy configurations */ +export const Policies = Object.freeze({ + /** Passphrase policy configuration */ + Passphrase: PASSPHRASE, + + /** Passphrase policy configuration */ + Password: PASSWORD, +}); diff --git a/libs/common/src/tools/generator/random.ts b/libs/common/src/tools/generator/random.ts new file mode 100644 index 0000000000..255a5df7a7 --- /dev/null +++ b/libs/common/src/tools/generator/random.ts @@ -0,0 +1,62 @@ +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; + +import { Randomizer } from "./abstractions/randomizer"; +import { WordOptions } from "./word-options"; + +/** A randomizer backed by a CryptoService. */ +export class CryptoServiceRandomizer implements Randomizer { + constructor(private crypto: CryptoService) {} + + async pick(list: Array) { + const index = await this.uniform(0, list.length - 1); + return list[index]; + } + + async pickWord(list: Array, options?: WordOptions) { + let word = await this.pick(list); + + if (options?.titleCase ?? false) { + word = word.charAt(0).toUpperCase() + word.slice(1); + } + + if (options?.number ?? false) { + const num = await this.crypto.randomNumber(1, 9999); + word = word + this.zeroPad(num.toString(), 4); + } + + return word; + } + + // ref: https://stackoverflow.com/a/12646864/1090359 + async shuffle(items: Array, options?: { copy?: boolean }) { + const shuffled = options?.copy ?? true ? [...items] : items; + + for (let i = items.length - 1; i > 0; i--) { + const j = await this.uniform(0, i); + [items[i], items[j]] = [items[j], items[i]]; + } + + return shuffled; + } + + async chars(length: number) { + let str = ""; + const charSet = "abcdefghijklmnopqrstuvwxyz1234567890"; + for (let i = 0; i < length; i++) { + const randomCharIndex = await this.uniform(0, charSet.length - 1); + str += charSet.charAt(randomCharIndex); + } + return str; + } + + async uniform(min: number, max: number) { + return this.crypto.randomNumber(min, max); + } + + // ref: https://stackoverflow.com/a/10073788 + private zeroPad(number: string, width: number) { + return number.length >= width + ? number + : new Array(width - number.length + 1).join("0") + number; + } +} diff --git a/libs/common/src/tools/generator/rx-operators.ts b/libs/common/src/tools/generator/rx-operators.ts index 6524ef7994..47233fa778 100644 --- a/libs/common/src/tools/generator/rx-operators.ts +++ b/libs/common/src/tools/generator/rx-operators.ts @@ -1,4 +1,7 @@ -import { distinctUntilChanged, map, OperatorFunction } from "rxjs"; +import { distinctUntilChanged, map, OperatorFunction, pipe } from "rxjs"; + +import { DefaultPolicyEvaluator } from "./default-policy-evaluator"; +import { PolicyConfiguration } from "./policies"; /** * An observable operator that reduces an emitted collection to a single object, @@ -36,3 +39,23 @@ export function distinctIfShallowMatch(): OperatorFunction { return isDistinct; }); } + +/** Maps an administrative console policy to a policy evaluator using the provided configuration. + * @param configuration the configuration that constructs the evaluator. + */ +export function mapPolicyToEvaluator( + configuration: PolicyConfiguration, +) { + return pipe( + reduceCollection(configuration.combine, configuration.disabledValue), + distinctIfShallowMatch(), + map(configuration.createEvaluator), + ); +} + +/** Constructs a method that maps a policy to the default (no-op) policy. */ +export function newDefaultEvaluator() { + return () => { + return pipe(map((_) => new DefaultPolicyEvaluator())); + }; +} diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts index 30f11b1e89..45e8716081 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts @@ -7,12 +7,13 @@ import { PolicyType } from "../../../admin-console/enums"; import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { Randomizer } from "../abstractions/randomizer"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { CATCHALL_SETTINGS } from "../key-definitions"; -import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options"; +import { DefaultCatchallOptions } from "./catchall-generator-options"; -import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; +import { CatchallGeneratorStrategy } from "."; const SomeUser = "some user" as UserId; const SomePolicy = mock({ @@ -40,8 +41,8 @@ describe("Email subaddress list generation strategy", () => { describe("durableState", () => { it("should use password settings key", () => { const provider = mock(); - const legacy = mock(); - const strategy = new CatchallGeneratorStrategy(legacy, provider); + const randomizer = mock(); + const strategy = new CatchallGeneratorStrategy(randomizer, provider); strategy.durableState(SomeUser); @@ -61,26 +62,14 @@ describe("Email subaddress list generation strategy", () => { describe("policy", () => { it("should use password generator policy", () => { - const legacy = mock(); - const strategy = new CatchallGeneratorStrategy(legacy, null); + const randomizer = mock(); + const strategy = new CatchallGeneratorStrategy(randomizer, null); 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 CatchallGeneratorStrategy(legacy, null); - const options = { - catchallType: "website-name", - catchallDomain: "example.com", - website: "foo.com", - } as CatchallGenerationOptions; - - await strategy.generate(options); - - expect(legacy.generateCatchall).toHaveBeenCalledWith(options); - }); + it.todo("generate catchall email addresses"); }); }); diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts index ee86fd9fd6..fb015a596f 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts @@ -1,13 +1,11 @@ -import { BehaviorSubject, map, pipe } from "rxjs"; - import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; -import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; -import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { Randomizer } from "../abstractions/randomizer"; import { CATCHALL_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; +import { newDefaultEvaluator } from "../rx-operators"; +import { clone$PerUserId, sharedStateByUserId } from "../util"; import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options"; @@ -19,34 +17,34 @@ export class CatchallGeneratorStrategy * @param usernameService generates a catchall address for a domain */ constructor( - private usernameService: UsernameGenerationServiceAbstraction, + private random: Randomizer, private stateProvider: StateProvider, + private defaultOptions: CatchallGenerationOptions = DefaultCatchallOptions, ) {} - /** {@link GeneratorStrategy.durableState} */ - durableState(id: UserId) { - return this.stateProvider.getUser(id, CATCHALL_SETTINGS); - } + // configuration + durableState = sharedStateByUserId(CATCHALL_SETTINGS, this.stateProvider); + defaults$ = clone$PerUserId(this.defaultOptions); + toEvaluator = newDefaultEvaluator(); + readonly policy = PolicyType.PasswordGenerator; - /** {@link GeneratorStrategy.defaults$} */ - defaults$(userId: UserId) { - return new BehaviorSubject({ ...DefaultCatchallOptions }).asObservable(); - } + // algorithm + async generate(options: CatchallGenerationOptions) { + const o = Object.assign({}, DefaultCatchallOptions, options); - /** {@link GeneratorStrategy.policy} */ - get policy() { - // Uses password generator since there aren't policies - // specific to usernames. - return PolicyType.PasswordGenerator; - } + if (o.catchallDomain == null || o.catchallDomain === "") { + return null; + } + if (o.catchallType == null) { + o.catchallType = "random"; + } - /** {@link GeneratorStrategy.toEvaluator} */ - toEvaluator() { - return pipe(map((_) => new DefaultPolicyEvaluator())); - } - - /** {@link GeneratorStrategy.generate} */ - generate(options: CatchallGenerationOptions) { - return this.usernameService.generateCatchall(options); + let startString = ""; + if (o.catchallType === "random") { + startString = await this.random.chars(8); + } else if (o.catchallType === "website-name") { + startString = o.website; + } + return startString + "@" + o.catchallDomain; } } 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 index 76e51f609c..128b69e673 100644 --- 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 @@ -7,12 +7,13 @@ import { PolicyType } from "../../../admin-console/enums"; import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { Randomizer } from "../abstractions/randomizer"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { EFF_USERNAME_SETTINGS } from "../key-definitions"; import { DefaultEffUsernameOptions } from "./eff-username-generator-options"; -import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; +import { EffUsernameGeneratorStrategy } from "."; const SomeUser = "some user" as UserId; const SomePolicy = mock({ @@ -40,8 +41,8 @@ describe("EFF long word list generation strategy", () => { describe("durableState", () => { it("should use password settings key", () => { const provider = mock(); - const legacy = mock(); - const strategy = new EffUsernameGeneratorStrategy(legacy, provider); + const randomizer = mock(); + const strategy = new EffUsernameGeneratorStrategy(randomizer, provider); strategy.durableState(SomeUser); @@ -61,26 +62,14 @@ describe("EFF long word list generation strategy", () => { describe("policy", () => { it("should use password generator policy", () => { - const legacy = mock(); - const strategy = new EffUsernameGeneratorStrategy(legacy, null); + const randomizer = mock(); + const strategy = new EffUsernameGeneratorStrategy(randomizer, null); 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, null); - const options = { - wordCapitalize: false, - wordIncludeNumber: false, - website: null as string, - }; - - await strategy.generate(options); - - expect(legacy.generateWord).toHaveBeenCalledWith(options); - }); + it.todo("generate username tests"); }); }); 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 index 70d1f85420..abd8e6b226 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts @@ -1,13 +1,13 @@ -import { BehaviorSubject, map, pipe } from "rxjs"; +import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; -import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; -import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { Randomizer } from "../abstractions/randomizer"; import { EFF_USERNAME_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; +import { newDefaultEvaluator } from "../rx-operators"; +import { clone$PerUserId, sharedStateByUserId } from "../util"; import { DefaultEffUsernameOptions, @@ -22,34 +22,23 @@ export class EffUsernameGeneratorStrategy * @param usernameService generates a username from EFF word list */ constructor( - private usernameService: UsernameGenerationServiceAbstraction, + private random: Randomizer, private stateProvider: StateProvider, + private defaultOptions: EffUsernameGenerationOptions = DefaultEffUsernameOptions, ) {} - /** {@link GeneratorStrategy.durableState} */ - durableState(id: UserId) { - return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS); - } + // configuration + durableState = sharedStateByUserId(EFF_USERNAME_SETTINGS, this.stateProvider); + defaults$ = clone$PerUserId(this.defaultOptions); + toEvaluator = newDefaultEvaluator(); + readonly policy = PolicyType.PasswordGenerator; - /** {@link GeneratorStrategy.defaults$} */ - defaults$(userId: UserId) { - return new BehaviorSubject({ ...DefaultEffUsernameOptions }).asObservable(); - } - - /** {@link GeneratorStrategy.policy} */ - get policy() { - // Uses password generator since there aren't policies - // specific to usernames. - return PolicyType.PasswordGenerator; - } - - /** {@link GeneratorStrategy.toEvaluator} */ - toEvaluator() { - return pipe(map((_) => new DefaultPolicyEvaluator())); - } - - /** {@link GeneratorStrategy.generate} */ - generate(options: EffUsernameGenerationOptions) { - return this.usernameService.generateWord(options); + // algorithm + async generate(options: EffUsernameGenerationOptions) { + const word = await this.random.pickWord(EFFLongWordList, { + titleCase: options.wordCapitalize ?? DefaultEffUsernameOptions.wordCapitalize, + number: options.wordIncludeNumber ?? DefaultEffUsernameOptions.wordIncludeNumber, + }); + return word; } } diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts index 7c1b4b9191..e78b432bfb 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts @@ -25,7 +25,7 @@ class TestForwarder extends ForwarderGeneratorStrategy { keyService: CryptoService, stateProvider: StateProvider, ) { - super(encryptService, keyService, stateProvider); + super(encryptService, keyService, stateProvider, { website: null, token: "" }); } get key() { diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts index 28ebcba4fd..4655a3fb72 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -1,4 +1,4 @@ -import { Observable, map, pipe } from "rxjs"; +import { map } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; @@ -13,8 +13,9 @@ import { SecretKeyDefinition } from "../../state/secret-key-definition"; import { SecretState } from "../../state/secret-state"; import { UserKeyEncryptor } from "../../state/user-key-encryptor"; import { GeneratorStrategy } from "../abstractions"; -import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { NoPolicy } from "../no-policy"; +import { newDefaultEvaluator } from "../rx-operators"; +import { clone$PerUserId, sharedByUserId } from "../util"; import { ApiOptions } from "./options/forwarder-options"; @@ -33,29 +34,25 @@ export abstract class ForwarderGeneratorStrategy< private readonly encryptService: EncryptService, private readonly keyService: CryptoService, private stateProvider: StateProvider, + private readonly defaultOptions: Options, ) { super(); - // Uses password generator since there aren't policies - // specific to usernames. - this.policy = PolicyType.PasswordGenerator; } - private durableStates = new Map>(); + /** configures forwarder secret storage */ + protected abstract readonly key: UserKeyDefinition; - /** {@link GeneratorStrategy.durableState} */ - durableState = (userId: UserId) => { - let state = this.durableStates.get(userId); + /** configures forwarder import buffer */ + protected abstract readonly rolloverKey: BufferedKeyDefinition; - if (!state) { - state = this.createState(userId); + // configuration + readonly policy = PolicyType.PasswordGenerator; + defaults$ = clone$PerUserId(this.defaultOptions); + toEvaluator = newDefaultEvaluator(); + durableState = sharedByUserId((userId) => this.getUserSecrets(userId)); - this.durableStates.set(userId, state); - } - - return state; - }; - - private createState(userId: UserId): SingleUserState { + // per-user encrypted state + private getUserSecrets(userId: UserId): SingleUserState { // construct the encryptor const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE); const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer); @@ -92,18 +89,4 @@ export abstract class ForwarderGeneratorStrategy< return rolloverState; } - - /** Gets the default options. */ - abstract defaults$: (userId: UserId) => Observable; - - /** Determine where forwarder configuration is stored */ - protected abstract readonly key: UserKeyDefinition; - - /** Determine where forwarder rollover configuration is stored */ - protected abstract readonly rolloverKey: BufferedKeyDefinition; - - /** {@link GeneratorStrategy.toEvaluator} */ - toEvaluator = () => { - return pipe(map((_) => new DefaultPolicyEvaluator())); - }; } diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.ts index 1212174951..ecf60da195 100644 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.ts +++ b/libs/common/src/tools/generator/username/forwarders/addy-io.ts @@ -1,11 +1,8 @@ -import { BehaviorSubject } from "rxjs"; - import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; -import { UserId } from "../../../../types/guid"; import { ADDY_IO_FORWARDER, ADDY_IO_BUFFER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; @@ -36,25 +33,14 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy< keyService: CryptoService, stateProvider: StateProvider, ) { - super(encryptService, keyService, stateProvider); + super(encryptService, keyService, stateProvider, DefaultAddyIoOptions); } - /** {@link ForwarderGeneratorStrategy.key} */ - get key() { - return ADDY_IO_FORWARDER; - } + // configuration + readonly key = ADDY_IO_FORWARDER; + readonly rolloverKey = ADDY_IO_BUFFER; - /** {@link ForwarderGeneratorStrategy.rolloverKey} */ - get rolloverKey() { - return ADDY_IO_BUFFER; - } - - /** {@link ForwarderGeneratorStrategy.defaults$} */ - defaults$ = (userId: UserId) => { - return new BehaviorSubject({ ...DefaultAddyIoOptions }); - }; - - /** {@link ForwarderGeneratorStrategy.generate} */ + // request generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts index 4a9040d74a..492105dfdf 100644 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts @@ -1,11 +1,8 @@ -import { BehaviorSubject } from "rxjs"; - import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; -import { UserId } from "../../../../types/guid"; import { DUCK_DUCK_GO_FORWARDER, DUCK_DUCK_GO_BUFFER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; @@ -32,25 +29,14 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy keyService: CryptoService, stateProvider: StateProvider, ) { - super(encryptService, keyService, stateProvider); + super(encryptService, keyService, stateProvider, DefaultDuckDuckGoOptions); } - /** {@link ForwarderGeneratorStrategy.key} */ - get key() { - return DUCK_DUCK_GO_FORWARDER; - } + // configuration + readonly key = DUCK_DUCK_GO_FORWARDER; + readonly rolloverKey = DUCK_DUCK_GO_BUFFER; - /** {@link ForwarderGeneratorStrategy.rolloverKey} */ - get rolloverKey() { - return DUCK_DUCK_GO_BUFFER; - } - - /** {@link ForwarderGeneratorStrategy.defaults$} */ - defaults$ = (userId: UserId) => { - return new BehaviorSubject({ ...DefaultDuckDuckGoOptions }); - }; - - /** {@link ForwarderGeneratorStrategy.generate} */ + // request generate = async (options: ApiOptions): Promise => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name); diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.ts index 0236e658fb..0c4e0e2cfd 100644 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.ts +++ b/libs/common/src/tools/generator/username/forwarders/fastmail.ts @@ -1,11 +1,8 @@ -import { BehaviorSubject } from "rxjs"; - import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; -import { UserId } from "../../../../types/guid"; import { FASTMAIL_FORWARDER, FASTMAIL_BUFFER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; @@ -34,25 +31,14 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy { - return new BehaviorSubject({ ...DefaultFastmailOptions }); - }; - - /** {@link ForwarderGeneratorStrategy.rolloverKey} */ - get rolloverKey() { - return FASTMAIL_BUFFER; - } - - /** {@link ForwarderGeneratorStrategy.generate} */ + // request generate = async (options: ApiOptions & EmailPrefixOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.Fastmail.name); @@ -76,7 +62,7 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy { - return new BehaviorSubject({ ...DefaultFirefoxRelayOptions }); - }; - - /** {@link ForwarderGeneratorStrategy.generate} */ + // request generate = async (options: ApiOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.FirefoxRelay.name); diff --git a/libs/common/src/tools/generator/username/forwarders/forward-email.ts b/libs/common/src/tools/generator/username/forwarders/forward-email.ts index af654d3917..20dfe01291 100644 --- a/libs/common/src/tools/generator/username/forwarders/forward-email.ts +++ b/libs/common/src/tools/generator/username/forwarders/forward-email.ts @@ -1,12 +1,9 @@ -import { BehaviorSubject } from "rxjs"; - import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { Utils } from "../../../../platform/misc/utils"; import { StateProvider } from "../../../../platform/state"; -import { UserId } from "../../../../types/guid"; import { FORWARD_EMAIL_FORWARDER, FORWARD_EMAIL_BUFFER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; @@ -36,25 +33,14 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< keyService: CryptoService, stateProvider: StateProvider, ) { - super(encryptService, keyService, stateProvider); + super(encryptService, keyService, stateProvider, DefaultForwardEmailOptions); } - /** {@link ForwarderGeneratorStrategy.key} */ - get key() { - return FORWARD_EMAIL_FORWARDER; - } + // configuration + readonly key = FORWARD_EMAIL_FORWARDER; + readonly rolloverKey = FORWARD_EMAIL_BUFFER; - /** {@link ForwarderGeneratorStrategy.defaults$} */ - defaults$ = (userId: UserId) => { - return new BehaviorSubject({ ...DefaultForwardEmailOptions }); - }; - - /** {@link ForwarderGeneratorStrategy.rolloverKey} */ - get rolloverKey() { - return FORWARD_EMAIL_BUFFER; - } - - /** {@link ForwarderGeneratorStrategy.generate} */ + // request generate = async (options: ApiOptions & EmailDomainOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.ForwardEmail.name); diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.ts index ee91a41145..593c734641 100644 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.ts +++ b/libs/common/src/tools/generator/username/forwarders/simple-login.ts @@ -1,11 +1,8 @@ -import { BehaviorSubject } from "rxjs"; - import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; -import { UserId } from "../../../../types/guid"; import { SIMPLE_LOGIN_FORWARDER, SIMPLE_LOGIN_BUFFER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; @@ -33,25 +30,14 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy { - return new BehaviorSubject({ ...DefaultSimpleLoginOptions }); - }; - - /** {@link ForwarderGeneratorStrategy.generate} */ + // request generate = async (options: SelfHostedApiOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.SimpleLogin.name); diff --git a/libs/common/src/tools/generator/username/index.ts b/libs/common/src/tools/generator/username/index.ts index 7c5ec45f74..a9d8e67608 100644 --- a/libs/common/src/tools/generator/username/index.ts +++ b/libs/common/src/tools/generator/username/index.ts @@ -3,4 +3,3 @@ export { CatchallGeneratorStrategy } from "./catchall-generator-strategy"; export { SubaddressGeneratorStrategy } from "./subaddress-generator-strategy"; export { UsernameGeneratorOptions } from "./username-generation-options"; export { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; -export { UsernameGenerationService } from "./username-generation.service"; diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts index b5ac9c4cf9..ba1d5aa2b8 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts @@ -7,15 +7,13 @@ import { PolicyType } from "../../../admin-console/enums"; import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { Randomizer } from "../abstractions/randomizer"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { SUBADDRESS_SETTINGS } from "../key-definitions"; -import { - DefaultSubaddressOptions, - SubaddressGenerationOptions, -} from "./subaddress-generator-options"; +import { DefaultSubaddressOptions } from "./subaddress-generator-options"; -import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; +import { SubaddressGeneratorStrategy } from "."; const SomeUser = "some user" as UserId; const SomePolicy = mock({ @@ -43,8 +41,8 @@ describe("Email subaddress list generation strategy", () => { describe("durableState", () => { it("should use password settings key", () => { const provider = mock(); - const legacy = mock(); - const strategy = new SubaddressGeneratorStrategy(legacy, provider); + const randomizer = mock(); + const strategy = new SubaddressGeneratorStrategy(randomizer, provider); strategy.durableState(SomeUser); @@ -64,26 +62,14 @@ describe("Email subaddress list generation strategy", () => { describe("policy", () => { it("should use password generator policy", () => { - const legacy = mock(); - const strategy = new SubaddressGeneratorStrategy(legacy, null); + const randomizer = mock(); + const strategy = new SubaddressGeneratorStrategy(randomizer, null); 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 SubaddressGeneratorStrategy(legacy, null); - const options = { - subaddressType: "website-name", - subaddressEmail: "someone@example.com", - website: "foo.com", - } as SubaddressGenerationOptions; - - await strategy.generate(options); - - expect(legacy.generateSubaddress).toHaveBeenCalledWith(options); - }); + it.todo("generate email subaddress tests"); }); }); diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts index 6106d6d476..e44735c213 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts @@ -1,13 +1,11 @@ -import { BehaviorSubject, map, pipe } from "rxjs"; - import { PolicyType } from "../../../admin-console/enums"; import { StateProvider } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; -import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; -import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { Randomizer } from "../abstractions/randomizer"; import { SUBADDRESS_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; +import { newDefaultEvaluator } from "../rx-operators"; +import { clone$PerUserId, sharedStateByUserId } from "../util"; import { DefaultSubaddressOptions, @@ -26,34 +24,42 @@ export class SubaddressGeneratorStrategy * @param usernameService generates an email subaddress from an email address */ constructor( - private usernameService: UsernameGenerationServiceAbstraction, + private random: Randomizer, private stateProvider: StateProvider, + private defaultOptions: SubaddressGenerationOptions = DefaultSubaddressOptions, ) {} - /** {@link GeneratorStrategy.durableState} */ - durableState(id: UserId) { - return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS); - } + // configuration + durableState = sharedStateByUserId(SUBADDRESS_SETTINGS, this.stateProvider); + defaults$ = clone$PerUserId(this.defaultOptions); + toEvaluator = newDefaultEvaluator(); + readonly policy = PolicyType.PasswordGenerator; - /** {@link GeneratorStrategy.defaults$} */ - defaults$(userId: UserId) { - return new BehaviorSubject({ ...DefaultSubaddressOptions }).asObservable(); - } + // algorithm + async generate(options: SubaddressGenerationOptions) { + const o = Object.assign({}, DefaultSubaddressOptions, options); - /** {@link GeneratorStrategy.policy} */ - get policy() { - // Uses password generator since there aren't policies - // specific to usernames. - return PolicyType.PasswordGenerator; - } + const subaddressEmail = o.subaddressEmail; + if (subaddressEmail == null || subaddressEmail.length < 3) { + return o.subaddressEmail; + } + const atIndex = subaddressEmail.indexOf("@"); + if (atIndex < 1 || atIndex >= subaddressEmail.length - 1) { + return subaddressEmail; + } + if (o.subaddressType == null) { + o.subaddressType = "random"; + } - /** {@link GeneratorStrategy.toEvaluator} */ - toEvaluator() { - return pipe(map((_) => new DefaultPolicyEvaluator())); - } + const emailBeginning = subaddressEmail.substr(0, atIndex); + const emailEnding = subaddressEmail.substr(atIndex + 1, subaddressEmail.length); - /** {@link GeneratorStrategy.generate} */ - generate(options: SubaddressGenerationOptions) { - return this.usernameService.generateSubaddress(options); + let subaddressString = ""; + if (o.subaddressType === "random") { + subaddressString = await this.random.chars(8); + } else if (o.subaddressType === "website-name") { + subaddressString = o.website; + } + return emailBeginning + "+" + subaddressString + "@" + emailEnding; } } diff --git a/libs/common/src/tools/generator/username/username-generation.service.ts b/libs/common/src/tools/generator/username/username-generation.service.ts deleted file mode 100644 index e659aacb51..0000000000 --- a/libs/common/src/tools/generator/username/username-generation.service.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { from } from "rxjs"; - -import { ApiService } from "../../../abstractions/api.service"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; -import { StateService } from "../../../platform/abstractions/state.service"; -import { EFFLongWordList } from "../../../platform/misc/wordlist"; -import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; - -import { - AnonAddyForwarder, - DuckDuckGoForwarder, - FastmailForwarder, - FirefoxRelayForwarder, - ForwardEmailForwarder, - Forwarder, - ForwarderOptions, - SimpleLoginForwarder, -} from "./email-forwarders"; -import { UsernameGeneratorOptions } from "./username-generation-options"; - -const DefaultOptions: UsernameGeneratorOptions = { - type: "word", - website: null, - wordCapitalize: true, - wordIncludeNumber: true, - subaddressType: "random", - catchallType: "random", - forwardedService: "", - forwardedAnonAddyDomain: "anonaddy.me", - forwardedAnonAddyBaseUrl: "https://app.addy.io", - forwardedForwardEmailDomain: "hideaddress.net", - forwardedSimpleLoginBaseUrl: "https://app.simplelogin.io", -}; - -export class UsernameGenerationService implements UsernameGenerationServiceAbstraction { - constructor( - private cryptoService: CryptoService, - private stateService: StateService, - private apiService: ApiService, - ) {} - - generateUsername(options: UsernameGeneratorOptions): Promise { - if (options.type === "catchall") { - return this.generateCatchall(options); - } else if (options.type === "subaddress") { - return this.generateSubaddress(options); - } else if (options.type === "forwarded") { - return this.generateForwarded(options); - } else { - return this.generateWord(options); - } - } - - async generateWord(options: UsernameGeneratorOptions): Promise { - const o = Object.assign({}, DefaultOptions, options); - - if (o.wordCapitalize == null) { - o.wordCapitalize = true; - } - if (o.wordIncludeNumber == null) { - o.wordIncludeNumber = true; - } - - const wordIndex = await this.cryptoService.randomNumber(0, EFFLongWordList.length - 1); - let word = EFFLongWordList[wordIndex]; - if (o.wordCapitalize) { - word = word.charAt(0).toUpperCase() + word.slice(1); - } - if (o.wordIncludeNumber) { - const num = await this.cryptoService.randomNumber(1, 9999); - word = word + this.zeroPad(num.toString(), 4); - } - return word; - } - - async generateSubaddress(options: UsernameGeneratorOptions): Promise { - const o = Object.assign({}, DefaultOptions, options); - - const subaddressEmail = o.subaddressEmail; - if (subaddressEmail == null || subaddressEmail.length < 3) { - return o.subaddressEmail; - } - const atIndex = subaddressEmail.indexOf("@"); - if (atIndex < 1 || atIndex >= subaddressEmail.length - 1) { - return subaddressEmail; - } - if (o.subaddressType == null) { - o.subaddressType = "random"; - } - - const emailBeginning = subaddressEmail.substr(0, atIndex); - const emailEnding = subaddressEmail.substr(atIndex + 1, subaddressEmail.length); - - let subaddressString = ""; - if (o.subaddressType === "random") { - subaddressString = await this.randomString(8); - } else if (o.subaddressType === "website-name") { - subaddressString = o.website; - } - return emailBeginning + "+" + subaddressString + "@" + emailEnding; - } - - async generateCatchall(options: UsernameGeneratorOptions): Promise { - const o = Object.assign({}, DefaultOptions, options); - - if (o.catchallDomain == null || o.catchallDomain === "") { - return null; - } - if (o.catchallType == null) { - o.catchallType = "random"; - } - - let startString = ""; - if (o.catchallType === "random") { - startString = await this.randomString(8); - } else if (o.catchallType === "website-name") { - startString = o.website; - } - return startString + "@" + o.catchallDomain; - } - - async generateForwarded(options: UsernameGeneratorOptions): Promise { - const o = Object.assign({}, DefaultOptions, options); - - if (o.forwardedService == null) { - return null; - } - - let forwarder: Forwarder = null; - const forwarderOptions = new ForwarderOptions(); - forwarderOptions.website = o.website; - if (o.forwardedService === "simplelogin") { - forwarder = new SimpleLoginForwarder(); - forwarderOptions.apiKey = o.forwardedSimpleLoginApiKey; - forwarderOptions.simplelogin.baseUrl = o.forwardedSimpleLoginBaseUrl; - } else if (o.forwardedService === "anonaddy") { - forwarder = new AnonAddyForwarder(); - forwarderOptions.apiKey = o.forwardedAnonAddyApiToken; - forwarderOptions.anonaddy.domain = o.forwardedAnonAddyDomain; - forwarderOptions.anonaddy.baseUrl = o.forwardedAnonAddyBaseUrl; - } else if (o.forwardedService === "firefoxrelay") { - forwarder = new FirefoxRelayForwarder(); - forwarderOptions.apiKey = o.forwardedFirefoxApiToken; - } else if (o.forwardedService === "fastmail") { - forwarder = new FastmailForwarder(); - forwarderOptions.apiKey = o.forwardedFastmailApiToken; - } else if (o.forwardedService === "duckduckgo") { - forwarder = new DuckDuckGoForwarder(); - forwarderOptions.apiKey = o.forwardedDuckDuckGoToken; - } else if (o.forwardedService === "forwardemail") { - forwarder = new ForwardEmailForwarder(); - forwarderOptions.apiKey = o.forwardedForwardEmailApiToken; - forwarderOptions.forwardemail.domain = o.forwardedForwardEmailDomain; - } - - if (forwarder == null) { - return null; - } - - return forwarder.generate(this.apiService, forwarderOptions); - } - - getOptions$() { - return from(this.getOptions()); - } - - async getOptions(): Promise { - let options = await this.stateService.getUsernameGenerationOptions(); - if (options == null) { - options = Object.assign({}, DefaultOptions); - } else { - options = Object.assign({}, DefaultOptions, options); - } - await this.stateService.setUsernameGenerationOptions(options); - return options; - } - - async saveOptions(options: UsernameGeneratorOptions) { - await this.stateService.setUsernameGenerationOptions(options); - } - - private async randomString(length: number) { - let str = ""; - const charSet = "abcdefghijklmnopqrstuvwxyz1234567890"; - for (let i = 0; i < length; i++) { - const randomCharIndex = await this.cryptoService.randomNumber(0, charSet.length - 1); - str += charSet.charAt(randomCharIndex); - } - return str; - } - - // ref: https://stackoverflow.com/a/10073788 - private zeroPad(number: string, width: number) { - return number.length >= width - ? number - : new Array(width - number.length + 1).join("0") + number; - } -} diff --git a/libs/common/src/tools/generator/util.ts b/libs/common/src/tools/generator/util.ts new file mode 100644 index 0000000000..ee526fc678 --- /dev/null +++ b/libs/common/src/tools/generator/util.ts @@ -0,0 +1,41 @@ +import { BehaviorSubject } from "rxjs"; + +import { SingleUserState, StateProvider, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; + +/** construct a method that outputs a copy of `defaultValue` as an observable. */ +export function clone$PerUserId(defaultValue: Value) { + const _subjects = new Map>(); + + return (key: UserId) => { + let value = _subjects.get(key); + + if (value === undefined) { + value = new BehaviorSubject({ ...defaultValue }); + _subjects.set(key, value); + } + + return value.asObservable(); + }; +} + +/** construct a method that caches user-specific states by userid. */ +export function sharedByUserId(create: (userId: UserId) => SingleUserState) { + const _subjects = new Map>(); + + return (key: UserId) => { + let value = _subjects.get(key); + + if (value === undefined) { + value = create(key); + _subjects.set(key, value); + } + + return value; + }; +} + +/** construct a method that loads a user-specific state from the provider. */ +export function sharedStateByUserId(key: UserKeyDefinition, provider: StateProvider) { + return (id: UserId) => provider.getUser(id, key); +} diff --git a/libs/common/src/tools/generator/word-options.ts b/libs/common/src/tools/generator/word-options.ts new file mode 100644 index 0000000000..1c98d0bac8 --- /dev/null +++ b/libs/common/src/tools/generator/word-options.ts @@ -0,0 +1,6 @@ +export type WordOptions = { + /** set the first letter uppercase */ + titleCase?: boolean; + /** append a number */ + number?: boolean; +};