mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-328] Move generator to tools (#4980)
* Move generator to tools libs/angular: - Move generator.component to tools libs/common: - Move password generation to tools - Move username generation including email-forwarders to tools apps/* - create tools-subfolder and move files regarding generator functionality - Update all the imports .github/: - Cleaned up whitelist-capital-letters.txt - Added team-tools-dev folders to CODEOWNERS * Remove unused barrel file
This commit is contained in:
committed by
GitHub
parent
e9d0f75b8a
commit
d4c812160f
@@ -0,0 +1,9 @@
|
||||
export class GeneratedPasswordHistory {
|
||||
password: string;
|
||||
date: number;
|
||||
|
||||
constructor(password: string, date: number) {
|
||||
this.password = password;
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
3
libs/common/src/tools/generator/password/index.ts
Normal file
3
libs/common/src/tools/generator/password/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||
export { PasswordGenerationService } from "./password-generation.service";
|
||||
export { GeneratedPasswordHistory } from "./generated-password-history";
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as zxcvbn from "zxcvbn";
|
||||
|
||||
import { PasswordGeneratorPolicyOptions } from "../../../models/domain/password-generator-policy-options";
|
||||
|
||||
import { GeneratedPasswordHistory } from "./generated-password-history";
|
||||
import { PasswordGeneratorOptions } from "./password-generator-options";
|
||||
|
||||
export abstract class PasswordGenerationServiceAbstraction {
|
||||
generatePassword: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||
generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||
getOptions: () => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||
enforcePasswordGeneratorPoliciesOnOptions: (
|
||||
options: PasswordGeneratorOptions
|
||||
) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
|
||||
saveOptions: (options: PasswordGeneratorOptions) => Promise<void>;
|
||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
||||
addHistory: (password: string) => Promise<void>;
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult;
|
||||
normalizeOptions: (
|
||||
options: PasswordGeneratorOptions,
|
||||
enforcedPolicyOptions: PasswordGeneratorPolicyOptions
|
||||
) => void;
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
import * as zxcvbn from "zxcvbn";
|
||||
|
||||
import { CryptoService } from "../../../abstractions/crypto.service";
|
||||
import { PolicyService } from "../../../abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "../../../abstractions/state.service";
|
||||
import { PolicyType } from "../../../enums/policyType";
|
||||
import { EFFLongWordList } from "../../../misc/wordlist";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { PasswordGeneratorPolicyOptions } from "../../../models/domain/password-generator-policy-options";
|
||||
|
||||
import { GeneratedPasswordHistory } from "./generated-password-history";
|
||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||
import { PasswordGeneratorOptions } from "./password-generator-options";
|
||||
|
||||
const DefaultOptions: PasswordGeneratorOptions = {
|
||||
length: 14,
|
||||
ambiguous: false,
|
||||
number: true,
|
||||
minNumber: 1,
|
||||
uppercase: true,
|
||||
minUppercase: 0,
|
||||
lowercase: true,
|
||||
minLowercase: 0,
|
||||
special: false,
|
||||
minSpecial: 1,
|
||||
type: "password",
|
||||
numWords: 3,
|
||||
wordSeparator: "-",
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
};
|
||||
|
||||
const MaxPasswordsInHistory = 100;
|
||||
|
||||
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private policyService: PolicyService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async generatePassword(options: PasswordGeneratorOptions): Promise<string> {
|
||||
// overload defaults with given options
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
if (o.type === "passphrase") {
|
||||
return this.generatePassphrase(options);
|
||||
}
|
||||
|
||||
// sanitize
|
||||
this.sanitizePasswordLength(o, true);
|
||||
|
||||
const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial;
|
||||
if (o.length < minLength) {
|
||||
o.length = minLength;
|
||||
}
|
||||
|
||||
const positions: string[] = [];
|
||||
if (o.lowercase && o.minLowercase > 0) {
|
||||
for (let i = 0; i < o.minLowercase; i++) {
|
||||
positions.push("l");
|
||||
}
|
||||
}
|
||||
if (o.uppercase && o.minUppercase > 0) {
|
||||
for (let i = 0; i < o.minUppercase; i++) {
|
||||
positions.push("u");
|
||||
}
|
||||
}
|
||||
if (o.number && o.minNumber > 0) {
|
||||
for (let i = 0; i < o.minNumber; i++) {
|
||||
positions.push("n");
|
||||
}
|
||||
}
|
||||
if (o.special && o.minSpecial > 0) {
|
||||
for (let i = 0; i < o.minSpecial; i++) {
|
||||
positions.push("s");
|
||||
}
|
||||
}
|
||||
while (positions.length < o.length) {
|
||||
positions.push("a");
|
||||
}
|
||||
|
||||
// shuffle
|
||||
await this.shuffleArray(positions);
|
||||
|
||||
// build out the char sets
|
||||
let allCharSet = "";
|
||||
|
||||
let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz";
|
||||
if (o.ambiguous) {
|
||||
lowercaseCharSet += "l";
|
||||
}
|
||||
if (o.lowercase) {
|
||||
allCharSet += lowercaseCharSet;
|
||||
}
|
||||
|
||||
let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
||||
if (o.ambiguous) {
|
||||
uppercaseCharSet += "IO";
|
||||
}
|
||||
if (o.uppercase) {
|
||||
allCharSet += uppercaseCharSet;
|
||||
}
|
||||
|
||||
let numberCharSet = "23456789";
|
||||
if (o.ambiguous) {
|
||||
numberCharSet += "01";
|
||||
}
|
||||
if (o.number) {
|
||||
allCharSet += numberCharSet;
|
||||
}
|
||||
|
||||
const specialCharSet = "!@#$%^&*";
|
||||
if (o.special) {
|
||||
allCharSet += specialCharSet;
|
||||
}
|
||||
|
||||
let password = "";
|
||||
for (let i = 0; i < o.length; i++) {
|
||||
let positionChars: string;
|
||||
switch (positions[i]) {
|
||||
case "l":
|
||||
positionChars = lowercaseCharSet;
|
||||
break;
|
||||
case "u":
|
||||
positionChars = uppercaseCharSet;
|
||||
break;
|
||||
case "n":
|
||||
positionChars = numberCharSet;
|
||||
break;
|
||||
case "s":
|
||||
positionChars = specialCharSet;
|
||||
break;
|
||||
case "a":
|
||||
positionChars = allCharSet;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1);
|
||||
password += positionChars.charAt(randomCharIndex);
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
async generatePassphrase(options: PasswordGeneratorOptions): Promise<string> {
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
if (o.numWords == null || o.numWords <= 2) {
|
||||
o.numWords = DefaultOptions.numWords;
|
||||
}
|
||||
if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) {
|
||||
o.wordSeparator = " ";
|
||||
}
|
||||
if (o.capitalize == null) {
|
||||
o.capitalize = false;
|
||||
}
|
||||
if (o.includeNumber == null) {
|
||||
o.includeNumber = false;
|
||||
}
|
||||
|
||||
const listLength = EFFLongWordList.length - 1;
|
||||
const wordList = new Array(o.numWords);
|
||||
for (let i = 0; i < o.numWords; i++) {
|
||||
const wordIndex = await this.cryptoService.randomNumber(0, listLength);
|
||||
if (o.capitalize) {
|
||||
wordList[i] = this.capitalize(EFFLongWordList[wordIndex]);
|
||||
} else {
|
||||
wordList[i] = EFFLongWordList[wordIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (o.includeNumber) {
|
||||
await this.appendRandomNumberToRandomWord(wordList);
|
||||
}
|
||||
return wordList.join(o.wordSeparator);
|
||||
}
|
||||
|
||||
async getOptions(): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> {
|
||||
let options = await this.stateService.getPasswordGenerationOptions();
|
||||
if (options == null) {
|
||||
options = Object.assign({}, DefaultOptions);
|
||||
} else {
|
||||
options = Object.assign({}, DefaultOptions, options);
|
||||
}
|
||||
await this.stateService.setPasswordGenerationOptions(options);
|
||||
const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(options);
|
||||
options = enforcedOptions[0];
|
||||
return [options, enforcedOptions[1]];
|
||||
}
|
||||
|
||||
async enforcePasswordGeneratorPoliciesOnOptions(
|
||||
options: PasswordGeneratorOptions
|
||||
): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> {
|
||||
let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions();
|
||||
if (enforcedPolicyOptions != null) {
|
||||
if (options.length < enforcedPolicyOptions.minLength) {
|
||||
options.length = enforcedPolicyOptions.minLength;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useUppercase) {
|
||||
options.uppercase = true;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useLowercase) {
|
||||
options.lowercase = true;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useNumbers) {
|
||||
options.number = true;
|
||||
}
|
||||
|
||||
if (options.minNumber < enforcedPolicyOptions.numberCount) {
|
||||
options.minNumber = enforcedPolicyOptions.numberCount;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useSpecial) {
|
||||
options.special = true;
|
||||
}
|
||||
|
||||
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
|
||||
options.minSpecial = enforcedPolicyOptions.specialCount;
|
||||
}
|
||||
|
||||
// Must normalize these fields because the receiving call expects all options to pass the current rules
|
||||
if (options.minSpecial + options.minNumber > options.length) {
|
||||
options.minSpecial = options.length - options.minNumber;
|
||||
}
|
||||
|
||||
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
|
||||
options.numWords = enforcedPolicyOptions.minNumberWords;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.capitalize) {
|
||||
options.capitalize = true;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.includeNumber) {
|
||||
options.includeNumber = true;
|
||||
}
|
||||
|
||||
// Force default type if password/passphrase selected via policy
|
||||
if (
|
||||
enforcedPolicyOptions.defaultType === "password" ||
|
||||
enforcedPolicyOptions.defaultType === "passphrase"
|
||||
) {
|
||||
options.type = enforcedPolicyOptions.defaultType;
|
||||
}
|
||||
} else {
|
||||
// UI layer expects an instantiated object to prevent more explicit null checks
|
||||
enforcedPolicyOptions = new PasswordGeneratorPolicyOptions();
|
||||
}
|
||||
return [options, enforcedPolicyOptions];
|
||||
}
|
||||
|
||||
async getPasswordGeneratorPolicyOptions(): Promise<PasswordGeneratorPolicyOptions> {
|
||||
const policies = await this.policyService?.getAll(PolicyType.PasswordGenerator);
|
||||
let enforcedOptions: PasswordGeneratorPolicyOptions = null;
|
||||
|
||||
if (policies == null || policies.length === 0) {
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
policies.forEach((currentPolicy) => {
|
||||
if (!currentPolicy.enabled || currentPolicy.data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enforcedOptions == null) {
|
||||
enforcedOptions = new PasswordGeneratorPolicyOptions();
|
||||
}
|
||||
|
||||
// Password wins in multi-org collisions
|
||||
if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== "password") {
|
||||
enforcedOptions.defaultType = currentPolicy.data.defaultType;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minLength != null &&
|
||||
currentPolicy.data.minLength > enforcedOptions.minLength
|
||||
) {
|
||||
enforcedOptions.minLength = currentPolicy.data.minLength;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useUpper) {
|
||||
enforcedOptions.useUppercase = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useLower) {
|
||||
enforcedOptions.useLowercase = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useNumbers) {
|
||||
enforcedOptions.useNumbers = true;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minNumbers != null &&
|
||||
currentPolicy.data.minNumbers > enforcedOptions.numberCount
|
||||
) {
|
||||
enforcedOptions.numberCount = currentPolicy.data.minNumbers;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useSpecial) {
|
||||
enforcedOptions.useSpecial = true;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minSpecial != null &&
|
||||
currentPolicy.data.minSpecial > enforcedOptions.specialCount
|
||||
) {
|
||||
enforcedOptions.specialCount = currentPolicy.data.minSpecial;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minNumberWords != null &&
|
||||
currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords
|
||||
) {
|
||||
enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.capitalize) {
|
||||
enforcedOptions.capitalize = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.includeNumber) {
|
||||
enforcedOptions.includeNumber = true;
|
||||
}
|
||||
});
|
||||
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
async saveOptions(options: PasswordGeneratorOptions) {
|
||||
await this.stateService.setPasswordGenerationOptions(options);
|
||||
}
|
||||
|
||||
async getHistory(): Promise<GeneratedPasswordHistory[]> {
|
||||
const hasKey = await this.cryptoService.hasKey();
|
||||
if (!hasKey) {
|
||||
return new Array<GeneratedPasswordHistory>();
|
||||
}
|
||||
|
||||
if ((await this.stateService.getDecryptedPasswordGenerationHistory()) == null) {
|
||||
const encrypted = await this.stateService.getEncryptedPasswordGenerationHistory();
|
||||
const decrypted = await this.decryptHistory(encrypted);
|
||||
await this.stateService.setDecryptedPasswordGenerationHistory(decrypted);
|
||||
}
|
||||
|
||||
const passwordGenerationHistory =
|
||||
await this.stateService.getDecryptedPasswordGenerationHistory();
|
||||
return passwordGenerationHistory != null
|
||||
? passwordGenerationHistory
|
||||
: new Array<GeneratedPasswordHistory>();
|
||||
}
|
||||
|
||||
async addHistory(password: string): Promise<void> {
|
||||
// Cannot add new history if no key is available
|
||||
const hasKey = await this.cryptoService.hasKey();
|
||||
if (!hasKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHistory = await this.getHistory();
|
||||
|
||||
// Prevent duplicates
|
||||
if (this.matchesPrevious(password, currentHistory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now()));
|
||||
|
||||
// Remove old items.
|
||||
if (currentHistory.length > MaxPasswordsInHistory) {
|
||||
currentHistory.pop();
|
||||
}
|
||||
|
||||
const newHistory = await this.encryptHistory(currentHistory);
|
||||
await this.stateService.setDecryptedPasswordGenerationHistory(currentHistory);
|
||||
return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<void> {
|
||||
await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId });
|
||||
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
||||
}
|
||||
|
||||
passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult {
|
||||
if (password == null || password.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let globalUserInputs = ["bitwarden", "bit", "warden"];
|
||||
if (userInputs != null && userInputs.length > 0) {
|
||||
globalUserInputs = globalUserInputs.concat(userInputs);
|
||||
}
|
||||
// Use a hash set to get rid of any duplicate user inputs
|
||||
const finalUserInputs = Array.from(new Set(globalUserInputs));
|
||||
const result = zxcvbn(password, finalUserInputs);
|
||||
return result;
|
||||
}
|
||||
|
||||
normalizeOptions(
|
||||
options: PasswordGeneratorOptions,
|
||||
enforcedPolicyOptions: PasswordGeneratorPolicyOptions
|
||||
) {
|
||||
options.minLowercase = 0;
|
||||
options.minUppercase = 0;
|
||||
|
||||
if (!options.length || options.length < 5) {
|
||||
options.length = 5;
|
||||
} else if (options.length > 128) {
|
||||
options.length = 128;
|
||||
}
|
||||
|
||||
if (options.length < enforcedPolicyOptions.minLength) {
|
||||
options.length = enforcedPolicyOptions.minLength;
|
||||
}
|
||||
|
||||
if (!options.minNumber) {
|
||||
options.minNumber = 0;
|
||||
} else if (options.minNumber > options.length) {
|
||||
options.minNumber = options.length;
|
||||
} else if (options.minNumber > 9) {
|
||||
options.minNumber = 9;
|
||||
}
|
||||
|
||||
if (options.minNumber < enforcedPolicyOptions.numberCount) {
|
||||
options.minNumber = enforcedPolicyOptions.numberCount;
|
||||
}
|
||||
|
||||
if (!options.minSpecial) {
|
||||
options.minSpecial = 0;
|
||||
} else if (options.minSpecial > options.length) {
|
||||
options.minSpecial = options.length;
|
||||
} else if (options.minSpecial > 9) {
|
||||
options.minSpecial = 9;
|
||||
}
|
||||
|
||||
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
|
||||
options.minSpecial = enforcedPolicyOptions.specialCount;
|
||||
}
|
||||
|
||||
if (options.minSpecial + options.minNumber > options.length) {
|
||||
options.minSpecial = options.length - options.minNumber;
|
||||
}
|
||||
|
||||
if (options.numWords == null || options.length < 3) {
|
||||
options.numWords = 3;
|
||||
} else if (options.numWords > 20) {
|
||||
options.numWords = 20;
|
||||
}
|
||||
|
||||
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
|
||||
options.numWords = enforcedPolicyOptions.minNumberWords;
|
||||
}
|
||||
|
||||
if (options.wordSeparator != null && options.wordSeparator.length > 1) {
|
||||
options.wordSeparator = options.wordSeparator[0];
|
||||
}
|
||||
|
||||
this.sanitizePasswordLength(options, false);
|
||||
}
|
||||
|
||||
private capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
private async appendRandomNumberToRandomWord(wordList: string[]) {
|
||||
if (wordList == null || wordList.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const index = await this.cryptoService.randomNumber(0, wordList.length - 1);
|
||||
const num = await this.cryptoService.randomNumber(0, 9);
|
||||
wordList[index] = wordList[index] + num;
|
||||
}
|
||||
|
||||
private async encryptHistory(
|
||||
history: GeneratedPasswordHistory[]
|
||||
): Promise<GeneratedPasswordHistory[]> {
|
||||
if (history == null || history.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const promises = history.map(async (item) => {
|
||||
const encrypted = await this.cryptoService.encrypt(item.password);
|
||||
return new GeneratedPasswordHistory(encrypted.encryptedString, item.date);
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async decryptHistory(
|
||||
history: GeneratedPasswordHistory[]
|
||||
): Promise<GeneratedPasswordHistory[]> {
|
||||
if (history == null || history.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const promises = history.map(async (item) => {
|
||||
const decrypted = await this.cryptoService.decryptToUtf8(new EncString(item.password));
|
||||
return new GeneratedPasswordHistory(decrypted, item.date);
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean {
|
||||
if (history == null || history.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return history[history.length - 1].password === password;
|
||||
}
|
||||
|
||||
// ref: https://stackoverflow.com/a/12646864/1090359
|
||||
private async shuffleArray(array: string[]) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = await this.cryptoService.randomNumber(0, i);
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizePasswordLength(options: any, forGeneration: boolean) {
|
||||
let minUppercaseCalc = 0;
|
||||
let minLowercaseCalc = 0;
|
||||
let minNumberCalc: number = options.minNumber;
|
||||
let minSpecialCalc: number = options.minSpecial;
|
||||
|
||||
if (options.uppercase && options.minUppercase <= 0) {
|
||||
minUppercaseCalc = 1;
|
||||
} else if (!options.uppercase) {
|
||||
minUppercaseCalc = 0;
|
||||
}
|
||||
|
||||
if (options.lowercase && options.minLowercase <= 0) {
|
||||
minLowercaseCalc = 1;
|
||||
} else if (!options.lowercase) {
|
||||
minLowercaseCalc = 0;
|
||||
}
|
||||
|
||||
if (options.number && options.minNumber <= 0) {
|
||||
minNumberCalc = 1;
|
||||
} else if (!options.number) {
|
||||
minNumberCalc = 0;
|
||||
}
|
||||
|
||||
if (options.special && options.minSpecial <= 0) {
|
||||
minSpecialCalc = 1;
|
||||
} else if (!options.special) {
|
||||
minSpecialCalc = 0;
|
||||
}
|
||||
|
||||
// This should never happen but is a final safety net
|
||||
if (!options.length || options.length < 1) {
|
||||
options.length = 10;
|
||||
}
|
||||
|
||||
const minLength: number = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc;
|
||||
// Normalize and Generation both require this modification
|
||||
if (options.length < minLength) {
|
||||
options.length = minLength;
|
||||
}
|
||||
|
||||
// Apply other changes if the options object passed in is for generation
|
||||
if (forGeneration) {
|
||||
options.minUppercase = minUppercaseCalc;
|
||||
options.minLowercase = minLowercaseCalc;
|
||||
options.minNumber = minNumberCalc;
|
||||
options.minSpecial = minSpecialCalc;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export type PasswordGeneratorOptions = {
|
||||
length?: number;
|
||||
ambiguous?: boolean;
|
||||
uppercase?: boolean;
|
||||
minUppercase?: number;
|
||||
lowercase?: boolean;
|
||||
minLowercase?: number;
|
||||
number?: boolean;
|
||||
minNumber?: number;
|
||||
special?: boolean;
|
||||
minSpecial?: number;
|
||||
numWords?: number;
|
||||
wordSeparator?: string;
|
||||
capitalize?: boolean;
|
||||
includeNumber?: boolean;
|
||||
type?: "password" | "passphrase";
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class AnonAddyForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
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.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class DuckDuckGoForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
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.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class FastmailForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
if (options.apiKey == null || options.apiKey === "") {
|
||||
throw "Invalid Fastmail API token.";
|
||||
}
|
||||
|
||||
const accountId = await this.getAccountId(apiService, options);
|
||||
if (accountId == null || accountId === "") {
|
||||
throw "Unable to obtain Fastmail masked email 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: accountId,
|
||||
create: {
|
||||
"new-masked-email": {
|
||||
state: "enabled",
|
||||
description: "",
|
||||
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") {
|
||||
if (json.methodResponses[0][1]?.created?.["new-masked-email"] != null) {
|
||||
return json.methodResponses[0][1]?.created?.["new-masked-email"]?.email;
|
||||
}
|
||||
if (json.methodResponses[0][1]?.notCreated?.["new-masked-email"] != null) {
|
||||
throw (
|
||||
"Fastmail error: " +
|
||||
json.methodResponses[0][1]?.notCreated?.["new-masked-email"]?.description
|
||||
);
|
||||
}
|
||||
} else if (json.methodResponses[0][0] === "error") {
|
||||
throw "Fastmail error: " + json.methodResponses[0][1]?.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw "Invalid Fastmail API token.";
|
||||
}
|
||||
throw "Unknown Fastmail error occurred.";
|
||||
}
|
||||
|
||||
private async getAccountId(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
const requestInit: RequestInit = {
|
||||
cache: "no-store",
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
Authorization: "Bearer " + options.apiKey,
|
||||
}),
|
||||
};
|
||||
const url = "https://api.fastmail.com/.well-known/jmap";
|
||||
const request = new Request(url, requestInit);
|
||||
const response = await apiService.nativeFetch(request);
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
if (json.primaryAccounts != null) {
|
||||
return json.primaryAccounts["https://www.fastmail.com/dev/maskedemail"];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class FirefoxRelayForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
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.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export class ForwarderOptions {
|
||||
apiKey: string;
|
||||
website: string;
|
||||
fastmail = new FastmailForwarderOptions();
|
||||
anonaddy = new AnonAddyForwarderOptions();
|
||||
}
|
||||
|
||||
export class FastmailForwarderOptions {
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
export class AnonAddyForwarderOptions {
|
||||
domain: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export interface Forwarder {
|
||||
generate(apiService: ApiService, options: ForwarderOptions): Promise<string>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export { AnonAddyForwarder } from "./anon-addy-forwarder";
|
||||
export { DuckDuckGoForwarder } from "./duck-duck-go-forwarder";
|
||||
export { FastmailForwarder } from "./fastmail-forwarder";
|
||||
export { FirefoxRelayForwarder } from "./firefox-relay-forwarder";
|
||||
export { Forwarder } from "./forwarder";
|
||||
export { ForwarderOptions } from "./forwarder-options";
|
||||
export { SimpleLoginForwarder } from "./simple-login-forwarder";
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class SimpleLoginForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
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.";
|
||||
}
|
||||
}
|
||||
2
libs/common/src/tools/generator/username/index.ts
Normal file
2
libs/common/src/tools/generator/username/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
|
||||
export { UsernameGenerationService } from "./username-generation.service";
|
||||
@@ -0,0 +1,9 @@
|
||||
export abstract class UsernameGenerationServiceAbstraction {
|
||||
generateUsername: (options: any) => Promise<string>;
|
||||
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>;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { CryptoService } from "../../../abstractions/crypto.service";
|
||||
import { StateService } from "../../../abstractions/state.service";
|
||||
import { EFFLongWordList } from "../../../misc/wordlist";
|
||||
|
||||
import {
|
||||
AnonAddyForwarder,
|
||||
DuckDuckGoForwarder,
|
||||
FastmailForwarder,
|
||||
FirefoxRelayForwarder,
|
||||
Forwarder,
|
||||
ForwarderOptions,
|
||||
SimpleLoginForwarder,
|
||||
} from "./email-forwarders";
|
||||
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
|
||||
|
||||
const DefaultOptions = {
|
||||
type: "word",
|
||||
wordCapitalize: true,
|
||||
wordIncludeNumber: true,
|
||||
subaddressType: "random",
|
||||
catchallType: "random",
|
||||
forwardedService: "",
|
||||
forwardedAnonAddyDomain: "anonaddy.me",
|
||||
};
|
||||
|
||||
export class UsernameGenerationService implements UsernameGenerationServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService
|
||||
) {}
|
||||
|
||||
generateUsername(options: any): Promise<string> {
|
||||
if (options.type === "catchall") {
|
||||
return this.generateCatchall(options);
|
||||
} else if (options.type === "subaddress") {
|
||||
return this.generateSubaddress(options);
|
||||
} else if (options.type === "forwarded") {
|
||||
return this.generateForwarded(options);
|
||||
} else {
|
||||
return this.generateWord(options);
|
||||
}
|
||||
}
|
||||
|
||||
async generateWord(options: any): Promise<string> {
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
if (o.wordCapitalize == null) {
|
||||
o.wordCapitalize = true;
|
||||
}
|
||||
if (o.wordIncludeNumber == null) {
|
||||
o.wordIncludeNumber = true;
|
||||
}
|
||||
|
||||
const wordIndex = await this.cryptoService.randomNumber(0, EFFLongWordList.length - 1);
|
||||
let word = EFFLongWordList[wordIndex];
|
||||
if (o.wordCapitalize) {
|
||||
word = word.charAt(0).toUpperCase() + word.slice(1);
|
||||
}
|
||||
if (o.wordIncludeNumber) {
|
||||
const num = await this.cryptoService.randomNumber(1, 9999);
|
||||
word = word + this.zeroPad(num.toString(), 4);
|
||||
}
|
||||
return word;
|
||||
}
|
||||
|
||||
async generateSubaddress(options: any): Promise<string> {
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
const subaddressEmail = o.subaddressEmail;
|
||||
if (subaddressEmail == null || subaddressEmail.length < 3) {
|
||||
return o.subaddressEmail;
|
||||
}
|
||||
const atIndex = subaddressEmail.indexOf("@");
|
||||
if (atIndex < 1 || atIndex >= subaddressEmail.length - 1) {
|
||||
return subaddressEmail;
|
||||
}
|
||||
if (o.subaddressType == null) {
|
||||
o.subaddressType = "random";
|
||||
}
|
||||
|
||||
const emailBeginning = subaddressEmail.substr(0, atIndex);
|
||||
const emailEnding = subaddressEmail.substr(atIndex + 1, subaddressEmail.length);
|
||||
|
||||
let subaddressString = "";
|
||||
if (o.subaddressType === "random") {
|
||||
subaddressString = await this.randomString(8);
|
||||
} else if (o.subaddressType === "website-name") {
|
||||
subaddressString = o.website;
|
||||
}
|
||||
return emailBeginning + "+" + subaddressString + "@" + emailEnding;
|
||||
}
|
||||
|
||||
async generateCatchall(options: any): Promise<string> {
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
if (o.catchallDomain == null || o.catchallDomain === "") {
|
||||
return null;
|
||||
}
|
||||
if (o.catchallType == null) {
|
||||
o.catchallType = "random";
|
||||
}
|
||||
|
||||
let startString = "";
|
||||
if (o.catchallType === "random") {
|
||||
startString = await this.randomString(8);
|
||||
} else if (o.catchallType === "website-name") {
|
||||
startString = o.website;
|
||||
}
|
||||
return startString + "@" + o.catchallDomain;
|
||||
}
|
||||
|
||||
async generateForwarded(options: any): Promise<string> {
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
if (o.forwardedService == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let forwarder: Forwarder = null;
|
||||
const forwarderOptions = new ForwarderOptions();
|
||||
forwarderOptions.website = o.website;
|
||||
if (o.forwardedService === "simplelogin") {
|
||||
forwarder = new SimpleLoginForwarder();
|
||||
forwarderOptions.apiKey = o.forwardedSimpleLoginApiKey;
|
||||
} else if (o.forwardedService === "anonaddy") {
|
||||
forwarder = new AnonAddyForwarder();
|
||||
forwarderOptions.apiKey = o.forwardedAnonAddyApiToken;
|
||||
forwarderOptions.anonaddy.domain = o.forwardedAnonAddyDomain;
|
||||
} else if (o.forwardedService === "firefoxrelay") {
|
||||
forwarder = new FirefoxRelayForwarder();
|
||||
forwarderOptions.apiKey = o.forwardedFirefoxApiToken;
|
||||
} else if (o.forwardedService === "fastmail") {
|
||||
forwarder = new FastmailForwarder();
|
||||
forwarderOptions.apiKey = o.forwardedFastmailApiToken;
|
||||
} else if (o.forwardedService === "duckduckgo") {
|
||||
forwarder = new DuckDuckGoForwarder();
|
||||
forwarderOptions.apiKey = o.forwardedDuckDuckGoToken;
|
||||
}
|
||||
|
||||
if (forwarder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return forwarder.generate(this.apiService, forwarderOptions);
|
||||
}
|
||||
|
||||
async getOptions(): Promise<any> {
|
||||
let options = await this.stateService.getUsernameGenerationOptions();
|
||||
if (options == null) {
|
||||
options = Object.assign({}, DefaultOptions);
|
||||
} else {
|
||||
options = Object.assign({}, DefaultOptions, options);
|
||||
}
|
||||
await this.stateService.setUsernameGenerationOptions(options);
|
||||
return options;
|
||||
}
|
||||
|
||||
async saveOptions(options: any) {
|
||||
await this.stateService.setUsernameGenerationOptions(options);
|
||||
}
|
||||
|
||||
private async randomString(length: number) {
|
||||
let str = "";
|
||||
const charSet = "abcdefghijklmnopqrstuvwxyz1234567890";
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomCharIndex = await this.cryptoService.randomNumber(0, charSet.length - 1);
|
||||
str += charSet.charAt(randomCharIndex);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// ref: https://stackoverflow.com/a/10073788
|
||||
private zeroPad(number: string, width: number) {
|
||||
return number.length >= width
|
||||
? number
|
||||
: new Array(width - number.length + 1).join("0") + number;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user