diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index ac25f4cf293..5244fed76ef 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1965,6 +1965,10 @@ "apiKey": { "message": "API Key" }, + "accountId": { + "message": "Account ID", + "description": "ID is short for 'Identifier'" + }, "ssoKeyConnectorError": { "message": "Key Connector error: make sure Key Connector is available and working correctly." }, diff --git a/apps/browser/src/popup/generator/generator.component.html b/apps/browser/src/popup/generator/generator.component.html index 65ace55a2b8..5a9a067a4e1 100644 --- a/apps/browser/src/popup/generator/generator.component.html +++ b/apps/browser/src/popup/generator/generator.component.html @@ -395,6 +395,28 @@ /> + +
+ + +
+
+ + +
+
diff --git a/apps/desktop/src/app/vault/generator.component.html b/apps/desktop/src/app/vault/generator.component.html index e5ad65a56e0..79e66ae5366 100644 --- a/apps/desktop/src/app/vault/generator.component.html +++ b/apps/desktop/src/app/vault/generator.component.html @@ -428,6 +428,28 @@ />
+ +
+ + +
+
+ + +
+
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index e399281c061..04fd939c77c 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1982,6 +1982,10 @@ "premiumSubcriptionRequired": { "message": "Premium subscription required" }, + "accountId": { + "message": "Account ID", + "description": "ID is short for 'Identifier'" + }, "organizationIsDisabled": { "message": "Organization is disabled." }, diff --git a/apps/web/src/app/tools/generator.component.html b/apps/web/src/app/tools/generator.component.html index 90c806059f7..67d23e61463 100644 --- a/apps/web/src/app/tools/generator.component.html +++ b/apps/web/src/app/tools/generator.component.html @@ -343,6 +343,28 @@ />
+
+
+ + +
+
+ + +
+
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0e711752773..fa2a98f4e54 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5312,5 +5312,9 @@ }, "numberOfUsers": { "message": "Number of users" + }, + "accountId": { + "message": "Account ID", + "description": "ID is short for 'Identifier'" } } diff --git a/libs/angular/src/components/generator.component.ts b/libs/angular/src/components/generator.component.ts index 3d819b333b4..15905632e45 100644 --- a/libs/angular/src/components/generator.component.ts +++ b/libs/angular/src/components/generator.component.ts @@ -74,8 +74,8 @@ export class GeneratorComponent implements OnInit { { name: "SimpleLogin", value: "simplelogin" }, { name: "AnonAddy", value: "anonaddy" }, { name: "Firefox Relay", value: "firefoxrelay" }, + { name: "FastMail", value: "fastmail" }, { name: "DuckDuckGo", value: "duckduckgo" }, - // { name: "FastMail", value: "fastmail" }, ]; } diff --git a/libs/common/src/emailForwarders/anonAddyForwarder.ts b/libs/common/src/emailForwarders/anonAddyForwarder.ts new file mode 100644 index 00000000000..af4e3c9bce0 --- /dev/null +++ b/libs/common/src/emailForwarders/anonAddyForwarder.ts @@ -0,0 +1,41 @@ +import { ApiService } from "../abstractions/api.service"; + +import { Forwarder } from "./forwarder"; +import { ForwarderOptions } from "./forwarderOptions"; + +export class AnonAddyForwarder implements Forwarder { + async generate(apiService: ApiService, options: ForwarderOptions): Promise { + if (options.apiKey == null || options.apiKey === "") { + throw "Invalid AnonAddy API token."; + } + if (options.anonaddy?.domain == null || options.anonaddy.domain === "") { + throw "Invalid AnonAddy domain."; + } + const requestInit: RequestInit = { + redirect: "manual", + cache: "no-store", + method: "POST", + headers: new Headers({ + Authorization: "Bearer " + options.apiKey, + "Content-Type": "application/json", + }), + }; + const url = "https://app.anonaddy.com/api/v1/aliases"; + requestInit.body = JSON.stringify({ + domain: options.anonaddy.domain, + description: + (options.website != null ? "Website: " + options.website + ". " : "") + + "Generated by Bitwarden.", + }); + const request = new Request(url, requestInit); + const response = await 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."; + } +} diff --git a/libs/common/src/emailForwarders/duckDuckGoForwarder.ts b/libs/common/src/emailForwarders/duckDuckGoForwarder.ts new file mode 100644 index 00000000000..a4730733874 --- /dev/null +++ b/libs/common/src/emailForwarders/duckDuckGoForwarder.ts @@ -0,0 +1,33 @@ +import { ApiService } from "../abstractions/api.service"; + +import { Forwarder } from "./forwarder"; +import { ForwarderOptions } from "./forwarderOptions"; + +export class DuckDuckGoForwarder implements Forwarder { + async generate(apiService: ApiService, options: ForwarderOptions): Promise { + if (options.apiKey == null || options.apiKey === "") { + throw "Invalid DuckDuckGo API token."; + } + const requestInit: RequestInit = { + redirect: "manual", + cache: "no-store", + method: "POST", + headers: new Headers({ + Authorization: "Bearer " + options.apiKey, + "Content-Type": "application/json", + }), + }; + const url = "https://quack.duckduckgo.com/api/email/addresses"; + const request = new Request(url, requestInit); + const response = await apiService.nativeFetch(request); + if (response.status === 200 || response.status === 201) { + const json = await response.json(); + if (json.address) { + return `${json.address}@duck.com`; + } + } else if (response.status === 401) { + throw "Invalid DuckDuckGo API token."; + } + throw "Unknown DuckDuckGo error occurred."; + } +} diff --git a/libs/common/src/emailForwarders/fastmailForwarder.ts b/libs/common/src/emailForwarders/fastmailForwarder.ts new file mode 100644 index 00000000000..9e7dcbb7467 --- /dev/null +++ b/libs/common/src/emailForwarders/fastmailForwarder.ts @@ -0,0 +1,67 @@ +import { ApiService } from "../abstractions/api.service"; + +import { Forwarder } from "./forwarder"; +import { ForwarderOptions } from "./forwarderOptions"; + +export class FastmailForwarder implements Forwarder { + async generate(apiService: ApiService, options: ForwarderOptions): Promise { + if (options.apiKey == null || options.apiKey === "") { + throw "Invalid Fastmail API token."; + } + if (options?.fastmail.accountId == null || options.fastmail.accountId === "") { + throw "Invalid Fastmail account ID."; + } + const requestInit: RequestInit = { + redirect: "manual", + cache: "no-store", + method: "POST", + headers: new Headers({ + Authorization: "Bearer " + options.apiKey, + "Content-Type": "application/json", + }), + }; + const url = "https://api.fastmail.com/jmap/api/"; + requestInit.body = JSON.stringify({ + using: ["https://www.fastmail.com/dev/maskedemail", "urn:ietf:params:jmap:core"], + methodCalls: [ + [ + "MaskedEmail/set", + { + accountId: "u" + options.fastmail.accountId, + create: { + "new-masked-email": { + state: "enabled", + description: + (options.website != null ? options.website + " - " : "") + + "Generated by Bitwarden", + url: options.website, + emailPrefix: options.fastmail.prefix, + }, + }, + }, + "0", + ], + ], + }); + const request = new Request(url, requestInit); + const response = await apiService.nativeFetch(request); + if (response.status === 200) { + const json = await response.json(); + if ( + json.methodResponses != null && + json.methodResponses.length > 0 && + json.methodResponses[0].length > 0 + ) { + if (json.methodResponses[0][0] === "MaskedEmail/set") { + return json.methodResponses[0][1]?.created?.["new-masked-email"]?.email; + } else if (json.methodResponses[0][0] === "error") { + throw "Fastmail error: " + json.methodResponses[0][1]?.description; + } + } + } + if (response.status === 401) { + throw "Invalid Fastmail API token."; + } + throw "Unknown Fastmail error occurred."; + } +} diff --git a/libs/common/src/emailForwarders/firefoxRelayForwarder.ts b/libs/common/src/emailForwarders/firefoxRelayForwarder.ts new file mode 100644 index 00000000000..bdfaecc117c --- /dev/null +++ b/libs/common/src/emailForwarders/firefoxRelayForwarder.ts @@ -0,0 +1,38 @@ +import { ApiService } from "../abstractions/api.service"; + +import { Forwarder } from "./forwarder"; +import { ForwarderOptions } from "./forwarderOptions"; + +export class FirefoxRelayForwarder implements Forwarder { + async generate(apiService: ApiService, options: ForwarderOptions): Promise { + if (options.apiKey == null || options.apiKey === "") { + throw "Invalid Firefox Relay API token."; + } + const requestInit: RequestInit = { + redirect: "manual", + cache: "no-store", + method: "POST", + headers: new Headers({ + Authorization: "Token " + options.apiKey, + "Content-Type": "application/json", + }), + }; + const url = "https://relay.firefox.com/api/v1/relayaddresses/"; + requestInit.body = JSON.stringify({ + enabled: true, + generated_for: options.website, + description: + (options.website != null ? options.website + " - " : "") + "Generated by Bitwarden.", + }); + const request = new Request(url, requestInit); + const response = await 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."; + } +} diff --git a/libs/common/src/emailForwarders/forwarder.ts b/libs/common/src/emailForwarders/forwarder.ts new file mode 100644 index 00000000000..05b84c913af --- /dev/null +++ b/libs/common/src/emailForwarders/forwarder.ts @@ -0,0 +1,7 @@ +import { ApiService } from "../abstractions/api.service"; + +import { ForwarderOptions } from "./forwarderOptions"; + +export interface Forwarder { + generate(apiService: ApiService, options: ForwarderOptions): Promise; +} diff --git a/libs/common/src/emailForwarders/forwarderOptions.ts b/libs/common/src/emailForwarders/forwarderOptions.ts new file mode 100644 index 00000000000..7482f82e496 --- /dev/null +++ b/libs/common/src/emailForwarders/forwarderOptions.ts @@ -0,0 +1,15 @@ +export class ForwarderOptions { + apiKey: string; + website: string; + fastmail = new FastmailForwarderOptions(); + anonaddy = new AnonAddyForwarderOptions(); +} + +export class FastmailForwarderOptions { + accountId: string; + prefix: string; +} + +export class AnonAddyForwarderOptions { + domain: string; +} diff --git a/libs/common/src/emailForwarders/simpleLoginForwarder.ts b/libs/common/src/emailForwarders/simpleLoginForwarder.ts new file mode 100644 index 00000000000..adc8a13aefc --- /dev/null +++ b/libs/common/src/emailForwarders/simpleLoginForwarder.ts @@ -0,0 +1,48 @@ +import { ApiService } from "../abstractions/api.service"; + +import { Forwarder } from "./forwarder"; +import { ForwarderOptions } from "./forwarderOptions"; + +export class SimpleLoginForwarder implements Forwarder { + async generate(apiService: ApiService, options: ForwarderOptions): Promise { + if (options.apiKey == null || options.apiKey === "") { + throw "Invalid SimpleLogin API key."; + } + const requestInit: RequestInit = { + redirect: "manual", + cache: "no-store", + method: "POST", + headers: new Headers({ + Authentication: options.apiKey, + "Content-Type": "application/json", + }), + }; + let url = "https://app.simplelogin.io/api/alias/random/new"; + if (options.website != null) { + url += "?hostname=" + options.website; + } + requestInit.body = JSON.stringify({ + note: + (options.website != null ? "Website: " + options.website + ". " : "") + + "Generated by Bitwarden.", + }); + const request = new Request(url, requestInit); + const response = await 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."; + } +} diff --git a/libs/common/src/services/usernameGeneration.service.ts b/libs/common/src/services/usernameGeneration.service.ts index 217505f25bc..3ad2c513403 100644 --- a/libs/common/src/services/usernameGeneration.service.ts +++ b/libs/common/src/services/usernameGeneration.service.ts @@ -2,6 +2,13 @@ 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"; +import { AnonAddyForwarder } from "../emailForwarders/anonAddyForwarder"; +import { DuckDuckGoForwarder } from "../emailForwarders/duckDuckGoForwarder"; +import { FastmailForwarder } from "../emailForwarders/fastmailForwarder"; +import { FirefoxRelayForwarder } from "../emailForwarders/firefoxRelayForwarder"; +import { Forwarder } from "../emailForwarders/forwarder"; +import { ForwarderOptions } from "../emailForwarders/forwarderOptions"; +import { SimpleLoginForwarder } from "../emailForwarders/simpleLoginForwarder"; import { EEFLongWordList } from "../misc/wordlist"; const DefaultOptions = { @@ -108,38 +115,33 @@ export class UsernameGenerationService implements BaseUsernameGenerationService return null; } + let forwarder: Forwarder = null; + const forwarderOptions = new ForwarderOptions(); + forwarderOptions.website = o.website; if (o.forwardedService === "simplelogin") { - if (o.forwardedSimpleLoginApiKey == null || o.forwardedSimpleLoginApiKey === "") { - return null; - } - return this.generateSimpleLoginAlias(o.forwardedSimpleLoginApiKey, o.website); + forwarder = new SimpleLoginForwarder(); + forwarderOptions.apiKey = o.forwardedSimpleLoginApiKey; } 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 - ); + forwarder = new AnonAddyForwarder(); + forwarderOptions.apiKey = o.forwardedAnonAddyApiToken; + forwarderOptions.anonaddy.domain = o.forwardedAnonAddyDomain; } else if (o.forwardedService === "firefoxrelay") { - if (o.forwardedFirefoxApiToken == null || o.forwardedFirefoxApiToken === "") { - return null; - } - return this.generateFirefoxRelayAlias(o.forwardedFirefoxApiToken, o.website); + forwarder = new FirefoxRelayForwarder(); + forwarderOptions.apiKey = o.forwardedFirefoxApiToken; + } else if (o.forwardedService === "fastmail") { + forwarder = new FastmailForwarder(); + forwarderOptions.apiKey = o.forwardedFastmailApiToken; + forwarderOptions.fastmail.accountId = o.forwardedFastmailAccountId; } else if (o.forwardedService === "duckduckgo") { - if (o.forwardedDuckDuckGoToken == null || o.forwardedDuckDuckGoToken === "") { - return null; - } - return this.generateDuckDuckGoAlias(o.forwardedDuckDuckGoToken); + forwarder = new DuckDuckGoForwarder(); + forwarderOptions.apiKey = o.forwardedDuckDuckGoToken; } - return null; + if (forwarder == null) { + return null; + } + + return forwarder.generate(this.apiService, forwarderOptions); } async getOptions(): Promise { @@ -173,137 +175,4 @@ export class UsernameGenerationService implements BaseUsernameGenerationService ? number : new Array(width - number.length + 1).join("0") + number; } - - private async generateSimpleLoginAlias(apiKey: string, website: string): Promise { - if (apiKey == null || apiKey === "") { - throw "Invalid SimpleLogin API key."; - } - const requestInit: RequestInit = { - redirect: "manual", - 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 (website != null) { - url += "?hostname=" + website; - } - requestInit.body = JSON.stringify({ - note: (website != null ? "Website: " + 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.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 = { - redirect: "manual", - 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 = { - redirect: "manual", - 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."; - } - - private async generateDuckDuckGoAlias(apiToken: string): Promise { - if (apiToken == null || apiToken === "") { - throw "Invalid DuckDuckGo API token."; - } - const requestInit: RequestInit = { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Bearer " + apiToken, - "Content-Type": "application/json", - }), - }; - const url = "https://quack.duckduckgo.com/api/email/addresses"; - 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(); - if (json.address) { - return `${json.address}@duck.com`; - } - } else if (response.status === 401) { - throw "Invalid DuckDuckGo API token."; - } - throw "Unknown DuckDuckGo error occurred."; - } }