From 38502b5447c67680ea077104bae3d039d624cab3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 17:55:32 -0500 Subject: [PATCH] convert password generator to jslib --- src/abstractions/index.ts | 1 + .../passwordGeneration.service.ts | 12 + src/services/index.ts | 1 + src/services/passwordGeneration.service.ts | 243 ++++++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 src/abstractions/passwordGeneration.service.ts create mode 100644 src/services/passwordGeneration.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index d026e4c372..9804abdf32 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -2,6 +2,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { CryptoService } from './crypto.service'; export { MessagingService } from './messaging.service'; +export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; export { StorageService } from './storage.service'; export { TokenService } from './token.service'; diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts new file mode 100644 index 0000000000..1f2433842b --- /dev/null +++ b/src/abstractions/passwordGeneration.service.ts @@ -0,0 +1,12 @@ +import { PasswordHistory } from '../models/domain/passwordHistory'; + +export interface PasswordGenerationService { + optionsCache: any; + history: PasswordHistory[]; + generatePassword(options: any): string; + getOptions(): any; + saveOptions(options: any): Promise; + getHistory(): PasswordHistory[]; + addHistory(password: string): Promise; + clear(): Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index 67e7b6aa59..084707f27d 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -2,6 +2,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { ConstantsService } from './constants.service'; export { CryptoService } from './crypto.service'; +export { PasswordGenerationService } from './passwordGeneration.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts new file mode 100644 index 0000000000..771494d0bc --- /dev/null +++ b/src/services/passwordGeneration.service.ts @@ -0,0 +1,243 @@ +import { CipherString } from '../models/domain/cipherString'; +import { PasswordHistory } from '../models/domain/passwordHistory'; + +import { UtilsService } from './utils.service'; + +import { CryptoService } from '../abstractions/crypto.service'; +import { + PasswordGenerationService as PasswordGenerationServiceInterface, +} from '../abstractions/passwordGeneration.service'; +import { StorageService } from '../abstractions/storage.service'; + +const DefaultOptions = { + length: 14, + ambiguous: false, + number: true, + minNumber: 1, + uppercase: true, + minUppercase: 1, + lowercase: true, + minLowercase: 1, + special: false, + minSpecial: 1, +}; + +const Keys = { + options: 'passwordGenerationOptions', + history: 'generatedPasswordHistory', +}; + +const MaxPasswordsInHistory = 100; + +export class PasswordGenerationService implements PasswordGenerationServiceInterface { + static generatePassword(options: any): string { + // overload defaults with given options + const o = Object.assign({}, DefaultOptions, options); + + // sanitize + if (o.uppercase && o.minUppercase < 0) { + o.minUppercase = 1; + } + if (o.lowercase && o.minLowercase < 0) { + o.minLowercase = 1; + } + if (o.number && o.minNumber < 0) { + o.minNumber = 1; + } + if (o.special && o.minSpecial < 0) { + 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 + positions.sort(() => { + return UtilsService.secureRandomNumber(0, 1) * 2 - 1; + }); + + // build out the char sets + let allCharSet = ''; + + let lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz'; + if (o.ambiguous) { + lowercaseCharSet += 'l'; + } + if (o.lowercase) { + allCharSet += lowercaseCharSet; + } + + let uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; + if (o.ambiguous) { + uppercaseCharSet += 'O'; + } + 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; + } + + const randomCharIndex = UtilsService.secureRandomNumber(0, positionChars.length - 1); + password += positionChars.charAt(randomCharIndex); + } + + return password; + } + + optionsCache: any; + history: PasswordHistory[] = []; + + constructor(private cryptoService: CryptoService, + private storageService: StorageService) { + storageService.get(Keys.history).then((encrypted) => { + return this.decryptHistory(encrypted); + }).then((history) => { + this.history = history; + }); + } + + generatePassword(options: any) { + return PasswordGenerationService.generatePassword(options); + } + + async getOptions() { + if (this.optionsCache == null) { + const options = await this.storageService.get(Keys.options); + if (options == null) { + this.optionsCache = DefaultOptions; + } else { + this.optionsCache = options; + } + } + + return this.optionsCache; + } + + async saveOptions(options: any) { + await this.storageService.save(Keys.options, options); + this.optionsCache = options; + } + + getHistory() { + return this.history || new Array(); + } + + async addHistory(password: string): Promise { + // Prevent duplicates + if (this.matchesPrevious(password)) { + return; + } + + this.history.push(new PasswordHistory(password, Date.now())); + + // Remove old items. + if (this.history.length > MaxPasswordsInHistory) { + this.history.shift(); + } + + const newHistory = await this.encryptHistory(); + return await this.storageService.save(Keys.history, newHistory); + } + + async clear(): Promise { + this.history = []; + return await this.storageService.remove(Keys.history); + } + + private async encryptHistory(): Promise { + if (this.history == null || this.history.length === 0) { + return Promise.resolve([]); + } + + const promises = this.history.map(async (item) => { + const encrypted = await this.cryptoService.encrypt(item.password); + return new PasswordHistory(encrypted.encryptedString, item.date); + }); + + return await Promise.all(promises); + } + + private async decryptHistory(history: PasswordHistory[]): Promise { + if (history == null || history.length === 0) { + return Promise.resolve([]); + } + + const promises = history.map(async (item) => { + const decrypted = await this.cryptoService.decrypt(new CipherString(item.password)); + return new PasswordHistory(decrypted, item.date); + }); + + return await Promise.all(promises); + } + + private matchesPrevious(password: string): boolean { + if (this.history == null || this.history.length === 0) { + return false; + } + + return this.history[this.history.length - 1].password === password; + } +}