diff --git a/angular/src/components/generator.component.ts b/angular/src/components/generator.component.ts index 6b6800cc242..9ef891099e5 100644 --- a/angular/src/components/generator.component.ts +++ b/angular/src/components/generator.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { first } from "rxjs/operators"; import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { StateService } from "jslib-common/abstractions/state.service"; @@ -15,6 +16,7 @@ export class GeneratorComponent implements OnInit { @Input() type: string; @Output() onSelected = new EventEmitter(); + usernameGeneratingPromise: Promise; typeOptions: any[]; passTypeOptions: any[]; usernameTypeOptions: any[]; @@ -36,6 +38,7 @@ export class GeneratorComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, protected stateService: StateService, protected i18nService: I18nService, + protected logService: LogService, protected route: ActivatedRoute, private win: Window ) { @@ -58,13 +61,20 @@ export class GeneratorComponent implements OnInit { value: "catchall", desc: i18nService.t("catchallEmailDesc"), }, + { + name: i18nService.t("forwardedEmail"), + value: "forwarded", + desc: i18nService.t("forwardedEmailDesc"), + }, { name: i18nService.t("randomWord"), value: "word" }, ]; this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }]; this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }]; this.forwardOptions = [ { name: "SimpleLogin", value: "simplelogin" }, - { name: "FastMail", value: "fastmail" }, + { name: "AnonAddy", value: "anonaddy" }, + { name: "Firefox Relay", value: "firefoxrelay" }, + // { name: "FastMail", value: "fastmail" }, ]; } @@ -104,13 +114,17 @@ export class GeneratorComponent implements OnInit { this.type = generatorOptions?.type ?? "password"; } } - await this.regenerate(); + if (this.regenerateWithoutButtonPress()) { + await this.regenerate(); + } }); } async typeChanged() { await this.stateService.setGeneratorOptions({ type: this.type }); - await this.regenerate(); + if (this.regenerateWithoutButtonPress()) { + await this.regenerate(); + } } async regenerate() { @@ -135,14 +149,17 @@ export class GeneratorComponent implements OnInit { this.normalizePasswordOptions(); await this.passwordGenerationService.saveOptions(this.passwordOptions); - if (regenerate) { + if (regenerate && this.regenerateWithoutButtonPress()) { await this.regeneratePassword(); } } async saveUsernameOptions(regenerate = true) { await this.usernameGenerationService.saveOptions(this.usernameOptions); - if (regenerate) { + if (this.usernameOptions.type === "forwarded") { + this.username = "-"; + } + if (regenerate && this.regenerateWithoutButtonPress()) { await this.regenerateUsername(); } } @@ -157,9 +174,16 @@ export class GeneratorComponent implements OnInit { } async generateUsername() { - this.username = await this.usernameGenerationService.generateUsername(this.usernameOptions); - if (this.username === "" || this.username === null) { - this.username = "-"; + try { + this.usernameGeneratingPromise = this.usernameGenerationService.generateUsername( + this.usernameOptions + ); + this.username = await this.usernameGeneratingPromise; + if (this.username === "" || this.username === null) { + this.username = "-"; + } + } catch (e) { + this.logService.error(e); } } @@ -185,6 +209,10 @@ export class GeneratorComponent implements OnInit { this.showOptions = !this.showOptions; } + regenerateWithoutButtonPress() { + return this.type !== "username" || this.usernameOptions.type !== "forwarded"; + } + private normalizePasswordOptions() { // Application level normalize options depedent on class variables this.passwordOptions.ambiguous = !this.avoidAmbiguous; diff --git a/angular/src/services/jslib-services.module.ts b/angular/src/services/jslib-services.module.ts index 68b527bc958..a230f980a59 100644 --- a/angular/src/services/jslib-services.module.ts +++ b/angular/src/services/jslib-services.module.ts @@ -232,7 +232,7 @@ export const CLIENT_TYPE = new InjectionToken("CLIENT_TYPE"); { provide: UsernameGenerationServiceAbstraction, useClass: UsernameGenerationService, - deps: [CryptoServiceAbstraction, StateServiceAbstraction], + deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction], }, { provide: ApiServiceAbstraction, diff --git a/common/src/abstractions/usernameGeneration.service.ts b/common/src/abstractions/usernameGeneration.service.ts index baba74d8e4c..b5bef9fc417 100644 --- a/common/src/abstractions/usernameGeneration.service.ts +++ b/common/src/abstractions/usernameGeneration.service.ts @@ -3,6 +3,7 @@ export abstract class UsernameGenerationService { generateWord: (options: any) => Promise; generateSubaddress: (options: any) => Promise; generateCatchall: (options: any) => Promise; + generateForwarded: (options: any) => Promise; getOptions: () => Promise; saveOptions: (options: any) => Promise; } diff --git a/common/src/services/usernameGeneration.service.ts b/common/src/services/usernameGeneration.service.ts index e4201d6e861..df8cba2b3d5 100644 --- a/common/src/services/usernameGeneration.service.ts +++ b/common/src/services/usernameGeneration.service.ts @@ -1,3 +1,4 @@ +import { ApiService } from "../abstractions/api.service"; import { CryptoService } from "../abstractions/crypto.service"; import { StateService } from "../abstractions/state.service"; import { UsernameGenerationService as BaseUsernameGenerationService } from "../abstractions/usernameGeneration.service"; @@ -9,10 +10,16 @@ const DefaultOptions = { wordIncludeNumber: true, subaddressType: "random", catchallType: "random", + forwardedType: "simplelogin", + forwardedAnonAddyDomain: "anonaddy.me", }; export class UsernameGenerationService implements BaseUsernameGenerationService { - constructor(private cryptoService: CryptoService, private stateService: StateService) {} + constructor( + private cryptoService: CryptoService, + private stateService: StateService, + private apiService: ApiService + ) {} generateUsername(options: any): Promise { if (options.type === "catchall") { @@ -20,7 +27,7 @@ export class UsernameGenerationService implements BaseUsernameGenerationService } else if (options.type === "subaddress") { return this.generateSubaddress(options); } else if (options.type === "forwarded") { - return this.generateSubaddress(options); + return this.generateForwarded(options); } else { return this.generateWord(options); } @@ -94,6 +101,46 @@ export class UsernameGenerationService implements BaseUsernameGenerationService return startString + "@" + o.catchallDomain; } + async generateForwarded(options: any): Promise { + const o = Object.assign({}, DefaultOptions, options); + + if (o.forwardedService == null) { + return null; + } + + if (o.forwardedService === "simplelogin") { + if (o.forwardedSimpleLoginApiKey == null || o.forwardedSimpleLoginApiKey === "") { + return null; + } + return this.generateSimpleLoginAlias( + o.forwardedSimpleLoginApiKey, + o.forwardedSimpleLoginHostname, + o.website + ); + } else if (o.forwardedService === "anonaddy") { + if ( + o.forwardedAnonAddyApiToken == null || + o.forwardedAnonAddyApiToken === "" || + o.forwardedAnonAddyDomain == null || + o.forwardedAnonAddyDomain == "" + ) { + return null; + } + return this.generateAnonAddyAlias( + o.forwardedAnonAddyApiToken, + o.forwardedAnonAddyDomain, + o.website + ); + } else if (o.forwardedService === "firefoxrelay") { + if (o.forwardedFirefoxApiToken == null || o.forwardedFirefoxApiToken === "") { + return null; + } + return this.generateFirefoxRelayAlias(o.forwardedFirefoxApiToken, o.website); + } + + return null; + } + async getOptions(): Promise { let options = await this.stateService.getUsernameGenerationOptions(); if (options == null) { @@ -125,4 +172,112 @@ export class UsernameGenerationService implements BaseUsernameGenerationService ? number : new Array(width - number.length + 1).join("0") + number; } + + private async generateSimpleLoginAlias( + apiKey: string, + hostname: string, + websiteNote: string + ): Promise { + if (apiKey == null || apiKey === "") { + throw "Invalid SimpleLogin API key."; + } + const requestInit: RequestInit = { + cache: "no-store", + method: "POST", + headers: new Headers({ + Authentication: apiKey, + "Content-Type": "application/json", + }), + }; + let url = "https://app.simplelogin.io/api/alias/random/new"; + if (hostname != null) { + url += "?hostname=" + hostname; + } + requestInit.body = JSON.stringify({ + note: + (websiteNote != null ? "Website: " + websiteNote + ". " : "") + "Generated by Bitwarden.", + }); + const request = new Request(url, requestInit); + const response = await this.apiService.nativeFetch(request); + if (response.status === 200 || response.status === 201) { + const json = await response.json(); + return json.alias; + } + if (response.status === 401) { + throw "Invalid SimpleLogin API key."; + } + try { + const json = await response.json(); + if (json?.error != null) { + throw "SimpleLogin error:" + json.error; + } + } catch { + // Do nothing... + } + throw "Unknown SimpleLogin error occurred."; + } + + private async generateAnonAddyAlias( + apiToken: string, + domain: string, + websiteNote: string + ): Promise { + if (apiToken == null || apiToken === "") { + throw "Invalid AnonAddy API token."; + } + const requestInit: RequestInit = { + cache: "no-store", + method: "POST", + headers: new Headers({ + Authorization: "Bearer " + apiToken, + "Content-Type": "application/json", + }), + }; + const url = "https://app.anonaddy.com/api/v1/aliases"; + requestInit.body = JSON.stringify({ + domain: domain, + description: + (websiteNote != null ? "Website: " + websiteNote + ". " : "") + "Generated by Bitwarden.", + }); + const request = new Request(url, requestInit); + const response = await this.apiService.nativeFetch(request); + if (response.status === 200 || response.status === 201) { + const json = await response.json(); + return json?.data?.email; + } + if (response.status === 401) { + throw "Invalid AnonAddy API token."; + } + throw "Unknown AnonAddy error occurred."; + } + + private async generateFirefoxRelayAlias(apiToken: string, website: string): Promise { + if (apiToken == null || apiToken === "") { + throw "Invalid Firefox Relay API token."; + } + const requestInit: RequestInit = { + cache: "no-store", + method: "POST", + headers: new Headers({ + Authorization: "Token " + apiToken, + "Content-Type": "application/json", + }), + }; + const url = "https://relay.firefox.com/api/v1/relayaddresses/"; + requestInit.body = JSON.stringify({ + enabled: true, + generated_for: website, + description: (website != null ? website + " - " : "") + "Generated by Bitwarden.", + }); + const request = new Request(url, requestInit); + const response = await this.apiService.nativeFetch(request); + if (response.status === 200 || response.status === 201) { + const json = await response.json(); + return json?.full_address; + } + if (response.status === 401) { + throw "Invalid Firefox Relay API token."; + } + throw "Unknown Firefox Relay error occurred."; + } }