diff --git a/libs/common/src/tools/types.ts b/libs/common/src/tools/types.ts index ec1903e622..2366ab18f5 100644 --- a/libs/common/src/tools/types.ts +++ b/libs/common/src/tools/types.ts @@ -1,5 +1,7 @@ import { Simplify } from "type-fest"; +import { IntegrationId } from "./integration"; + /** Constraints that are shared by all primitive field types */ type PrimitiveConstraint = { /** `true` indicates the field is required; otherwise the field is optional */ @@ -144,4 +146,6 @@ export type VaultItemRequest = { /** Options that provide contextual information about the application state * when a generator is invoked. */ -export type GenerationRequest = Partial; +export type GenerationRequest = Partial & Partial<{ + integration: IntegrationId | null +}>; diff --git a/libs/tools/generator/core/src/data/generator-types.ts b/libs/tools/generator/core/src/data/generator-types.ts index 6c351b82e3..2b777e8a4a 100644 --- a/libs/tools/generator/core/src/data/generator-types.ts +++ b/libs/tools/generator/core/src/data/generator-types.ts @@ -5,7 +5,7 @@ export const PasswordAlgorithms = Object.freeze(["password", "passphrase"] as co export const UsernameAlgorithms = Object.freeze(["username"] as const); /** Types of email addresses that may be generated by the credential generator */ -export const EmailAlgorithms = Object.freeze(["catchall", "forwarder", "subaddress"] as const); +export const EmailAlgorithms = Object.freeze(["catchall", "forwarder", "subaddress", "addyio"] as const); /** All types of credentials that may be generated by the credential generator */ export const CredentialAlgorithms = Object.freeze([ diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts index 2c96b0c2d3..2dd011f472 100644 --- a/libs/tools/generator/core/src/data/generators.ts +++ b/libs/tools/generator/core/src/data/generators.ts @@ -1,9 +1,11 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; -import { Randomizer } from "../abstractions"; import { EmailRandomizer, PasswordRandomizer, UsernameRandomizer } from "../engine"; +import { Forwarder } from "../engine/forwarder"; +import { AddyIoSettings } from "../integration"; import { DefaultPolicyEvaluator, DynamicPasswordPolicyConstraints, @@ -25,6 +27,7 @@ import { CredentialGenerator, CredentialGeneratorConfiguration, EffUsernameGenerationOptions, + GeneratorDependencyProvider, NoPolicy, PassphraseGenerationOptions, PassphraseGeneratorPolicy, @@ -33,6 +36,7 @@ import { SubaddressGenerationOptions, } from "../types"; +import { DefaultAddyIoOptions } from "./default-addy-io-options"; import { DefaultCatchallOptions } from "./default-catchall-options"; import { DefaultEffUsernameOptions } from "./default-eff-username-options"; import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries"; @@ -47,8 +51,8 @@ const PASSPHRASE = Object.freeze({ nameKey: "passphrase", onlyOnRequest: false, engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new PasswordRandomizer(randomizer); + create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + return new PasswordRandomizer(dependencies.randomizer); }, }, settings: { @@ -84,8 +88,8 @@ const PASSWORD = Object.freeze({ nameKey: "password", onlyOnRequest: false, engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new PasswordRandomizer(randomizer); + create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + return new PasswordRandomizer(dependencies.randomizer); }, }, settings: { @@ -129,8 +133,8 @@ const USERNAME = Object.freeze({ nameKey: "randomWord", onlyOnRequest: false, engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new UsernameRandomizer(randomizer); + create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + return new UsernameRandomizer(dependencies.randomizer); }, }, settings: { @@ -160,8 +164,8 @@ const CATCHALL = Object.freeze({ descriptionKey: "catchallEmailDesc", onlyOnRequest: false, engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new EmailRandomizer(randomizer); + create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); }, }, settings: { @@ -191,8 +195,8 @@ const SUBADDRESS = Object.freeze({ descriptionKey: "plusAddressedEmailDesc", onlyOnRequest: false, engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new EmailRandomizer(randomizer); + create(dependencies: GeneratorDependencyProvider): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); }, }, settings: { @@ -215,6 +219,42 @@ const SUBADDRESS = Object.freeze({ }, } satisfies CredentialGeneratorConfiguration); +// FIXME: forwarders should dynamically extend generators; they shouldn't +// have their own entries. They're included here solely in order to quickly +// create generator configurations during UI modernization +const ADDYIO = Object.freeze({ + id: "addyio", + category: "email", + nameKey: "addyIoKey", + onlyOnRequest: true, + engine: { + create(dependencies: GeneratorDependencyProvider) { + return new Forwarder(dependencies.client, dependencies.i18nService); + } + }, + settings: { + initial: DefaultAddyIoOptions, + constraints: {}, + account: new UserKeyDefinition(GENERATOR_DISK, "addyIoGenerator", { + deserializer: (value) => value, + clearOn: [], + }) + }, + policy: { + type: PolicyType.PasswordGenerator, + disabledValue: {}, + combine(_acc: NoPolicy, _policy: Policy) { + return {}; + }, + createEvaluator(_policy: NoPolicy) { + return new DefaultPolicyEvaluator(); + }, + toConstraints(_policy: NoPolicy) { + return new IdentityConstraint(); + }, + } +} satisfies CredentialGeneratorConfiguration); + /** Generator configurations */ export const Generators = Object.freeze({ /** Passphrase generator configuration */ @@ -231,4 +271,6 @@ export const Generators = Object.freeze({ /** Email subaddress generator configuration */ subaddress: SUBADDRESS, + + addyio: ADDYIO, }); diff --git a/libs/tools/generator/core/src/data/integrations.ts b/libs/tools/generator/core/src/data/integrations.ts index 6132891b36..52cf403d18 100644 --- a/libs/tools/generator/core/src/data/integrations.ts +++ b/libs/tools/generator/core/src/data/integrations.ts @@ -1,3 +1,7 @@ +import { IntegrationId } from "@bitwarden/common/tools/integration"; +import { ApiSettings } from "@bitwarden/common/tools/integration/rpc"; + +import { ForwarderConfiguration } from "../engine"; import { AddyIo } from "../integration/addy-io"; import { DuckDuckGo } from "../integration/duck-duck-go"; import { Fastmail } from "../integration/fastmail"; @@ -13,3 +17,9 @@ export const Integrations = Object.freeze({ ForwardEmail, SimpleLogin, } as const); + +const integrations = Object.fromEntries(Object.values(Integrations).map((i) => [i.id, i as ForwarderConfiguration])); + +export function getIntegration(id: IntegrationId) : ForwarderConfiguration { + return integrations[id as string] +} diff --git a/libs/tools/generator/core/src/engine/forwarder.ts b/libs/tools/generator/core/src/engine/forwarder.ts new file mode 100644 index 0000000000..a6c4e5f3cc --- /dev/null +++ b/libs/tools/generator/core/src/engine/forwarder.ts @@ -0,0 +1,80 @@ +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ApiSettings, IntegrationRequest, RestClient } from "@bitwarden/common/tools/integration/rpc"; +import { GenerationRequest } from "@bitwarden/common/tools/types"; + +import { getIntegration } from "../data"; +import { + CredentialGenerator, + GeneratedCredential, +} from "../types"; + +import { AccountRequest, ForwarderConfiguration } from "./forwarder-configuration"; +import { ForwarderContext } from "./forwarder-context"; +import { CreateForwardingAddressRpc, GetAccountIdRpc } from "./rpc"; + +/** Generation algorithms that produce randomized email addresses */ +export class Forwarder + implements + CredentialGenerator +{ + /** Instantiates the email randomizer + * @param random data source for random data + */ + constructor(private client: RestClient, private i18nService: I18nService) {} + + async generate( + request: GenerationRequest, + settings: ApiSettings, + ) { + if(!request.integration) { + throw new Error("Invalid integration request received by generator."); + } + + const integration = getIntegration(request.integration); + if(!integration) { + throw new Error("Invalid integration request received by generator."); + } + + const requestOptions: IntegrationRequest & AccountRequest = { website: request.website }; + + const getAccount = await this.getAccountId(integration, settings); + if (getAccount) { + requestOptions.accountId = await this.client.fetchJson(getAccount, requestOptions); + } + + const create = this.createForwardingAddress(integration, settings); + const result = await this.client.fetchJson(create, requestOptions); + + return new GeneratedCredential(result, "forwarder", Date.now()); + } + + private createContext( + configuration: ForwarderConfiguration, + settings: Settings, + ) { + return new ForwarderContext(configuration, settings, this.i18nService); + } + + private createForwardingAddress( + configuration: ForwarderConfiguration, + settings: Settings, + ) { + const context = this.createContext(configuration, settings); + const rpc = new CreateForwardingAddressRpc(configuration, context); + return rpc; + } + + private getAccountId( + configuration: ForwarderConfiguration, + settings: Settings, + ) { + if (!configuration.forwarder.getAccountId) { + return null; + } + + const context = this.createContext(configuration, settings); + const rpc = new GetAccountIdRpc(configuration, context); + + return rpc; + } +} diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index 693ffd654d..a8c84eac99 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -22,12 +22,15 @@ import { Simplify } from "type-fest"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { OnDependency, SingleUserDependency, UserDependency, } from "@bitwarden/common/tools/dependencies"; +import { IntegrationId } from "@bitwarden/common/tools/integration"; +import { RestClient } from "@bitwarden/common/tools/integration/rpc"; import { isDynamic } from "@bitwarden/common/tools/state/state-constraints-dependency"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; @@ -42,7 +45,7 @@ import { CredentialGeneratorInfo, CredentialPreference, } from "../types"; -import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration"; +import { CredentialGeneratorConfiguration as Configuration, GeneratorDependencyProvider } from "../types/credential-generator-configuration"; import { GeneratorConstraints } from "../types/generator-constraints"; import { PREFERENCES } from "./credential-preferences"; @@ -59,6 +62,8 @@ type Generate$Dependencies = Simplify & Partial; + + integration$?: Observable }; type Algorithms$Dependencies = Partial; @@ -68,8 +73,18 @@ export class CredentialGeneratorService { private randomizer: Randomizer, private stateProvider: StateProvider, private policyService: PolicyService, + private client: RestClient, + private i18nService: I18nService ) {} + private getDependencyProvider() : GeneratorDependencyProvider { + return { + client: this.client, + i18nService: this.i18nService, + randomizer: this.randomizer + }; + } + // FIXME: the rxjs methods of this service can be a lot more resilient if // `Subjects` are introduced where sharing occurs @@ -84,11 +99,14 @@ export class CredentialGeneratorService { dependencies?: Generate$Dependencies, ) { // instantiate the engine - const engine = configuration.engine.create(this.randomizer); + const engine = configuration.engine.create(this.getDependencyProvider()); // stream blocks until all of these values are received const website$ = dependencies?.website$ ?? new BehaviorSubject(null); - const request$ = website$.pipe(map((website) => ({ website }))); + const integration$ = dependencies?.integration$ ?? new BehaviorSubject(null); + const request$ = combineLatest([website$, integration$]).pipe( + map(([website, integration]) => ({ website, integration })) + ); const settings$ = this.settings$(configuration, dependencies); // monitor completion diff --git a/libs/tools/generator/core/src/types/credential-generator-configuration.ts b/libs/tools/generator/core/src/types/credential-generator-configuration.ts index 8302450d44..2994f690ea 100644 --- a/libs/tools/generator/core/src/types/credential-generator-configuration.ts +++ b/libs/tools/generator/core/src/types/credential-generator-configuration.ts @@ -1,4 +1,6 @@ +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { RestClient } from "@bitwarden/common/tools/integration/rpc"; import { Constraints } from "@bitwarden/common/tools/types"; import { Randomizer } from "../abstractions"; @@ -6,6 +8,12 @@ import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from ".. import { CredentialGenerator } from "./credential-generator"; +export type GeneratorDependencyProvider = { + randomizer: Randomizer, + client: RestClient, + i18nService: I18nService +}; + /** Credential generator metadata common across credential generators */ export type CredentialGeneratorInfo = { /** Uniquely identifies the credential configuration @@ -40,7 +48,7 @@ export type CredentialGeneratorConfiguration = CredentialGener // the credential generator, but engine configurations should return // the underlying type. `create` may be able to do double-duty w/ an // engine definition if `CredentialGenerator` can be made covariant. - create: (randomizer: Randomizer) => CredentialGenerator; + create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator; }; /** Defines the stored parameters for credential generation */ settings: {