mirror of
https://github.com/bitwarden/browser.git
synced 2024-10-30 08:10:34 +01:00
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:
parent
e40e7de808
commit
fe65a337c8
@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
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 { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { StateService } from "jslib-common/abstractions/state.service";
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
@ -15,6 +16,7 @@ export class GeneratorComponent implements OnInit {
|
|||||||
@Input() type: string;
|
@Input() type: string;
|
||||||
@Output() onSelected = new EventEmitter<string>();
|
@Output() onSelected = new EventEmitter<string>();
|
||||||
|
|
||||||
|
usernameGeneratingPromise: Promise<string>;
|
||||||
typeOptions: any[];
|
typeOptions: any[];
|
||||||
passTypeOptions: any[];
|
passTypeOptions: any[];
|
||||||
usernameTypeOptions: any[];
|
usernameTypeOptions: any[];
|
||||||
@ -36,6 +38,7 @@ export class GeneratorComponent implements OnInit {
|
|||||||
protected platformUtilsService: PlatformUtilsService,
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
|
protected logService: LogService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
private win: Window
|
private win: Window
|
||||||
) {
|
) {
|
||||||
@ -58,13 +61,20 @@ export class GeneratorComponent implements OnInit {
|
|||||||
value: "catchall",
|
value: "catchall",
|
||||||
desc: i18nService.t("catchallEmailDesc"),
|
desc: i18nService.t("catchallEmailDesc"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: i18nService.t("forwardedEmail"),
|
||||||
|
value: "forwarded",
|
||||||
|
desc: i18nService.t("forwardedEmailDesc"),
|
||||||
|
},
|
||||||
{ name: i18nService.t("randomWord"), value: "word" },
|
{ name: i18nService.t("randomWord"), value: "word" },
|
||||||
];
|
];
|
||||||
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||||
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
|
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||||
this.forwardOptions = [
|
this.forwardOptions = [
|
||||||
{ name: "SimpleLogin", value: "simplelogin" },
|
{ 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";
|
this.type = generatorOptions?.type ?? "password";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.regenerate();
|
if (this.regenerateWithoutButtonPress()) {
|
||||||
|
await this.regenerate();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async typeChanged() {
|
async typeChanged() {
|
||||||
await this.stateService.setGeneratorOptions({ type: this.type });
|
await this.stateService.setGeneratorOptions({ type: this.type });
|
||||||
await this.regenerate();
|
if (this.regenerateWithoutButtonPress()) {
|
||||||
|
await this.regenerate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async regenerate() {
|
async regenerate() {
|
||||||
@ -135,14 +149,17 @@ export class GeneratorComponent implements OnInit {
|
|||||||
this.normalizePasswordOptions();
|
this.normalizePasswordOptions();
|
||||||
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
||||||
|
|
||||||
if (regenerate) {
|
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||||
await this.regeneratePassword();
|
await this.regeneratePassword();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveUsernameOptions(regenerate = true) {
|
async saveUsernameOptions(regenerate = true) {
|
||||||
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
||||||
if (regenerate) {
|
if (this.usernameOptions.type === "forwarded") {
|
||||||
|
this.username = "-";
|
||||||
|
}
|
||||||
|
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||||
await this.regenerateUsername();
|
await this.regenerateUsername();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,9 +174,16 @@ export class GeneratorComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateUsername() {
|
async generateUsername() {
|
||||||
this.username = await this.usernameGenerationService.generateUsername(this.usernameOptions);
|
try {
|
||||||
if (this.username === "" || this.username === null) {
|
this.usernameGeneratingPromise = this.usernameGenerationService.generateUsername(
|
||||||
this.username = "-";
|
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;
|
this.showOptions = !this.showOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
regenerateWithoutButtonPress() {
|
||||||
|
return this.type !== "username" || this.usernameOptions.type !== "forwarded";
|
||||||
|
}
|
||||||
|
|
||||||
private normalizePasswordOptions() {
|
private normalizePasswordOptions() {
|
||||||
// Application level normalize options depedent on class variables
|
// Application level normalize options depedent on class variables
|
||||||
this.passwordOptions.ambiguous = !this.avoidAmbiguous;
|
this.passwordOptions.ambiguous = !this.avoidAmbiguous;
|
||||||
|
@ -232,7 +232,7 @@ export const CLIENT_TYPE = new InjectionToken<boolean>("CLIENT_TYPE");
|
|||||||
{
|
{
|
||||||
provide: UsernameGenerationServiceAbstraction,
|
provide: UsernameGenerationServiceAbstraction,
|
||||||
useClass: UsernameGenerationService,
|
useClass: UsernameGenerationService,
|
||||||
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
|
deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ApiServiceAbstraction,
|
provide: ApiServiceAbstraction,
|
||||||
|
@ -3,6 +3,7 @@ export abstract class UsernameGenerationService {
|
|||||||
generateWord: (options: any) => Promise<string>;
|
generateWord: (options: any) => Promise<string>;
|
||||||
generateSubaddress: (options: any) => Promise<string>;
|
generateSubaddress: (options: any) => Promise<string>;
|
||||||
generateCatchall: (options: any) => Promise<string>;
|
generateCatchall: (options: any) => Promise<string>;
|
||||||
|
generateForwarded: (options: any) => Promise<string>;
|
||||||
getOptions: () => Promise<any>;
|
getOptions: () => Promise<any>;
|
||||||
saveOptions: (options: any) => Promise<void>;
|
saveOptions: (options: any) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ApiService } from "../abstractions/api.service";
|
||||||
import { CryptoService } from "../abstractions/crypto.service";
|
import { CryptoService } from "../abstractions/crypto.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { UsernameGenerationService as BaseUsernameGenerationService } from "../abstractions/usernameGeneration.service";
|
import { UsernameGenerationService as BaseUsernameGenerationService } from "../abstractions/usernameGeneration.service";
|
||||||
@ -9,10 +10,16 @@ const DefaultOptions = {
|
|||||||
wordIncludeNumber: true,
|
wordIncludeNumber: true,
|
||||||
subaddressType: "random",
|
subaddressType: "random",
|
||||||
catchallType: "random",
|
catchallType: "random",
|
||||||
|
forwardedType: "simplelogin",
|
||||||
|
forwardedAnonAddyDomain: "anonaddy.me",
|
||||||
};
|
};
|
||||||
|
|
||||||
export class UsernameGenerationService implements BaseUsernameGenerationService {
|
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> {
|
generateUsername(options: any): Promise<string> {
|
||||||
if (options.type === "catchall") {
|
if (options.type === "catchall") {
|
||||||
@ -20,7 +27,7 @@ export class UsernameGenerationService implements BaseUsernameGenerationService
|
|||||||
} else if (options.type === "subaddress") {
|
} else if (options.type === "subaddress") {
|
||||||
return this.generateSubaddress(options);
|
return this.generateSubaddress(options);
|
||||||
} else if (options.type === "forwarded") {
|
} else if (options.type === "forwarded") {
|
||||||
return this.generateSubaddress(options);
|
return this.generateForwarded(options);
|
||||||
} else {
|
} else {
|
||||||
return this.generateWord(options);
|
return this.generateWord(options);
|
||||||
}
|
}
|
||||||
@ -94,6 +101,46 @@ export class UsernameGenerationService implements BaseUsernameGenerationService
|
|||||||
return startString + "@" + o.catchallDomain;
|
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> {
|
async getOptions(): Promise<any> {
|
||||||
let options = await this.stateService.getUsernameGenerationOptions();
|
let options = await this.stateService.getUsernameGenerationOptions();
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
@ -125,4 +172,112 @@ export class UsernameGenerationService implements BaseUsernameGenerationService
|
|||||||
? number
|
? number
|
||||||
: new Array(width - number.length + 1).join("0") + 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.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user