Forwarded email alias generation (#772)

* generate forwarded alias with SL and AD

* added forwarded email to type list

* add ApiService dep

* ApiServiceAbstraction

* use proper status codes

* only generate on button press

* reset username to `-`

* reset username when forwarded

* Authorization header for anonaddy

* use proper anonaddy json path

* firefox relay support

* update description for firefox

* log username generation errors
This commit is contained in:
Kyle Spearrin 2022-04-27 10:08:46 -04:00 committed by GitHub
parent e40e7de808
commit fe65a337c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 195 additions and 11 deletions

View File

@ -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<string>();
usernameGeneratingPromise: Promise<string>;
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;

View File

@ -232,7 +232,7 @@ export const CLIENT_TYPE = new InjectionToken<boolean>("CLIENT_TYPE");
{
provide: UsernameGenerationServiceAbstraction,
useClass: UsernameGenerationService,
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction],
},
{
provide: ApiServiceAbstraction,

View File

@ -3,6 +3,7 @@ export abstract class UsernameGenerationService {
generateWord: (options: any) => Promise<string>;
generateSubaddress: (options: any) => Promise<string>;
generateCatchall: (options: any) => Promise<string>;
generateForwarded: (options: any) => Promise<string>;
getOptions: () => Promise<any>;
saveOptions: (options: any) => Promise<void>;
}

View File

@ -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<string> {
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<string> {
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<any> {
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<string> {
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<string> {
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<string> {
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.";
}
}