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);