2018-11-13 04:54:18 +01:00
|
|
|
import * as zxcvbn from 'zxcvbn';
|
|
|
|
|
2018-02-19 19:07:19 +01:00
|
|
|
import { CipherString } from '../models/domain/cipherString';
|
2018-07-27 22:44:20 +02:00
|
|
|
import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory';
|
2018-01-09 23:55:32 +01:00
|
|
|
|
2018-02-19 19:07:19 +01:00
|
|
|
import { CryptoService } from '../abstractions/crypto.service';
|
2018-01-09 23:55:32 +01:00
|
|
|
import {
|
2018-01-29 23:59:57 +01:00
|
|
|
PasswordGenerationService as PasswordGenerationServiceAbstraction,
|
2018-02-19 19:07:19 +01:00
|
|
|
} from '../abstractions/passwordGeneration.service';
|
|
|
|
import { StorageService } from '../abstractions/storage.service';
|
2018-01-09 23:55:32 +01:00
|
|
|
|
2018-10-08 23:54:54 +02:00
|
|
|
import { EEFLongWordList } from '../misc/wordlist';
|
2018-10-08 23:26:13 +02:00
|
|
|
|
2018-01-09 23:55:32 +01:00
|
|
|
const DefaultOptions = {
|
|
|
|
length: 14,
|
|
|
|
ambiguous: false,
|
|
|
|
number: true,
|
|
|
|
minNumber: 1,
|
|
|
|
uppercase: true,
|
2018-02-09 18:08:33 +01:00
|
|
|
minUppercase: 0,
|
2018-01-09 23:55:32 +01:00
|
|
|
lowercase: true,
|
2018-02-09 18:08:33 +01:00
|
|
|
minLowercase: 0,
|
2018-01-09 23:55:32 +01:00
|
|
|
special: false,
|
|
|
|
minSpecial: 1,
|
2018-10-09 04:06:06 +02:00
|
|
|
type: 'password',
|
2018-10-08 23:54:54 +02:00
|
|
|
numWords: 3,
|
2018-10-09 04:06:06 +02:00
|
|
|
wordSeparator: '-',
|
2019-06-06 20:06:38 +02:00
|
|
|
capitalize: false,
|
|
|
|
includeNumber: false,
|
2018-01-09 23:55:32 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
const Keys = {
|
|
|
|
options: 'passwordGenerationOptions',
|
|
|
|
history: 'generatedPasswordHistory',
|
|
|
|
};
|
|
|
|
|
|
|
|
const MaxPasswordsInHistory = 100;
|
|
|
|
|
2018-01-29 23:59:57 +01:00
|
|
|
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
|
2018-04-23 19:03:47 +02:00
|
|
|
private optionsCache: any;
|
2018-07-27 22:44:20 +02:00
|
|
|
private history: GeneratedPasswordHistory[];
|
2018-04-23 19:03:47 +02:00
|
|
|
|
|
|
|
constructor(private cryptoService: CryptoService, private storageService: StorageService) { }
|
|
|
|
|
|
|
|
async generatePassword(options: any): Promise<string> {
|
2018-01-09 23:55:32 +01:00
|
|
|
// overload defaults with given options
|
|
|
|
const o = Object.assign({}, DefaultOptions, options);
|
|
|
|
|
2018-10-09 04:06:06 +02:00
|
|
|
if (o.type === 'passphrase') {
|
2018-10-08 23:26:13 +02:00
|
|
|
return this.generatePassphrase(options);
|
|
|
|
}
|
|
|
|
|
2018-01-09 23:55:32 +01:00
|
|
|
// sanitize
|
2018-02-09 18:08:33 +01:00
|
|
|
if (o.uppercase && o.minUppercase <= 0) {
|
2018-01-09 23:55:32 +01:00
|
|
|
o.minUppercase = 1;
|
|
|
|
}
|
2018-02-09 18:08:33 +01:00
|
|
|
if (o.lowercase && o.minLowercase <= 0) {
|
2018-01-09 23:55:32 +01:00
|
|
|
o.minLowercase = 1;
|
|
|
|
}
|
2018-02-09 18:08:33 +01:00
|
|
|
if (o.number && o.minNumber <= 0) {
|
2018-01-09 23:55:32 +01:00
|
|
|
o.minNumber = 1;
|
|
|
|
}
|
2018-02-09 18:08:33 +01:00
|
|
|
if (o.special && o.minSpecial <= 0) {
|
2018-01-09 23:55:32 +01:00
|
|
|
o.minSpecial = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!o.length || o.length < 1) {
|
|
|
|
o.length = 10;
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2018-04-23 19:03:47 +02:00
|
|
|
await this.shuffleArray(positions);
|
2018-01-09 23:55:32 +01:00
|
|
|
|
|
|
|
// build out the char sets
|
|
|
|
let allCharSet = '';
|
|
|
|
|
|
|
|
let lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz';
|
|
|
|
if (o.ambiguous) {
|
|
|
|
lowercaseCharSet += 'l';
|
|
|
|
}
|
|
|
|
if (o.lowercase) {
|
|
|
|
allCharSet += lowercaseCharSet;
|
|
|
|
}
|
|
|
|
|
2019-04-29 19:52:33 +02:00
|
|
|
let uppercaseCharSet = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
2018-01-09 23:55:32 +01:00
|
|
|
if (o.ambiguous) {
|
2019-04-29 19:52:33 +02:00
|
|
|
uppercaseCharSet += 'IO';
|
2018-01-09 23:55:32 +01:00
|
|
|
}
|
|
|
|
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;
|
2018-04-23 19:03:47 +02:00
|
|
|
default:
|
|
|
|
break;
|
2018-01-09 23:55:32 +01:00
|
|
|
}
|
|
|
|
|
2018-04-23 19:03:47 +02:00
|
|
|
const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1);
|
2018-01-09 23:55:32 +01:00
|
|
|
password += positionChars.charAt(randomCharIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
return password;
|
|
|
|
}
|
|
|
|
|
2018-10-08 23:26:13 +02:00
|
|
|
async generatePassphrase(options: any): Promise<string> {
|
|
|
|
const o = Object.assign({}, DefaultOptions, options);
|
|
|
|
|
2018-10-08 23:54:54 +02:00
|
|
|
if (o.numWords == null || o.numWords <= 2) {
|
|
|
|
o.numWords = DefaultOptions.numWords;
|
|
|
|
}
|
|
|
|
if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) {
|
2018-10-09 04:06:06 +02:00
|
|
|
o.wordSeparator = ' ';
|
2018-10-08 23:54:54 +02:00
|
|
|
}
|
2019-06-06 20:06:38 +02:00
|
|
|
if (o.capitalize == null) {
|
|
|
|
o.addCommonRequirements = false;
|
|
|
|
}
|
|
|
|
if (o.includeNumber == null) {
|
|
|
|
o.includeNumber = false;
|
|
|
|
}
|
|
|
|
|
2018-10-08 23:54:54 +02:00
|
|
|
const listLength = EEFLongWordList.length - 1;
|
2019-06-06 20:15:48 +02:00
|
|
|
const wordList = new Array(o.numWords);
|
2018-10-08 23:26:13 +02:00
|
|
|
for (let i = 0; i < o.numWords; i++) {
|
2018-10-08 23:54:54 +02:00
|
|
|
const wordIndex = await this.cryptoService.randomNumber(0, listLength);
|
2019-06-06 20:06:38 +02:00
|
|
|
if (o.capitalize) {
|
|
|
|
wordList[i] = this.capitalize(EEFLongWordList[wordIndex]);
|
|
|
|
} else {
|
|
|
|
wordList[i] = EEFLongWordList[wordIndex];
|
|
|
|
}
|
2018-10-08 23:26:13 +02:00
|
|
|
}
|
2019-06-06 20:06:38 +02:00
|
|
|
|
|
|
|
if (o.includeNumber) {
|
2019-06-06 20:15:48 +02:00
|
|
|
await this.appendRandomNumberToRandomWord(wordList);
|
2019-06-06 20:06:38 +02:00
|
|
|
}
|
2018-10-08 23:54:54 +02:00
|
|
|
return wordList.join(o.wordSeparator);
|
2018-10-08 23:26:13 +02:00
|
|
|
}
|
|
|
|
|
2018-01-09 23:55:32 +01:00
|
|
|
async getOptions() {
|
|
|
|
if (this.optionsCache == null) {
|
|
|
|
const options = await this.storageService.get(Keys.options);
|
|
|
|
if (options == null) {
|
|
|
|
this.optionsCache = DefaultOptions;
|
|
|
|
} else {
|
2018-10-10 15:59:09 +02:00
|
|
|
this.optionsCache = Object.assign({}, DefaultOptions, options);
|
2018-01-09 23:55:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.optionsCache;
|
|
|
|
}
|
|
|
|
|
|
|
|
async saveOptions(options: any) {
|
|
|
|
await this.storageService.save(Keys.options, options);
|
|
|
|
this.optionsCache = options;
|
|
|
|
}
|
|
|
|
|
2018-07-27 22:44:20 +02:00
|
|
|
async getHistory(): Promise<GeneratedPasswordHistory[]> {
|
2018-06-13 23:14:26 +02:00
|
|
|
const hasKey = await this.cryptoService.hasKey();
|
2018-01-19 20:58:06 +01:00
|
|
|
if (!hasKey) {
|
2018-07-27 22:44:20 +02:00
|
|
|
return new Array<GeneratedPasswordHistory>();
|
2018-01-19 20:58:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.history) {
|
2018-07-27 22:44:20 +02:00
|
|
|
const encrypted = await this.storageService.get<GeneratedPasswordHistory[]>(Keys.history);
|
2018-01-19 20:58:06 +01:00
|
|
|
this.history = await this.decryptHistory(encrypted);
|
|
|
|
}
|
|
|
|
|
2018-07-27 22:44:20 +02:00
|
|
|
return this.history || new Array<GeneratedPasswordHistory>();
|
2018-01-09 23:55:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async addHistory(password: string): Promise<any> {
|
2018-01-19 20:58:06 +01:00
|
|
|
// Cannot add new history if no key is available
|
2018-06-13 23:14:26 +02:00
|
|
|
const hasKey = await this.cryptoService.hasKey();
|
2018-01-19 20:58:06 +01:00
|
|
|
if (!hasKey) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const currentHistory = await this.getHistory();
|
|
|
|
|
2018-01-09 23:55:32 +01:00
|
|
|
// Prevent duplicates
|
2018-01-19 20:58:06 +01:00
|
|
|
if (this.matchesPrevious(password, currentHistory)) {
|
2018-01-09 23:55:32 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-27 22:44:20 +02:00
|
|
|
currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now()));
|
2018-01-09 23:55:32 +01:00
|
|
|
|
|
|
|
// Remove old items.
|
2018-01-19 20:58:06 +01:00
|
|
|
if (currentHistory.length > MaxPasswordsInHistory) {
|
2018-02-18 04:51:28 +01:00
|
|
|
currentHistory.pop();
|
2018-01-09 23:55:32 +01:00
|
|
|
}
|
|
|
|
|
2018-01-19 20:58:06 +01:00
|
|
|
const newHistory = await this.encryptHistory(currentHistory);
|
2018-01-09 23:55:32 +01:00
|
|
|
return await this.storageService.save(Keys.history, newHistory);
|
|
|
|
}
|
|
|
|
|
|
|
|
async clear(): Promise<any> {
|
|
|
|
this.history = [];
|
|
|
|
return await this.storageService.remove(Keys.history);
|
|
|
|
}
|
|
|
|
|
2018-11-13 04:54:18 +01:00
|
|
|
passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult {
|
|
|
|
if (password == null || password.length === 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
let globalUserInputs = ['bitwarden', 'bit', 'warden'];
|
2018-12-13 01:37:06 +01:00
|
|
|
if (userInputs != null && userInputs.length > 0) {
|
2018-11-13 04:54:18 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-06-06 20:06:38 +02:00
|
|
|
private capitalize(str: string) {
|
|
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
|
|
}
|
|
|
|
|
2019-06-06 20:15:48 +02:00
|
|
|
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;
|
2019-06-06 20:06:38 +02:00
|
|
|
}
|
|
|
|
|
2018-07-27 22:44:20 +02:00
|
|
|
private async encryptHistory(history: GeneratedPasswordHistory[]): Promise<GeneratedPasswordHistory[]> {
|
2018-01-19 20:58:06 +01:00
|
|
|
if (history == null || history.length === 0) {
|
2018-01-09 23:55:32 +01:00
|
|
|
return Promise.resolve([]);
|
|
|
|
}
|
|
|
|
|
2018-01-19 20:58:06 +01:00
|
|
|
const promises = history.map(async (item) => {
|
2018-01-09 23:55:32 +01:00
|
|
|
const encrypted = await this.cryptoService.encrypt(item.password);
|
2018-07-27 22:44:20 +02:00
|
|
|
return new GeneratedPasswordHistory(encrypted.encryptedString, item.date);
|
2018-01-09 23:55:32 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
return await Promise.all(promises);
|
|
|
|
}
|
|
|
|
|
2018-07-27 22:44:20 +02:00
|
|
|
private async decryptHistory(history: GeneratedPasswordHistory[]): Promise<GeneratedPasswordHistory[]> {
|
2018-01-09 23:55:32 +01:00
|
|
|
if (history == null || history.length === 0) {
|
|
|
|
return Promise.resolve([]);
|
|
|
|
}
|
|
|
|
|
|
|
|
const promises = history.map(async (item) => {
|
2018-04-22 05:14:04 +02:00
|
|
|
const decrypted = await this.cryptoService.decryptToUtf8(new CipherString(item.password));
|
2018-07-27 22:44:20 +02:00
|
|
|
return new GeneratedPasswordHistory(decrypted, item.date);
|
2018-01-09 23:55:32 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
return await Promise.all(promises);
|
|
|
|
}
|
|
|
|
|
2018-07-27 22:44:20 +02:00
|
|
|
private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean {
|
2018-01-19 20:58:06 +01:00
|
|
|
if (history == null || history.length === 0) {
|
2018-01-09 23:55:32 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-01-19 20:58:06 +01:00
|
|
|
return history[history.length - 1].password === password;
|
2018-01-09 23:55:32 +01:00
|
|
|
}
|
2018-04-23 19:03:47 +02:00
|
|
|
|
|
|
|
// 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]];
|
|
|
|
}
|
|
|
|
}
|
2018-01-09 23:55:32 +01:00
|
|
|
}
|