diff --git a/src/background.html b/src/background.html index 1c618fa8..9703a9aa 100644 --- a/src/background.html +++ b/src/background.html @@ -15,7 +15,6 @@ - diff --git a/src/background.js b/src/background.js index 9e33c3b3..64a61e8b 100644 --- a/src/background.js +++ b/src/background.js @@ -4,6 +4,7 @@ import i18nService from './services/i18nService.js'; import LockService from './services/lockService.js'; import UtilsService from './services/utils.service'; import CryptoService from './services/crypto.service'; +import PasswordGenerationService from './services/passwordGeneration.service'; // Model imports import { AttachmentData } from './models/data/attachmentData'; @@ -86,7 +87,7 @@ var bg_isBackground = true, setIcon, refreshBadgeAndMenu); window.bg_syncService = bg_syncService = new SyncService(bg_cipherService, bg_folderService, bg_userService, bg_apiService, bg_settingsService, bg_cryptoService, logout); - window.bg_passwordGenerationService = bg_passwordGenerationService = new PasswordGenerationService(bg_constantsService, bg_utilsService, bg_cryptoService); + window.bg_passwordGenerationService = bg_passwordGenerationService = new PasswordGenerationService(bg_cryptoService); window.bg_totpService = bg_totpService = new TotpService(bg_constantsService); window.bg_autofillService = bg_autofillService = new AutofillService(bg_utilsService, bg_totpService, bg_tokenService, bg_cipherService, bg_constantsService); @@ -101,7 +102,7 @@ var bg_isBackground = true, eventAction: 'Generated Password From Command' }); bg_passwordGenerationService.getOptions().then(function (options) { - var password = bg_passwordGenerationService.generatePassword(options); + var password = PasswordGenerationService.generatePassword(options); bg_utilsService.copyToClipboard(password); bg_passwordGenerationService.addHistory(password); }); @@ -236,7 +237,7 @@ var bg_isBackground = true, eventAction: 'Generated Password From Context Menu' }); bg_passwordGenerationService.getOptions().then(function (options) { - var password = bg_passwordGenerationService.generatePassword(options); + var password = PasswordGenerationService.generatePassword(options); bg_utilsService.copyToClipboard(password); bg_passwordGenerationService.addHistory(password); }); diff --git a/src/models/domain/passwordHistory.ts b/src/models/domain/passwordHistory.ts new file mode 100644 index 00000000..fc4eb566 --- /dev/null +++ b/src/models/domain/passwordHistory.ts @@ -0,0 +1,9 @@ +export default class PasswordHistory { + password: string; + date: number; + + constructor(password: string, date: number) { + this.password = password; + this.date = date; + } +} diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index d51d9fdb..3e98b0bb 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -17,11 +17,11 @@ const Keys = { const SigningAlgorithm = { name: 'HMAC', - hash: { name: 'SHA-256' } + hash: { name: 'SHA-256' }, }; const AesAlgorithm = { - name: 'AES-CBC' + name: 'AES-CBC', }; const Crypto = window.crypto; @@ -269,7 +269,7 @@ export default class CryptoService { return this.encrypt(bytes, key, 'raw'); } - async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey, + async encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey, plainValueEncoding: string = 'utf8'): Promise { if (!plainValue) { return Promise.resolve(null); @@ -307,7 +307,7 @@ export default class CryptoService { return encBytes.buffer; } - async decrypt(cipherString: CipherString, key: SymmetricCryptoKey, + async decrypt(cipherString: CipherString, key?: SymmetricCryptoKey, outputEncoding: string = 'utf8'): Promise { const ivBytes: string = forge.util.decode64(cipherString.initializationVector); const ctBytes: string = forge.util.decode64(cipherString.cipherText); @@ -531,7 +531,7 @@ export default class CryptoService { private async computeMacWC(dataBuf: ArrayBuffer, macKeyBuf: ArrayBuffer): Promise { const key = await Subtle.importKey('raw', macKeyBuf, SigningAlgorithm, false, ['sign']); - return await Subtle.sign(SigningAlgorithm, key, dataBuf); + return await Subtle.sign(SigningAlgorithm, key, dataBuf); } // Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification). diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts new file mode 100644 index 00000000..b724c97b --- /dev/null +++ b/src/services/passwordGeneration.service.ts @@ -0,0 +1,245 @@ +import { CipherString } from '../models/domain/cipherString'; +import PasswordHistory from '../models/domain/passwordHistory'; + +import ConstantsService from './constants.service'; +import CryptoService from './crypto.service'; +import UtilsService from './utils.service'; + +const DefaultOptions = { + length: 10, + ambiguous: false, + number: true, + minNumber: 1, + uppercase: true, + minUppercase: 1, + lowercase: true, + minLowercase: 1, + special: false, + minSpecial: 1, +}; + +const Keys = { + options: 'passwordGenerationOptions', +}; + +const MaxPasswordsInHistory = 100; + +export default class PasswordGenerationService { + static generatePassword(options: any): string { + // overload defaults with given options + const o = UtilsService.extendObject({}, 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) { + const self = this; + + const historyKey = ConstantsService.generatedPasswordHistoryKey; + UtilsService.getObjFromStorage(historyKey).then((encrypted) => { + return self.decryptHistory(encrypted); + }).then((history) => { + self.history = history; + }); + } + + // TODO: remove in favor of static + generatePassword(options: any) { + return PasswordGenerationService.generatePassword(options); + } + + async getOptions() { + if (this.optionsCache) { + return this.optionsCache; + } + + const options = await UtilsService.getObjFromStorage(Keys.options); + this.optionsCache = options; + return options; + } + + async saveOptions(options: any) { + await UtilsService.saveObjToStorage(Keys.options, options); + this.optionsCache = options; + } + + getHistory() { + return this.history; + } + + async addHistory(password: string) { + // 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(); + } + + await this.saveHistory(); + } + + clear(): Promise { + this.history = []; + return UtilsService.removeFromStorage(ConstantsService.generatedPasswordHistoryKey); + } + + private async saveHistory() { + const history = await this.encryptHistory(); + return UtilsService.saveObjToStorage(ConstantsService.generatedPasswordHistoryKey, history); + } + + private encryptHistory(): Promise { + if (this.history == null) { + return Promise.resolve([]); + } + + const self = this; + const promises = self.history.map(async (item) => { + const encrypted = await self.cryptoService.encrypt(item.password); + return new PasswordHistory(encrypted.encryptedString, item.date); + }); + + return Promise.all(promises); + } + + private decryptHistory(history: PasswordHistory[]): Promise { + if (history == null) { + return Promise.resolve([]); + } + + const self = this; + const promises = history.map(async (item) => { + const decrypted = await self.cryptoService.decrypt(new CipherString(item.password)); + return new PasswordHistory(decrypted, item.date); + }); + + return Promise.all(promises); + } + + private matchesPrevious(password: string): boolean { + if (this.history == null) { + return false; + } + + const len = this.history.length; + return len !== 0 && this.history[len - 1].password === password; + } +} diff --git a/src/services/passwordGenerationService.js b/src/services/passwordGenerationService.js deleted file mode 100644 index 21d4b274..00000000 --- a/src/services/passwordGenerationService.js +++ /dev/null @@ -1,266 +0,0 @@ -function PasswordGenerationService(constantsService, utilsService, cryptoService) { - this.optionsCache = null; - this.constantsService = constantsService; - this.utilsService = utilsService; - this.cryptoService = cryptoService; - this.history = []; - - initPasswordGenerationService(this); -} - -function initPasswordGenerationService(self) { - var optionsKey = 'passwordGenerationOptions'; - var defaultOptions = { - length: 10, - ambiguous: false, - number: true, - minNumber: 1, - uppercase: true, - minUppercase: 1, - lowercase: true, - minLowercase: 1, - special: false, - minSpecial: 1 - }; - - PasswordGenerationService.prototype.generatePassword = function (options) { - // overload defaults with given options - var o = extend({}, 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; - var minLength = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; - if (o.length < minLength) o.length = minLength; - - var positions = []; - if (o.lowercase && o.minLowercase > 0) { - for (var i = 0; i < o.minLowercase; i++) { - positions.push('l'); - } - } - if (o.uppercase && o.minUppercase > 0) { - for (var j = 0; j < o.minUppercase; j++) { - positions.push('u'); - } - } - if (o.number && o.minNumber > 0) { - for (var k = 0; k < o.minNumber; k++) { - positions.push('n'); - } - } - if (o.special && o.minSpecial > 0) { - for (var l = 0; l < o.minSpecial; l++) { - positions.push('s'); - } - } - while (positions.length < o.length) { - positions.push('a'); - } - - // shuffle - positions.sort(function () { - return randomInt(0, 1) * 2 - 1; - }); - - // build out the char sets - var allCharSet = ''; - - var lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz'; - if (o.ambiguous) lowercaseCharSet += 'l'; - if (o.lowercase) allCharSet += lowercaseCharSet; - - var uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; - if (o.ambiguous) uppercaseCharSet += 'O'; - if (o.uppercase) allCharSet += uppercaseCharSet; - - var numberCharSet = '23456789'; - if (o.ambiguous) numberCharSet += '01'; - if (o.number) allCharSet += numberCharSet; - - var specialCharSet = '!@#$%^&*'; - if (o.special) allCharSet += specialCharSet; - - var password = ''; - for (var m = 0; m < o.length; m++) { - var positionChars; - switch (positions[m]) { - 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; - } - - var randomCharIndex = randomInt(0, positionChars.length - 1); - password += positionChars.charAt(randomCharIndex); - } - - return password; - }; - - // EFForg/OpenWireless - // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js - function randomInt(min, max) { - var rval = 0; - var range = max - min + 1; - - var bits_needed = Math.ceil(Math.log2(range)); - if (bits_needed > 53) { - throw new Exception('We cannot generate numbers larger than 53 bits.'); - } - var bytes_needed = Math.ceil(bits_needed / 8); - var mask = Math.pow(2, bits_needed) - 1; - // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 - - // Create byte array and fill with N random numbers - var byteArray = new Uint8Array(bytes_needed); - window.crypto.getRandomValues(byteArray); - - var p = (bytes_needed - 1) * 8; - for (var i = 0; i < bytes_needed; i++) { - rval += byteArray[i] * Math.pow(2, p); - p -= 8; - } - - // Use & to apply the mask and reduce the number of recursive lookups - rval = rval & mask; - - if (rval >= range) { - // Integer out of acceptable range - return randomInt(min, max); - } - // Return an integer that falls within the range - return min + rval; - } - - function extend() { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (arguments[i].hasOwnProperty(key)) { - arguments[0][key] = arguments[i][key]; - } - } - } - - return arguments[0]; - } - - PasswordGenerationService.prototype.getOptions = function () { - var deferred = Q.defer(); - var self = this; - - if (self.optionsCache) { - deferred.resolve(self.optionsCache); - return deferred.promise; - } - - chrome.storage.local.get(optionsKey, function (obj) { - var options = obj[optionsKey]; - if (!options) { - options = defaultOptions; - } - - self.optionsCache = options; - deferred.resolve(self.optionsCache); - }); - - return deferred.promise; - }; - - PasswordGenerationService.prototype.saveOptions = function (options) { - var deferred = Q.defer(); - var self = this; - - var obj = {}; - obj[optionsKey] = options; - chrome.storage.local.set(obj, function () { - self.optionsCache = options; - deferred.resolve(); - }); - - return deferred.promise; - }; - - // History - - var historyKey = self.constantsService.generatedPasswordHistory; - var maxPasswordsInHistory = 100; - - self.utilsService.getObjFromStorage(historyKey).then(function (encrypted) { - return decrypt(encrypted); - }).then(function (history) { - history.forEach(function (item) { - self.history.push(item); - }); - }); - - PasswordGenerationService.prototype.getHistory = function () { - return self.history; - }; - - PasswordGenerationService.prototype.addHistory = function (password) { - // Prevent duplicates - if (matchesPrevious(password)) { - return; - } - - self.history.push({ - password: password, - date: Date.now() - }); - - // Remove old items. - if (self.history.length > maxPasswordsInHistory) { - self.history.shift(); - } - - save(); - }; - - PasswordGenerationService.prototype.clear = function () { - self.history = []; - self.utilsService.removeFromStorage(historyKey); - }; - - function save() { - return encryptHistory().then(function (history) { - return self.utilsService.saveObjToStorage(historyKey, history); - }); - } - - function encryptHistory() { - var promises = self.history.map(function (historyItem) { - return self.cryptoService.encrypt(historyItem.password).then(function (encrypted) { - return { - password: encrypted.encryptedString, - date: historyItem.date - }; - }); - }); - - return Q.all(promises); - } - - function decrypt(history) { - var promises = history.map(function (item) { - return self.cryptoService.decrypt(new CipherString(item.password)).then(function (decrypted) { - return { - password: decrypted, - date: item.date - }; - }); - }); - - return Q.all(promises); - } - - function matchesPrevious(password) { - var len = self.history.length; - return len !== 0 && self.history[len - 1].password === password; - } -} diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts index f165d04d..73381c67 100644 --- a/src/services/utils.service.ts +++ b/src/services/utils.service.ts @@ -8,6 +8,55 @@ const AnalyticsIds = { }; export default class UtilsService { + static extendObject(...objects: any[]): any { + for (let i = 1; i < objects.length; i++) { + for (const key in objects[i]) { + if (objects[i].hasOwnProperty(key)) { + objects[0][key] = objects[i][key]; + } + } + } + + return objects[0]; + } + + // EFForg/OpenWireless + // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js + static secureRandomNumber(min: number, max: number): number { + let rval = 0; + const range = max - min + 1; + const bitsNeeded = Math.ceil(Math.log2(range)); + if (bitsNeeded > 53) { + throw new Error('We cannot generate numbers larger than 53 bits.'); + } + + const bytesNeeded = Math.ceil(bitsNeeded / 8); + const mask = Math.pow(2, bitsNeeded) - 1; + // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 + + // Create byte array and fill with N random numbers + const byteArray = new Uint8Array(bytesNeeded); + window.crypto.getRandomValues(byteArray); + + let p = (bytesNeeded - 1) * 8; + for (let i = 0; i < bytesNeeded; i++) { + rval += byteArray[i] * Math.pow(2, p); + p -= 8; + } + + // Use & to apply the mask and reduce the number of recursive lookups + // tslint:disable-next-line + rval = rval & mask; + + if (rval >= range) { + // Integer out of acceptable range + return UtilsService.secureRandomNumber(min, max); + } + + // Return an integer that falls within the range + return min + rval; + } + static fromB64ToArray(str: string): Uint8Array { const binaryString = window.atob(str); const bytes = new Uint8Array(binaryString.length);