mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-7290] replace legacy abstraction with generation algorithms (#9435)
* replace legacy abstraction with generation algorithms * delete mv2-based generator services
This commit is contained in:
39
libs/common/src/tools/generator/abstractions/randomizer.ts
Normal file
39
libs/common/src/tools/generator/abstractions/randomizer.ts
Normal file
@@ -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<Entry>(list: Array<Entry>): Promise<Entry>;
|
||||||
|
|
||||||
|
/** 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<string>, options?: WordOptions): Promise<string>;
|
||||||
|
|
||||||
|
/** 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<Entry>(items: Array<Entry>): Promise<Array<Entry>>;
|
||||||
|
|
||||||
|
/** 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<string>;
|
||||||
|
|
||||||
|
/** 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<number>;
|
||||||
|
}
|
||||||
@@ -40,11 +40,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
GeneratedPasswordHistory,
|
GeneratedPasswordHistory,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
PasswordGenerationService,
|
|
||||||
PasswordGeneratorOptions,
|
PasswordGeneratorOptions,
|
||||||
PasswordGeneratorPolicy,
|
PasswordGeneratorPolicy,
|
||||||
PasswordGeneratorStrategy,
|
PasswordGeneratorStrategy,
|
||||||
} from "./password";
|
} from "./password";
|
||||||
|
import { CryptoServiceRandomizer } from "./random";
|
||||||
|
|
||||||
type MappedOptions = {
|
type MappedOptions = {
|
||||||
generator: GeneratorNavigation;
|
generator: GeneratorNavigation;
|
||||||
@@ -60,17 +60,15 @@ export function legacyPasswordGenerationServiceFactory(
|
|||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
): PasswordGenerationServiceAbstraction {
|
): PasswordGenerationServiceAbstraction {
|
||||||
// FIXME: Once the password generation service is replaced with this service
|
const randomizer = new CryptoServiceRandomizer(cryptoService);
|
||||||
// in the clients, factor out the deprecated service in its entirety.
|
|
||||||
const deprecatedService = new PasswordGenerationService(cryptoService, null, null);
|
|
||||||
|
|
||||||
const passwords = new DefaultGeneratorService(
|
const passwords = new DefaultGeneratorService(
|
||||||
new PasswordGeneratorStrategy(deprecatedService, stateProvider),
|
new PasswordGeneratorStrategy(randomizer, stateProvider),
|
||||||
policyService,
|
policyService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const passphrases = new DefaultGeneratorService(
|
const passphrases = new DefaultGeneratorService(
|
||||||
new PassphraseGeneratorStrategy(deprecatedService, stateProvider),
|
new PassphraseGeneratorStrategy(randomizer, stateProvider),
|
||||||
policyService,
|
policyService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { DefaultGeneratorService } from "./default-generator.service";
|
|||||||
import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service";
|
import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service";
|
||||||
import { GeneratorNavigation } from "./navigation/generator-navigation";
|
import { GeneratorNavigation } from "./navigation/generator-navigation";
|
||||||
import { NoPolicy } from "./no-policy";
|
import { NoPolicy } from "./no-policy";
|
||||||
|
import { CryptoServiceRandomizer } from "./random";
|
||||||
import {
|
import {
|
||||||
CatchallGeneratorStrategy,
|
CatchallGeneratorStrategy,
|
||||||
SubaddressGeneratorStrategy,
|
SubaddressGeneratorStrategy,
|
||||||
@@ -37,7 +38,6 @@ import {
|
|||||||
} from "./username/options/forwarder-options";
|
} from "./username/options/forwarder-options";
|
||||||
import { SubaddressGenerationOptions } from "./username/subaddress-generator-options";
|
import { SubaddressGenerationOptions } from "./username/subaddress-generator-options";
|
||||||
import { UsernameGeneratorOptions } from "./username/username-generation-options";
|
import { UsernameGeneratorOptions } from "./username/username-generation-options";
|
||||||
import { UsernameGenerationService } from "./username/username-generation.service";
|
|
||||||
|
|
||||||
type MappedOptions = {
|
type MappedOptions = {
|
||||||
generator: GeneratorNavigation;
|
generator: GeneratorNavigation;
|
||||||
@@ -65,22 +65,20 @@ export function legacyUsernameGenerationServiceFactory(
|
|||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
): UsernameGenerationServiceAbstraction {
|
): UsernameGenerationServiceAbstraction {
|
||||||
// FIXME: Once the username generation service is replaced with this service
|
const randomizer = new CryptoServiceRandomizer(cryptoService);
|
||||||
// in the clients, factor out the deprecated service in its entirety.
|
|
||||||
const deprecatedService = new UsernameGenerationService(cryptoService, null, null);
|
|
||||||
|
|
||||||
const effUsername = new DefaultGeneratorService(
|
const effUsername = new DefaultGeneratorService(
|
||||||
new EffUsernameGeneratorStrategy(deprecatedService, stateProvider),
|
new EffUsernameGeneratorStrategy(randomizer, stateProvider),
|
||||||
policyService,
|
policyService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const subaddress = new DefaultGeneratorService(
|
const subaddress = new DefaultGeneratorService(
|
||||||
new SubaddressGeneratorStrategy(deprecatedService, stateProvider),
|
new SubaddressGeneratorStrategy(randomizer, stateProvider),
|
||||||
policyService,
|
policyService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const catchall = new DefaultGeneratorService(
|
const catchall = new DefaultGeneratorService(
|
||||||
new CatchallGeneratorStrategy(deprecatedService, stateProvider),
|
new CatchallGeneratorStrategy(randomizer, stateProvider),
|
||||||
policyService,
|
policyService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
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 { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
||||||
@@ -65,8 +65,8 @@ describe("Password generation strategy", () => {
|
|||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
const provider = mock<StateProvider>();
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy, provider);
|
const strategy = new PassphraseGeneratorStrategy(randomizer, provider);
|
||||||
|
|
||||||
strategy.durableState(SomeUser);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
@@ -86,36 +86,14 @@ describe("Password generation strategy", () => {
|
|||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it.todo("should generate a password using the given options");
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
|
||||||
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<PasswordGenerationServiceAbstraction>();
|
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
|
||||||
|
|
||||||
await strategy.generate({ type: "foo" } as any);
|
|
||||||
|
|
||||||
expect(legacy.generatePassphrase).toHaveBeenCalledWith({ type: "passphrase" });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
import { BehaviorSubject, map, pipe } from "rxjs";
|
|
||||||
|
|
||||||
import { GeneratorStrategy } from "..";
|
import { GeneratorStrategy } from "..";
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
|
import { EFFLongWordList } from "../../../platform/misc/wordlist";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
|
||||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
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 {
|
import {
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
DefaultPassphraseGenerationOptions,
|
DefaultPassphraseGenerationOptions,
|
||||||
} from "./passphrase-generation-options";
|
} from "./passphrase-generation-options";
|
||||||
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
import { PassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
||||||
import {
|
|
||||||
DisabledPassphraseGeneratorPolicy,
|
|
||||||
PassphraseGeneratorPolicy,
|
|
||||||
leastPrivilege,
|
|
||||||
} from "./passphrase-generator-policy";
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy} */
|
/** Generates passphrases composed of random words */
|
||||||
export class PassphraseGeneratorStrategy
|
export class PassphraseGeneratorStrategy
|
||||||
implements GeneratorStrategy<PassphraseGenerationOptions, PassphraseGeneratorPolicy>
|
implements GeneratorStrategy<PassphraseGenerationOptions, PassphraseGeneratorPolicy>
|
||||||
{
|
{
|
||||||
@@ -28,36 +23,48 @@ export class PassphraseGeneratorStrategy
|
|||||||
* @param stateProvider provides durable state
|
* @param stateProvider provides durable state
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private legacy: PasswordGenerationServiceAbstraction,
|
private randomizer: Randomizer,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.durableState} */
|
// configuration
|
||||||
durableState(id: UserId) {
|
durableState = sharedStateByUserId(PASSPHRASE_SETTINGS, this.stateProvider);
|
||||||
return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS);
|
defaults$ = clone$PerUserId(DefaultPassphraseGenerationOptions);
|
||||||
}
|
readonly policy = PolicyType.PasswordGenerator;
|
||||||
|
|
||||||
/** Gets the default options. */
|
|
||||||
defaults$(_: UserId) {
|
|
||||||
return new BehaviorSubject({ ...DefaultPassphraseGenerationOptions }).asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
|
||||||
get policy() {
|
|
||||||
return PolicyType.PasswordGenerator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
|
||||||
toEvaluator() {
|
toEvaluator() {
|
||||||
return pipe(
|
return mapPolicyToEvaluator(Policies.Passphrase);
|
||||||
reduceCollection(leastPrivilege, DisabledPassphraseGeneratorPolicy),
|
|
||||||
distinctIfShallowMatch(),
|
|
||||||
map((policy) => new PassphraseGeneratorOptionsEvaluator(policy)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.generate} */
|
// algorithm
|
||||||
generate(options: PassphraseGenerationOptions): Promise<string> {
|
async generate(options: PassphraseGenerationOptions): Promise<string> {
|
||||||
return this.legacy.generatePassphrase({ ...options, type: "passphrase" });
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ export { PasswordGeneratorStrategy } from "./password-generator-strategy";
|
|||||||
// legacy interfaces
|
// legacy interfaces
|
||||||
export { PasswordGeneratorOptions } from "./password-generator-options";
|
export { PasswordGeneratorOptions } from "./password-generator-options";
|
||||||
export { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
export { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
||||||
export { PasswordGenerationService } from "./password-generation.service";
|
|
||||||
export { GeneratedPasswordHistory } from "./generated-password-history";
|
export { GeneratedPasswordHistory } from "./generated-password-history";
|
||||||
|
|||||||
@@ -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<string> {
|
|
||||||
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<string> {
|
|
||||||
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<PasswordGeneratorPolicyOptions> {
|
|
||||||
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<GeneratedPasswordHistory[]> {
|
|
||||||
const hasKey = await this.cryptoService.hasUserKey();
|
|
||||||
if (!hasKey) {
|
|
||||||
return new Array<GeneratedPasswordHistory>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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<GeneratedPasswordHistory>();
|
|
||||||
}
|
|
||||||
|
|
||||||
async addHistory(password: string): Promise<void> {
|
|
||||||
// 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<GeneratedPasswordHistory[]> {
|
|
||||||
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<GeneratedPasswordHistory[]> {
|
|
||||||
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<GeneratedPasswordHistory[]> {
|
|
||||||
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]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,13 +12,13 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
|
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DefaultPasswordGenerationOptions,
|
DefaultPasswordGenerationOptions,
|
||||||
PasswordGenerationServiceAbstraction,
|
|
||||||
PasswordGeneratorOptionsEvaluator,
|
PasswordGeneratorOptionsEvaluator,
|
||||||
PasswordGeneratorStrategy,
|
PasswordGeneratorStrategy,
|
||||||
} from ".";
|
} from ".";
|
||||||
@@ -74,8 +74,8 @@ describe("Password generation strategy", () => {
|
|||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
const provider = mock<StateProvider>();
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy, provider);
|
const strategy = new PasswordGeneratorStrategy(randomizer, provider);
|
||||||
|
|
||||||
strategy.durableState(SomeUser);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
@@ -95,40 +95,14 @@ describe("Password generation strategy", () => {
|
|||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it.todo("should generate a password using the given options");
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
|
||||||
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<PasswordGenerationServiceAbstraction>();
|
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
|
||||||
|
|
||||||
await strategy.generate({ type: "foo" } as any);
|
|
||||||
|
|
||||||
expect(legacy.generatePassword).toHaveBeenCalledWith({ type: "password" });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
import { BehaviorSubject, map, pipe } from "rxjs";
|
|
||||||
|
|
||||||
import { GeneratorStrategy } from "..";
|
import { GeneratorStrategy } from "..";
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
|
||||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
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 {
|
import {
|
||||||
DefaultPasswordGenerationOptions,
|
DefaultPasswordGenerationOptions,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
} from "./password-generation-options";
|
} from "./password-generation-options";
|
||||||
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
import { PasswordGeneratorPolicy } from "./password-generator-policy";
|
||||||
import {
|
|
||||||
DisabledPasswordGeneratorPolicy,
|
|
||||||
PasswordGeneratorPolicy,
|
|
||||||
leastPrivilege,
|
|
||||||
} from "./password-generator-policy";
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy} */
|
/** Generates passwords composed of random characters */
|
||||||
export class PasswordGeneratorStrategy
|
export class PasswordGeneratorStrategy
|
||||||
implements GeneratorStrategy<PasswordGenerationOptions, PasswordGeneratorPolicy>
|
implements GeneratorStrategy<PasswordGenerationOptions, PasswordGeneratorPolicy>
|
||||||
{
|
{
|
||||||
@@ -27,36 +21,108 @@ export class PasswordGeneratorStrategy
|
|||||||
* @param legacy generates the password
|
* @param legacy generates the password
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private legacy: PasswordGenerationServiceAbstraction,
|
private randomizer: Randomizer,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.durableState} */
|
// configuration
|
||||||
durableState(id: UserId) {
|
durableState = sharedStateByUserId(PASSWORD_SETTINGS, this.stateProvider);
|
||||||
return this.stateProvider.getUser(id, PASSWORD_SETTINGS);
|
defaults$ = clone$PerUserId(DefaultPasswordGenerationOptions);
|
||||||
}
|
readonly policy = PolicyType.PasswordGenerator;
|
||||||
|
|
||||||
/** Gets the default options. */
|
|
||||||
defaults$(_: UserId) {
|
|
||||||
return new BehaviorSubject({ ...DefaultPasswordGenerationOptions }).asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
|
||||||
get policy() {
|
|
||||||
return PolicyType.PasswordGenerator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
|
||||||
toEvaluator() {
|
toEvaluator() {
|
||||||
return pipe(
|
return mapPolicyToEvaluator(Policies.Password);
|
||||||
reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy),
|
|
||||||
distinctIfShallowMatch(),
|
|
||||||
map((policy) => new PasswordGeneratorOptionsEvaluator(policy)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.generate} */
|
// algorithm
|
||||||
generate(options: PasswordGenerationOptions): Promise<string> {
|
async generate(options: PasswordGenerationOptions): Promise<string> {
|
||||||
return this.legacy.generatePassword({ ...options, type: "password" });
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
libs/common/src/tools/generator/policies.ts
Normal file
48
libs/common/src/tools/generator/policies.ts
Normal file
@@ -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<Policy, Evaluator> = {
|
||||||
|
/** 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<PassphraseGeneratorPolicy, PassphraseGeneratorOptionsEvaluator>);
|
||||||
|
|
||||||
|
const PASSWORD = Object.freeze({
|
||||||
|
disabledValue: DisabledPasswordGeneratorPolicy,
|
||||||
|
combine: passwordLeastPrivilege,
|
||||||
|
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
||||||
|
} as PolicyConfiguration<PasswordGeneratorPolicy, PasswordGeneratorOptionsEvaluator>);
|
||||||
|
|
||||||
|
/** Policy configurations */
|
||||||
|
export const Policies = Object.freeze({
|
||||||
|
/** Passphrase policy configuration */
|
||||||
|
Passphrase: PASSPHRASE,
|
||||||
|
|
||||||
|
/** Passphrase policy configuration */
|
||||||
|
Password: PASSWORD,
|
||||||
|
});
|
||||||
62
libs/common/src/tools/generator/random.ts
Normal file
62
libs/common/src/tools/generator/random.ts
Normal file
@@ -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<Entry>(list: Array<Entry>) {
|
||||||
|
const index = await this.uniform(0, list.length - 1);
|
||||||
|
return list[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
async pickWord(list: Array<string>, 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<T>(items: Array<T>, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
* An observable operator that reduces an emitted collection to a single object,
|
||||||
@@ -36,3 +39,23 @@ export function distinctIfShallowMatch<Item>(): OperatorFunction<Item, Item> {
|
|||||||
return isDistinct;
|
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<Policy, Evaluator>(
|
||||||
|
configuration: PolicyConfiguration<Policy, Evaluator>,
|
||||||
|
) {
|
||||||
|
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<Target>() {
|
||||||
|
return () => {
|
||||||
|
return pipe(map((_) => new DefaultPolicyEvaluator<Target>()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
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 SomeUser = "some user" as UserId;
|
||||||
const SomePolicy = mock<Policy>({
|
const SomePolicy = mock<Policy>({
|
||||||
@@ -40,8 +41,8 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
const provider = mock<StateProvider>();
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy, provider);
|
const strategy = new CatchallGeneratorStrategy(randomizer, provider);
|
||||||
|
|
||||||
strategy.durableState(SomeUser);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
@@ -61,26 +62,14 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
const strategy = new CatchallGeneratorStrategy(randomizer, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it.todo("generate catchall email addresses");
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { BehaviorSubject, map, pipe } from "rxjs";
|
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
|
||||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||||
import { NoPolicy } from "../no-policy";
|
import { NoPolicy } from "../no-policy";
|
||||||
|
import { newDefaultEvaluator } from "../rx-operators";
|
||||||
|
import { clone$PerUserId, sharedStateByUserId } from "../util";
|
||||||
|
|
||||||
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
|
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
|
||||||
|
|
||||||
@@ -19,34 +17,34 @@ export class CatchallGeneratorStrategy
|
|||||||
* @param usernameService generates a catchall address for a domain
|
* @param usernameService generates a catchall address for a domain
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private usernameService: UsernameGenerationServiceAbstraction,
|
private random: Randomizer,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
|
private defaultOptions: CatchallGenerationOptions = DefaultCatchallOptions,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.durableState} */
|
// configuration
|
||||||
durableState(id: UserId) {
|
durableState = sharedStateByUserId(CATCHALL_SETTINGS, this.stateProvider);
|
||||||
return this.stateProvider.getUser(id, CATCHALL_SETTINGS);
|
defaults$ = clone$PerUserId(this.defaultOptions);
|
||||||
|
toEvaluator = newDefaultEvaluator<CatchallGenerationOptions>();
|
||||||
|
readonly policy = PolicyType.PasswordGenerator;
|
||||||
|
|
||||||
|
// algorithm
|
||||||
|
async generate(options: CatchallGenerationOptions) {
|
||||||
|
const o = Object.assign({}, DefaultCatchallOptions, options);
|
||||||
|
|
||||||
|
if (o.catchallDomain == null || o.catchallDomain === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (o.catchallType == null) {
|
||||||
|
o.catchallType = "random";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.defaults$} */
|
let startString = "";
|
||||||
defaults$(userId: UserId) {
|
if (o.catchallType === "random") {
|
||||||
return new BehaviorSubject({ ...DefaultCatchallOptions }).asObservable();
|
startString = await this.random.chars(8);
|
||||||
|
} else if (o.catchallType === "website-name") {
|
||||||
|
startString = o.website;
|
||||||
}
|
}
|
||||||
|
return startString + "@" + o.catchallDomain;
|
||||||
/** {@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<CatchallGenerationOptions>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.generate} */
|
|
||||||
generate(options: CatchallGenerationOptions) {
|
|
||||||
return this.usernameService.generateCatchall(options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { DefaultEffUsernameOptions } from "./eff-username-generator-options";
|
import { DefaultEffUsernameOptions } from "./eff-username-generator-options";
|
||||||
|
|
||||||
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { EffUsernameGeneratorStrategy } from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
const SomePolicy = mock<Policy>({
|
const SomePolicy = mock<Policy>({
|
||||||
@@ -40,8 +41,8 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
const provider = mock<StateProvider>();
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy, provider);
|
const strategy = new EffUsernameGeneratorStrategy(randomizer, provider);
|
||||||
|
|
||||||
strategy.durableState(SomeUser);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
@@ -61,26 +62,14 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
const strategy = new EffUsernameGeneratorStrategy(randomizer, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it.todo("generate username tests");
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
|
||||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||||
import { NoPolicy } from "../no-policy";
|
import { NoPolicy } from "../no-policy";
|
||||||
|
import { newDefaultEvaluator } from "../rx-operators";
|
||||||
|
import { clone$PerUserId, sharedStateByUserId } from "../util";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DefaultEffUsernameOptions,
|
DefaultEffUsernameOptions,
|
||||||
@@ -22,34 +22,23 @@ export class EffUsernameGeneratorStrategy
|
|||||||
* @param usernameService generates a username from EFF word list
|
* @param usernameService generates a username from EFF word list
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private usernameService: UsernameGenerationServiceAbstraction,
|
private random: Randomizer,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
|
private defaultOptions: EffUsernameGenerationOptions = DefaultEffUsernameOptions,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.durableState} */
|
// configuration
|
||||||
durableState(id: UserId) {
|
durableState = sharedStateByUserId(EFF_USERNAME_SETTINGS, this.stateProvider);
|
||||||
return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS);
|
defaults$ = clone$PerUserId(this.defaultOptions);
|
||||||
}
|
toEvaluator = newDefaultEvaluator<EffUsernameGenerationOptions>();
|
||||||
|
readonly policy = PolicyType.PasswordGenerator;
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.defaults$} */
|
// algorithm
|
||||||
defaults$(userId: UserId) {
|
async generate(options: EffUsernameGenerationOptions) {
|
||||||
return new BehaviorSubject({ ...DefaultEffUsernameOptions }).asObservable();
|
const word = await this.random.pickWord(EFFLongWordList, {
|
||||||
}
|
titleCase: options.wordCapitalize ?? DefaultEffUsernameOptions.wordCapitalize,
|
||||||
|
number: options.wordIncludeNumber ?? DefaultEffUsernameOptions.wordIncludeNumber,
|
||||||
/** {@link GeneratorStrategy.policy} */
|
});
|
||||||
get policy() {
|
return word;
|
||||||
// Uses password generator since there aren't policies
|
|
||||||
// specific to usernames.
|
|
||||||
return PolicyType.PasswordGenerator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
|
||||||
toEvaluator() {
|
|
||||||
return pipe(map((_) => new DefaultPolicyEvaluator<EffUsernameGenerationOptions>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.generate} */
|
|
||||||
generate(options: EffUsernameGenerationOptions) {
|
|
||||||
return this.usernameService.generateWord(options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
|||||||
keyService: CryptoService,
|
keyService: CryptoService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
super(encryptService, keyService, stateProvider);
|
super(encryptService, keyService, stateProvider, { website: null, token: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
get key() {
|
get key() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Observable, map, pipe } from "rxjs";
|
import { map } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
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 { SecretState } from "../../state/secret-state";
|
||||||
import { UserKeyEncryptor } from "../../state/user-key-encryptor";
|
import { UserKeyEncryptor } from "../../state/user-key-encryptor";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
|
||||||
import { NoPolicy } from "../no-policy";
|
import { NoPolicy } from "../no-policy";
|
||||||
|
import { newDefaultEvaluator } from "../rx-operators";
|
||||||
|
import { clone$PerUserId, sharedByUserId } from "../util";
|
||||||
|
|
||||||
import { ApiOptions } from "./options/forwarder-options";
|
import { ApiOptions } from "./options/forwarder-options";
|
||||||
|
|
||||||
@@ -33,29 +34,25 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
private readonly encryptService: EncryptService,
|
private readonly encryptService: EncryptService,
|
||||||
private readonly keyService: CryptoService,
|
private readonly keyService: CryptoService,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
|
private readonly defaultOptions: Options,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
// Uses password generator since there aren't policies
|
|
||||||
// specific to usernames.
|
|
||||||
this.policy = PolicyType.PasswordGenerator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private durableStates = new Map<UserId, SingleUserState<Options>>();
|
/** configures forwarder secret storage */
|
||||||
|
protected abstract readonly key: UserKeyDefinition<Options>;
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.durableState} */
|
/** configures forwarder import buffer */
|
||||||
durableState = (userId: UserId) => {
|
protected abstract readonly rolloverKey: BufferedKeyDefinition<Options, Options>;
|
||||||
let state = this.durableStates.get(userId);
|
|
||||||
|
|
||||||
if (!state) {
|
// configuration
|
||||||
state = this.createState(userId);
|
readonly policy = PolicyType.PasswordGenerator;
|
||||||
|
defaults$ = clone$PerUserId(this.defaultOptions);
|
||||||
|
toEvaluator = newDefaultEvaluator<Options>();
|
||||||
|
durableState = sharedByUserId((userId) => this.getUserSecrets(userId));
|
||||||
|
|
||||||
this.durableStates.set(userId, state);
|
// per-user encrypted state
|
||||||
}
|
private getUserSecrets(userId: UserId): SingleUserState<Options> {
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
private createState(userId: UserId): SingleUserState<Options> {
|
|
||||||
// construct the encryptor
|
// construct the encryptor
|
||||||
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
|
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
|
||||||
const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
||||||
@@ -92,18 +89,4 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
|
|
||||||
return rolloverState;
|
return rolloverState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets the default options. */
|
|
||||||
abstract defaults$: (userId: UserId) => Observable<Options>;
|
|
||||||
|
|
||||||
/** Determine where forwarder configuration is stored */
|
|
||||||
protected abstract readonly key: UserKeyDefinition<Options>;
|
|
||||||
|
|
||||||
/** Determine where forwarder rollover configuration is stored */
|
|
||||||
protected abstract readonly rolloverKey: BufferedKeyDefinition<Options, Options>;
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
|
||||||
toEvaluator = () => {
|
|
||||||
return pipe(map((_) => new DefaultPolicyEvaluator<Options>()));
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
|
||||||
import { ADDY_IO_FORWARDER, ADDY_IO_BUFFER } from "../../key-definitions";
|
import { ADDY_IO_FORWARDER, ADDY_IO_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
@@ -36,25 +33,14 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy<
|
|||||||
keyService: CryptoService,
|
keyService: CryptoService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
super(encryptService, keyService, stateProvider);
|
super(encryptService, keyService, stateProvider, DefaultAddyIoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.key} */
|
// configuration
|
||||||
get key() {
|
readonly key = ADDY_IO_FORWARDER;
|
||||||
return ADDY_IO_FORWARDER;
|
readonly rolloverKey = ADDY_IO_BUFFER;
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
// request
|
||||||
get rolloverKey() {
|
|
||||||
return ADDY_IO_BUFFER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
|
||||||
defaults$ = (userId: UserId) => {
|
|
||||||
return new BehaviorSubject({ ...DefaultAddyIoOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
|
||||||
generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => {
|
generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name);
|
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name);
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
|
||||||
import { DUCK_DUCK_GO_FORWARDER, DUCK_DUCK_GO_BUFFER } from "../../key-definitions";
|
import { DUCK_DUCK_GO_FORWARDER, DUCK_DUCK_GO_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
@@ -32,25 +29,14 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions>
|
|||||||
keyService: CryptoService,
|
keyService: CryptoService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
super(encryptService, keyService, stateProvider);
|
super(encryptService, keyService, stateProvider, DefaultDuckDuckGoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.key} */
|
// configuration
|
||||||
get key() {
|
readonly key = DUCK_DUCK_GO_FORWARDER;
|
||||||
return DUCK_DUCK_GO_FORWARDER;
|
readonly rolloverKey = DUCK_DUCK_GO_BUFFER;
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
// request
|
||||||
get rolloverKey() {
|
|
||||||
return DUCK_DUCK_GO_BUFFER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
|
||||||
defaults$ = (userId: UserId) => {
|
|
||||||
return new BehaviorSubject({ ...DefaultDuckDuckGoOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
|
||||||
generate = async (options: ApiOptions): Promise<string> => {
|
generate = async (options: ApiOptions): Promise<string> => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name);
|
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name);
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
|
||||||
import { FASTMAIL_FORWARDER, FASTMAIL_BUFFER } from "../../key-definitions";
|
import { FASTMAIL_FORWARDER, FASTMAIL_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
@@ -34,25 +31,14 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
|
|||||||
keyService: CryptoService,
|
keyService: CryptoService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
super(encryptService, keyService, stateProvider);
|
super(encryptService, keyService, stateProvider, DefaultFastmailOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.key} */
|
// configuration
|
||||||
get key() {
|
readonly key = FASTMAIL_FORWARDER;
|
||||||
return FASTMAIL_FORWARDER;
|
readonly rolloverKey = FASTMAIL_BUFFER;
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
// request
|
||||||
defaults$ = (userId: UserId) => {
|
|
||||||
return new BehaviorSubject({ ...DefaultFastmailOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
|
||||||
get rolloverKey() {
|
|
||||||
return FASTMAIL_BUFFER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
|
||||||
generate = async (options: ApiOptions & EmailPrefixOptions) => {
|
generate = async (options: ApiOptions & EmailPrefixOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.Fastmail.name);
|
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.Fastmail.name);
|
||||||
@@ -76,7 +62,7 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
|
|||||||
"new-masked-email": {
|
"new-masked-email": {
|
||||||
state: "enabled",
|
state: "enabled",
|
||||||
description: "",
|
description: "",
|
||||||
forDomain: options.website,
|
forDomain: options.website ?? "",
|
||||||
emailPrefix: options.prefix,
|
emailPrefix: options.prefix,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
|
||||||
import { FIREFOX_RELAY_FORWARDER, FIREFOX_RELAY_BUFFER } from "../../key-definitions";
|
import { FIREFOX_RELAY_FORWARDER, FIREFOX_RELAY_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
@@ -32,25 +29,14 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions
|
|||||||
keyService: CryptoService,
|
keyService: CryptoService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
super(encryptService, keyService, stateProvider);
|
super(encryptService, keyService, stateProvider, DefaultFirefoxRelayOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.key} */
|
// configuration
|
||||||
get key() {
|
readonly key = FIREFOX_RELAY_FORWARDER;
|
||||||
return FIREFOX_RELAY_FORWARDER;
|
readonly rolloverKey = FIREFOX_RELAY_BUFFER;
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
// request
|
||||||
get rolloverKey() {
|
|
||||||
return FIREFOX_RELAY_BUFFER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
|
||||||
defaults$ = (userId: UserId) => {
|
|
||||||
return new BehaviorSubject({ ...DefaultFirefoxRelayOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
|
||||||
generate = async (options: ApiOptions) => {
|
generate = async (options: ApiOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.FirefoxRelay.name);
|
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.FirefoxRelay.name);
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { Utils } from "../../../../platform/misc/utils";
|
import { Utils } from "../../../../platform/misc/utils";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
|
||||||
import { FORWARD_EMAIL_FORWARDER, FORWARD_EMAIL_BUFFER } from "../../key-definitions";
|
import { FORWARD_EMAIL_FORWARDER, FORWARD_EMAIL_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
@@ -36,25 +33,14 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy<
|
|||||||
keyService: CryptoService,
|
keyService: CryptoService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
super(encryptService, keyService, stateProvider);
|
super(encryptService, keyService, stateProvider, DefaultForwardEmailOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.key} */
|
// configuration
|
||||||
get key() {
|
readonly key = FORWARD_EMAIL_FORWARDER;
|
||||||
return FORWARD_EMAIL_FORWARDER;
|
readonly rolloverKey = FORWARD_EMAIL_BUFFER;
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
// request
|
||||||
defaults$ = (userId: UserId) => {
|
|
||||||
return new BehaviorSubject({ ...DefaultForwardEmailOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
|
||||||
get rolloverKey() {
|
|
||||||
return FORWARD_EMAIL_BUFFER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
|
||||||
generate = async (options: ApiOptions & EmailDomainOptions) => {
|
generate = async (options: ApiOptions & EmailDomainOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.ForwardEmail.name);
|
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.ForwardEmail.name);
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "../../../../abstractions/api.service";
|
import { ApiService } from "../../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
|
||||||
import { SIMPLE_LOGIN_FORWARDER, SIMPLE_LOGIN_BUFFER } from "../../key-definitions";
|
import { SIMPLE_LOGIN_FORWARDER, SIMPLE_LOGIN_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
@@ -33,25 +30,14 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedA
|
|||||||
keyService: CryptoService,
|
keyService: CryptoService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
super(encryptService, keyService, stateProvider);
|
super(encryptService, keyService, stateProvider, DefaultSimpleLoginOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.key} */
|
// configuration
|
||||||
get key() {
|
readonly key = SIMPLE_LOGIN_FORWARDER;
|
||||||
return SIMPLE_LOGIN_FORWARDER;
|
readonly rolloverKey = SIMPLE_LOGIN_BUFFER;
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
// request
|
||||||
get rolloverKey() {
|
|
||||||
return SIMPLE_LOGIN_BUFFER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
|
||||||
defaults$ = (userId: UserId) => {
|
|
||||||
return new BehaviorSubject({ ...DefaultSimpleLoginOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
|
||||||
generate = async (options: SelfHostedApiOptions) => {
|
generate = async (options: SelfHostedApiOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.SimpleLogin.name);
|
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.SimpleLogin.name);
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ export { CatchallGeneratorStrategy } from "./catchall-generator-strategy";
|
|||||||
export { SubaddressGeneratorStrategy } from "./subaddress-generator-strategy";
|
export { SubaddressGeneratorStrategy } from "./subaddress-generator-strategy";
|
||||||
export { UsernameGeneratorOptions } from "./username-generation-options";
|
export { UsernameGeneratorOptions } from "./username-generation-options";
|
||||||
export { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
export { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||||
export { UsernameGenerationService } from "./username-generation.service";
|
|
||||||
|
|||||||
@@ -7,15 +7,13 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import {
|
import { DefaultSubaddressOptions } from "./subaddress-generator-options";
|
||||||
DefaultSubaddressOptions,
|
|
||||||
SubaddressGenerationOptions,
|
|
||||||
} from "./subaddress-generator-options";
|
|
||||||
|
|
||||||
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { SubaddressGeneratorStrategy } from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
const SomePolicy = mock<Policy>({
|
const SomePolicy = mock<Policy>({
|
||||||
@@ -43,8 +41,8 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
const provider = mock<StateProvider>();
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy, provider);
|
const strategy = new SubaddressGeneratorStrategy(randomizer, provider);
|
||||||
|
|
||||||
strategy.durableState(SomeUser);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
@@ -64,26 +62,14 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const randomizer = mock<Randomizer>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
const strategy = new SubaddressGeneratorStrategy(randomizer, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it.todo("generate email subaddress tests");
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { BehaviorSubject, map, pipe } from "rxjs";
|
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
import { Randomizer } from "../abstractions/randomizer";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
|
||||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||||
import { NoPolicy } from "../no-policy";
|
import { NoPolicy } from "../no-policy";
|
||||||
|
import { newDefaultEvaluator } from "../rx-operators";
|
||||||
|
import { clone$PerUserId, sharedStateByUserId } from "../util";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DefaultSubaddressOptions,
|
DefaultSubaddressOptions,
|
||||||
@@ -26,34 +24,42 @@ export class SubaddressGeneratorStrategy
|
|||||||
* @param usernameService generates an email subaddress from an email address
|
* @param usernameService generates an email subaddress from an email address
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private usernameService: UsernameGenerationServiceAbstraction,
|
private random: Randomizer,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
|
private defaultOptions: SubaddressGenerationOptions = DefaultSubaddressOptions,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.durableState} */
|
// configuration
|
||||||
durableState(id: UserId) {
|
durableState = sharedStateByUserId(SUBADDRESS_SETTINGS, this.stateProvider);
|
||||||
return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS);
|
defaults$ = clone$PerUserId(this.defaultOptions);
|
||||||
|
toEvaluator = newDefaultEvaluator<SubaddressGenerationOptions>();
|
||||||
|
readonly policy = PolicyType.PasswordGenerator;
|
||||||
|
|
||||||
|
// algorithm
|
||||||
|
async generate(options: SubaddressGenerationOptions) {
|
||||||
|
const o = Object.assign({}, DefaultSubaddressOptions, 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.defaults$} */
|
const emailBeginning = subaddressEmail.substr(0, atIndex);
|
||||||
defaults$(userId: UserId) {
|
const emailEnding = subaddressEmail.substr(atIndex + 1, subaddressEmail.length);
|
||||||
return new BehaviorSubject({ ...DefaultSubaddressOptions }).asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
let subaddressString = "";
|
||||||
get policy() {
|
if (o.subaddressType === "random") {
|
||||||
// Uses password generator since there aren't policies
|
subaddressString = await this.random.chars(8);
|
||||||
// specific to usernames.
|
} else if (o.subaddressType === "website-name") {
|
||||||
return PolicyType.PasswordGenerator;
|
subaddressString = o.website;
|
||||||
}
|
}
|
||||||
|
return emailBeginning + "+" + subaddressString + "@" + emailEnding;
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
|
||||||
toEvaluator() {
|
|
||||||
return pipe(map((_) => new DefaultPolicyEvaluator<SubaddressGenerationOptions>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.generate} */
|
|
||||||
generate(options: SubaddressGenerationOptions) {
|
|
||||||
return this.usernameService.generateSubaddress(options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string> {
|
|
||||||
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<string> {
|
|
||||||
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<string> {
|
|
||||||
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<string> {
|
|
||||||
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<string> {
|
|
||||||
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<UsernameGeneratorOptions> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
libs/common/src/tools/generator/util.ts
Normal file
41
libs/common/src/tools/generator/util.ts
Normal file
@@ -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<Value>(defaultValue: Value) {
|
||||||
|
const _subjects = new Map<UserId, BehaviorSubject<Value>>();
|
||||||
|
|
||||||
|
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<Value>(create: (userId: UserId) => SingleUserState<Value>) {
|
||||||
|
const _subjects = new Map<UserId, SingleUserState<Value>>();
|
||||||
|
|
||||||
|
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<Value>(key: UserKeyDefinition<Value>, provider: StateProvider) {
|
||||||
|
return (id: UserId) => provider.getUser<Value>(id, key);
|
||||||
|
}
|
||||||
6
libs/common/src/tools/generator/word-options.ts
Normal file
6
libs/common/src/tools/generator/word-options.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type WordOptions = {
|
||||||
|
/** set the first letter uppercase */
|
||||||
|
titleCase?: boolean;
|
||||||
|
/** append a number */
|
||||||
|
number?: boolean;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user