mirror of
https://github.com/bitwarden/browser
synced 2025-12-30 15:13:32 +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:
@@ -7,12 +7,13 @@ import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { Randomizer } from "../abstractions/randomizer";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||
|
||||
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
|
||||
import { DefaultCatchallOptions } from "./catchall-generator-options";
|
||||
|
||||
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
import { CatchallGeneratorStrategy } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomePolicy = mock<Policy>({
|
||||
@@ -40,8 +41,8 @@ describe("Email subaddress list generation strategy", () => {
|
||||
describe("durableState", () => {
|
||||
it("should use password settings key", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new CatchallGeneratorStrategy(legacy, provider);
|
||||
const randomizer = mock<Randomizer>();
|
||||
const strategy = new CatchallGeneratorStrategy(randomizer, provider);
|
||||
|
||||
strategy.durableState(SomeUser);
|
||||
|
||||
@@ -61,26 +62,14 @@ describe("Email subaddress list generation strategy", () => {
|
||||
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||
const randomizer = mock<Randomizer>();
|
||||
const strategy = new CatchallGeneratorStrategy(randomizer, null);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generate()", () => {
|
||||
it("should call the legacy service with the given options", async () => {
|
||||
const legacy = mock<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);
|
||||
});
|
||||
it.todo("generate catchall email addresses");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { BehaviorSubject, map, pipe } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { Randomizer } from "../abstractions/randomizer";
|
||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||
import { NoPolicy } from "../no-policy";
|
||||
import { newDefaultEvaluator } from "../rx-operators";
|
||||
import { clone$PerUserId, sharedStateByUserId } from "../util";
|
||||
|
||||
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
|
||||
|
||||
@@ -19,34 +17,34 @@ export class CatchallGeneratorStrategy
|
||||
* @param usernameService generates a catchall address for a domain
|
||||
*/
|
||||
constructor(
|
||||
private usernameService: UsernameGenerationServiceAbstraction,
|
||||
private random: Randomizer,
|
||||
private stateProvider: StateProvider,
|
||||
private defaultOptions: CatchallGenerationOptions = DefaultCatchallOptions,
|
||||
) {}
|
||||
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState(id: UserId) {
|
||||
return this.stateProvider.getUser(id, CATCHALL_SETTINGS);
|
||||
}
|
||||
// configuration
|
||||
durableState = sharedStateByUserId(CATCHALL_SETTINGS, this.stateProvider);
|
||||
defaults$ = clone$PerUserId(this.defaultOptions);
|
||||
toEvaluator = newDefaultEvaluator<CatchallGenerationOptions>();
|
||||
readonly policy = PolicyType.PasswordGenerator;
|
||||
|
||||
/** {@link GeneratorStrategy.defaults$} */
|
||||
defaults$(userId: UserId) {
|
||||
return new BehaviorSubject({ ...DefaultCatchallOptions }).asObservable();
|
||||
}
|
||||
// algorithm
|
||||
async generate(options: CatchallGenerationOptions) {
|
||||
const o = Object.assign({}, DefaultCatchallOptions, options);
|
||||
|
||||
/** {@link GeneratorStrategy.policy} */
|
||||
get policy() {
|
||||
// Uses password generator since there aren't policies
|
||||
// specific to usernames.
|
||||
return PolicyType.PasswordGenerator;
|
||||
}
|
||||
if (o.catchallDomain == null || o.catchallDomain === "") {
|
||||
return null;
|
||||
}
|
||||
if (o.catchallType == null) {
|
||||
o.catchallType = "random";
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator() {
|
||||
return pipe(map((_) => new DefaultPolicyEvaluator<CatchallGenerationOptions>()));
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.generate} */
|
||||
generate(options: CatchallGenerationOptions) {
|
||||
return this.usernameService.generateCatchall(options);
|
||||
let startString = "";
|
||||
if (o.catchallType === "random") {
|
||||
startString = await this.random.chars(8);
|
||||
} else if (o.catchallType === "website-name") {
|
||||
startString = o.website;
|
||||
}
|
||||
return startString + "@" + o.catchallDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { Randomizer } from "../abstractions/randomizer";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||
|
||||
import { DefaultEffUsernameOptions } from "./eff-username-generator-options";
|
||||
|
||||
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
import { EffUsernameGeneratorStrategy } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomePolicy = mock<Policy>({
|
||||
@@ -40,8 +41,8 @@ describe("EFF long word list generation strategy", () => {
|
||||
describe("durableState", () => {
|
||||
it("should use password settings key", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new EffUsernameGeneratorStrategy(legacy, provider);
|
||||
const randomizer = mock<Randomizer>();
|
||||
const strategy = new EffUsernameGeneratorStrategy(randomizer, provider);
|
||||
|
||||
strategy.durableState(SomeUser);
|
||||
|
||||
@@ -61,26 +62,14 @@ describe("EFF long word list generation strategy", () => {
|
||||
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||
const randomizer = mock<Randomizer>();
|
||||
const strategy = new EffUsernameGeneratorStrategy(randomizer, null);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generate()", () => {
|
||||
it("should call the legacy service with the given options", async () => {
|
||||
const legacy = mock<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);
|
||||
});
|
||||
it.todo("generate username tests");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { BehaviorSubject, map, pipe } from "rxjs";
|
||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { Randomizer } from "../abstractions/randomizer";
|
||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||
import { NoPolicy } from "../no-policy";
|
||||
import { newDefaultEvaluator } from "../rx-operators";
|
||||
import { clone$PerUserId, sharedStateByUserId } from "../util";
|
||||
|
||||
import {
|
||||
DefaultEffUsernameOptions,
|
||||
@@ -22,34 +22,23 @@ export class EffUsernameGeneratorStrategy
|
||||
* @param usernameService generates a username from EFF word list
|
||||
*/
|
||||
constructor(
|
||||
private usernameService: UsernameGenerationServiceAbstraction,
|
||||
private random: Randomizer,
|
||||
private stateProvider: StateProvider,
|
||||
private defaultOptions: EffUsernameGenerationOptions = DefaultEffUsernameOptions,
|
||||
) {}
|
||||
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState(id: UserId) {
|
||||
return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS);
|
||||
}
|
||||
// configuration
|
||||
durableState = sharedStateByUserId(EFF_USERNAME_SETTINGS, this.stateProvider);
|
||||
defaults$ = clone$PerUserId(this.defaultOptions);
|
||||
toEvaluator = newDefaultEvaluator<EffUsernameGenerationOptions>();
|
||||
readonly policy = PolicyType.PasswordGenerator;
|
||||
|
||||
/** {@link GeneratorStrategy.defaults$} */
|
||||
defaults$(userId: UserId) {
|
||||
return new BehaviorSubject({ ...DefaultEffUsernameOptions }).asObservable();
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.policy} */
|
||||
get policy() {
|
||||
// Uses password generator since there aren't policies
|
||||
// specific to usernames.
|
||||
return PolicyType.PasswordGenerator;
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator() {
|
||||
return pipe(map((_) => new DefaultPolicyEvaluator<EffUsernameGenerationOptions>()));
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.generate} */
|
||||
generate(options: EffUsernameGenerationOptions) {
|
||||
return this.usernameService.generateWord(options);
|
||||
// algorithm
|
||||
async generate(options: EffUsernameGenerationOptions) {
|
||||
const word = await this.random.pickWord(EFFLongWordList, {
|
||||
titleCase: options.wordCapitalize ?? DefaultEffUsernameOptions.wordCapitalize,
|
||||
number: options.wordIncludeNumber ?? DefaultEffUsernameOptions.wordIncludeNumber,
|
||||
});
|
||||
return word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
||||
keyService: CryptoService,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(encryptService, keyService, stateProvider);
|
||||
super(encryptService, keyService, stateProvider, { website: null, token: "" });
|
||||
}
|
||||
|
||||
get key() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Observable, map, pipe } from "rxjs";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
@@ -13,8 +13,9 @@ import { SecretKeyDefinition } from "../../state/secret-key-definition";
|
||||
import { SecretState } from "../../state/secret-state";
|
||||
import { UserKeyEncryptor } from "../../state/user-key-encryptor";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { NoPolicy } from "../no-policy";
|
||||
import { newDefaultEvaluator } from "../rx-operators";
|
||||
import { clone$PerUserId, sharedByUserId } from "../util";
|
||||
|
||||
import { ApiOptions } from "./options/forwarder-options";
|
||||
|
||||
@@ -33,29 +34,25 @@ export abstract class ForwarderGeneratorStrategy<
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly keyService: CryptoService,
|
||||
private stateProvider: StateProvider,
|
||||
private readonly defaultOptions: Options,
|
||||
) {
|
||||
super();
|
||||
// Uses password generator since there aren't policies
|
||||
// specific to usernames.
|
||||
this.policy = PolicyType.PasswordGenerator;
|
||||
}
|
||||
|
||||
private durableStates = new Map<UserId, SingleUserState<Options>>();
|
||||
/** configures forwarder secret storage */
|
||||
protected abstract readonly key: UserKeyDefinition<Options>;
|
||||
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState = (userId: UserId) => {
|
||||
let state = this.durableStates.get(userId);
|
||||
/** configures forwarder import buffer */
|
||||
protected abstract readonly rolloverKey: BufferedKeyDefinition<Options, Options>;
|
||||
|
||||
if (!state) {
|
||||
state = this.createState(userId);
|
||||
// configuration
|
||||
readonly policy = PolicyType.PasswordGenerator;
|
||||
defaults$ = clone$PerUserId(this.defaultOptions);
|
||||
toEvaluator = newDefaultEvaluator<Options>();
|
||||
durableState = sharedByUserId((userId) => this.getUserSecrets(userId));
|
||||
|
||||
this.durableStates.set(userId, state);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
private createState(userId: UserId): SingleUserState<Options> {
|
||||
// per-user encrypted state
|
||||
private getUserSecrets(userId: UserId): SingleUserState<Options> {
|
||||
// construct the encryptor
|
||||
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
|
||||
const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
||||
@@ -92,18 +89,4 @@ export abstract class ForwarderGeneratorStrategy<
|
||||
|
||||
return rolloverState;
|
||||
}
|
||||
|
||||
/** Gets the default options. */
|
||||
abstract defaults$: (userId: UserId) => Observable<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 { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "../../../../platform/state";
|
||||
import { UserId } from "../../../../types/guid";
|
||||
import { ADDY_IO_FORWARDER, ADDY_IO_BUFFER } from "../../key-definitions";
|
||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||
import { Forwarders } from "../options/constants";
|
||||
@@ -36,25 +33,14 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy<
|
||||
keyService: CryptoService,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(encryptService, keyService, stateProvider);
|
||||
super(encryptService, keyService, stateProvider, DefaultAddyIoOptions);
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.key} */
|
||||
get key() {
|
||||
return ADDY_IO_FORWARDER;
|
||||
}
|
||||
// configuration
|
||||
readonly key = ADDY_IO_FORWARDER;
|
||||
readonly rolloverKey = ADDY_IO_BUFFER;
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||
get rolloverKey() {
|
||||
return ADDY_IO_BUFFER;
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||
defaults$ = (userId: UserId) => {
|
||||
return new BehaviorSubject({ ...DefaultAddyIoOptions });
|
||||
};
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||
// request
|
||||
generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => {
|
||||
if (!options.token || options.token === "") {
|
||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "../../../../platform/state";
|
||||
import { UserId } from "../../../../types/guid";
|
||||
import { DUCK_DUCK_GO_FORWARDER, DUCK_DUCK_GO_BUFFER } from "../../key-definitions";
|
||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||
import { Forwarders } from "../options/constants";
|
||||
@@ -32,25 +29,14 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions>
|
||||
keyService: CryptoService,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(encryptService, keyService, stateProvider);
|
||||
super(encryptService, keyService, stateProvider, DefaultDuckDuckGoOptions);
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.key} */
|
||||
get key() {
|
||||
return DUCK_DUCK_GO_FORWARDER;
|
||||
}
|
||||
// configuration
|
||||
readonly key = DUCK_DUCK_GO_FORWARDER;
|
||||
readonly rolloverKey = DUCK_DUCK_GO_BUFFER;
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||
get rolloverKey() {
|
||||
return DUCK_DUCK_GO_BUFFER;
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||
defaults$ = (userId: UserId) => {
|
||||
return new BehaviorSubject({ ...DefaultDuckDuckGoOptions });
|
||||
};
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||
// request
|
||||
generate = async (options: ApiOptions): Promise<string> => {
|
||||
if (!options.token || options.token === "") {
|
||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "../../../../platform/state";
|
||||
import { UserId } from "../../../../types/guid";
|
||||
import { FASTMAIL_FORWARDER, FASTMAIL_BUFFER } from "../../key-definitions";
|
||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||
import { Forwarders } from "../options/constants";
|
||||
@@ -34,25 +31,14 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
|
||||
keyService: CryptoService,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(encryptService, keyService, stateProvider);
|
||||
super(encryptService, keyService, stateProvider, DefaultFastmailOptions);
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.key} */
|
||||
get key() {
|
||||
return FASTMAIL_FORWARDER;
|
||||
}
|
||||
// configuration
|
||||
readonly key = FASTMAIL_FORWARDER;
|
||||
readonly rolloverKey = FASTMAIL_BUFFER;
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||
defaults$ = (userId: UserId) => {
|
||||
return new BehaviorSubject({ ...DefaultFastmailOptions });
|
||||
};
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||
get rolloverKey() {
|
||||
return FASTMAIL_BUFFER;
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||
// request
|
||||
generate = async (options: ApiOptions & EmailPrefixOptions) => {
|
||||
if (!options.token || options.token === "") {
|
||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.Fastmail.name);
|
||||
@@ -76,7 +62,7 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
|
||||
"new-masked-email": {
|
||||
state: "enabled",
|
||||
description: "",
|
||||
forDomain: options.website,
|
||||
forDomain: options.website ?? "",
|
||||
emailPrefix: options.prefix,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "../../../../platform/state";
|
||||
import { UserId } from "../../../../types/guid";
|
||||
import { FIREFOX_RELAY_FORWARDER, FIREFOX_RELAY_BUFFER } from "../../key-definitions";
|
||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||
import { Forwarders } from "../options/constants";
|
||||
@@ -32,25 +29,14 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions
|
||||
keyService: CryptoService,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(encryptService, keyService, stateProvider);
|
||||
super(encryptService, keyService, stateProvider, DefaultFirefoxRelayOptions);
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.key} */
|
||||
get key() {
|
||||
return FIREFOX_RELAY_FORWARDER;
|
||||
}
|
||||
// configuration
|
||||
readonly key = FIREFOX_RELAY_FORWARDER;
|
||||
readonly rolloverKey = FIREFOX_RELAY_BUFFER;
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||
get rolloverKey() {
|
||||
return FIREFOX_RELAY_BUFFER;
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||
defaults$ = (userId: UserId) => {
|
||||
return new BehaviorSubject({ ...DefaultFirefoxRelayOptions });
|
||||
};
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||
// request
|
||||
generate = async (options: ApiOptions) => {
|
||||
if (!options.token || options.token === "") {
|
||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.FirefoxRelay.name);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||
import { Utils } from "../../../../platform/misc/utils";
|
||||
import { StateProvider } from "../../../../platform/state";
|
||||
import { UserId } from "../../../../types/guid";
|
||||
import { FORWARD_EMAIL_FORWARDER, FORWARD_EMAIL_BUFFER } from "../../key-definitions";
|
||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||
import { Forwarders } from "../options/constants";
|
||||
@@ -36,25 +33,14 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy<
|
||||
keyService: CryptoService,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(encryptService, keyService, stateProvider);
|
||||
super(encryptService, keyService, stateProvider, DefaultForwardEmailOptions);
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.key} */
|
||||
get key() {
|
||||
return FORWARD_EMAIL_FORWARDER;
|
||||
}
|
||||
// configuration
|
||||
readonly key = FORWARD_EMAIL_FORWARDER;
|
||||
readonly rolloverKey = FORWARD_EMAIL_BUFFER;
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||
defaults$ = (userId: UserId) => {
|
||||
return new BehaviorSubject({ ...DefaultForwardEmailOptions });
|
||||
};
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||
get rolloverKey() {
|
||||
return FORWARD_EMAIL_BUFFER;
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||
// request
|
||||
generate = async (options: ApiOptions & EmailDomainOptions) => {
|
||||
if (!options.token || options.token === "") {
|
||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.ForwardEmail.name);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "../../../../platform/state";
|
||||
import { UserId } from "../../../../types/guid";
|
||||
import { SIMPLE_LOGIN_FORWARDER, SIMPLE_LOGIN_BUFFER } from "../../key-definitions";
|
||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||
import { Forwarders } from "../options/constants";
|
||||
@@ -33,25 +30,14 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedA
|
||||
keyService: CryptoService,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(encryptService, keyService, stateProvider);
|
||||
super(encryptService, keyService, stateProvider, DefaultSimpleLoginOptions);
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.key} */
|
||||
get key() {
|
||||
return SIMPLE_LOGIN_FORWARDER;
|
||||
}
|
||||
// configuration
|
||||
readonly key = SIMPLE_LOGIN_FORWARDER;
|
||||
readonly rolloverKey = SIMPLE_LOGIN_BUFFER;
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||
get rolloverKey() {
|
||||
return SIMPLE_LOGIN_BUFFER;
|
||||
}
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||
defaults$ = (userId: UserId) => {
|
||||
return new BehaviorSubject({ ...DefaultSimpleLoginOptions });
|
||||
};
|
||||
|
||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||
// request
|
||||
generate = async (options: SelfHostedApiOptions) => {
|
||||
if (!options.token || options.token === "") {
|
||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.SimpleLogin.name);
|
||||
|
||||
@@ -3,4 +3,3 @@ export { CatchallGeneratorStrategy } from "./catchall-generator-strategy";
|
||||
export { SubaddressGeneratorStrategy } from "./subaddress-generator-strategy";
|
||||
export { UsernameGeneratorOptions } from "./username-generation-options";
|
||||
export { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||
export { UsernameGenerationService } from "./username-generation.service";
|
||||
|
||||
@@ -7,15 +7,13 @@ import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { Randomizer } from "../abstractions/randomizer";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||
|
||||
import {
|
||||
DefaultSubaddressOptions,
|
||||
SubaddressGenerationOptions,
|
||||
} from "./subaddress-generator-options";
|
||||
import { DefaultSubaddressOptions } from "./subaddress-generator-options";
|
||||
|
||||
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
import { SubaddressGeneratorStrategy } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomePolicy = mock<Policy>({
|
||||
@@ -43,8 +41,8 @@ describe("Email subaddress list generation strategy", () => {
|
||||
describe("durableState", () => {
|
||||
it("should use password settings key", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new SubaddressGeneratorStrategy(legacy, provider);
|
||||
const randomizer = mock<Randomizer>();
|
||||
const strategy = new SubaddressGeneratorStrategy(randomizer, provider);
|
||||
|
||||
strategy.durableState(SomeUser);
|
||||
|
||||
@@ -64,26 +62,14 @@ describe("Email subaddress list generation strategy", () => {
|
||||
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||
const randomizer = mock<Randomizer>();
|
||||
const strategy = new SubaddressGeneratorStrategy(randomizer, null);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generate()", () => {
|
||||
it("should call the legacy service with the given options", async () => {
|
||||
const legacy = mock<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);
|
||||
});
|
||||
it.todo("generate email subaddress tests");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { BehaviorSubject, map, pipe } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { Randomizer } from "../abstractions/randomizer";
|
||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||
import { NoPolicy } from "../no-policy";
|
||||
import { newDefaultEvaluator } from "../rx-operators";
|
||||
import { clone$PerUserId, sharedStateByUserId } from "../util";
|
||||
|
||||
import {
|
||||
DefaultSubaddressOptions,
|
||||
@@ -26,34 +24,42 @@ export class SubaddressGeneratorStrategy
|
||||
* @param usernameService generates an email subaddress from an email address
|
||||
*/
|
||||
constructor(
|
||||
private usernameService: UsernameGenerationServiceAbstraction,
|
||||
private random: Randomizer,
|
||||
private stateProvider: StateProvider,
|
||||
private defaultOptions: SubaddressGenerationOptions = DefaultSubaddressOptions,
|
||||
) {}
|
||||
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState(id: UserId) {
|
||||
return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS);
|
||||
}
|
||||
// configuration
|
||||
durableState = sharedStateByUserId(SUBADDRESS_SETTINGS, this.stateProvider);
|
||||
defaults$ = clone$PerUserId(this.defaultOptions);
|
||||
toEvaluator = newDefaultEvaluator<SubaddressGenerationOptions>();
|
||||
readonly policy = PolicyType.PasswordGenerator;
|
||||
|
||||
/** {@link GeneratorStrategy.defaults$} */
|
||||
defaults$(userId: UserId) {
|
||||
return new BehaviorSubject({ ...DefaultSubaddressOptions }).asObservable();
|
||||
}
|
||||
// algorithm
|
||||
async generate(options: SubaddressGenerationOptions) {
|
||||
const o = Object.assign({}, DefaultSubaddressOptions, options);
|
||||
|
||||
/** {@link GeneratorStrategy.policy} */
|
||||
get policy() {
|
||||
// Uses password generator since there aren't policies
|
||||
// specific to usernames.
|
||||
return PolicyType.PasswordGenerator;
|
||||
}
|
||||
const subaddressEmail = o.subaddressEmail;
|
||||
if (subaddressEmail == null || subaddressEmail.length < 3) {
|
||||
return o.subaddressEmail;
|
||||
}
|
||||
const atIndex = subaddressEmail.indexOf("@");
|
||||
if (atIndex < 1 || atIndex >= subaddressEmail.length - 1) {
|
||||
return subaddressEmail;
|
||||
}
|
||||
if (o.subaddressType == null) {
|
||||
o.subaddressType = "random";
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator() {
|
||||
return pipe(map((_) => new DefaultPolicyEvaluator<SubaddressGenerationOptions>()));
|
||||
}
|
||||
const emailBeginning = subaddressEmail.substr(0, atIndex);
|
||||
const emailEnding = subaddressEmail.substr(atIndex + 1, subaddressEmail.length);
|
||||
|
||||
/** {@link GeneratorStrategy.generate} */
|
||||
generate(options: SubaddressGenerationOptions) {
|
||||
return this.usernameService.generateSubaddress(options);
|
||||
let subaddressString = "";
|
||||
if (o.subaddressType === "random") {
|
||||
subaddressString = await this.random.chars(8);
|
||||
} else if (o.subaddressType === "website-name") {
|
||||
subaddressString = o.website;
|
||||
}
|
||||
return emailBeginning + "+" + subaddressString + "@" + emailEnding;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user