From f1262147a33f302b5e569f13f56739f05bbec362 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 17 Oct 2017 22:42:05 +0200 Subject: [PATCH] History of generated passwords (#310) * Save last 5 passwords. * Move password history to seperate page. * Use the util helpers for accessing the local storage. * Change close to back for password history. Remove unused html. * Change orderBy to use the date instead of magic array. * Move historyService to background. * Add passwords generated from shortcut and contextmenu to history. * Fix return to edit/add not working in password generator history. * Change password icon to clipboard. * Change link to password history to use on-click. * Clear password generator history on logout. * Code style fix. * Add new .wrap class for wrapping long text. Fix password icon. --- src/_locales/en/messages.json | 9 ++ src/background.js | 7 +- src/popup/app/config.js | 7 ++ .../tools/toolsPasswordGeneratorController.js | 9 ++ ...toolsPasswordGeneratorHistoryController.js | 31 +++++++ .../tools/views/toolsPasswordGenerator.html | 4 + .../views/toolsPasswordGeneratorHistory.html | 27 ++++++ src/popup/index.html | 1 + src/popup/less/components.less | 4 + src/services/constantsService.js | 1 + src/services/passwordGenerationService.js | 91 ++++++++++++++++++- 11 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 src/popup/app/tools/toolsPasswordGeneratorHistoryController.js create mode 100644 src/popup/app/tools/views/toolsPasswordGeneratorHistory.html diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index fa774d642f2..52b100e3241 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -945,5 +945,14 @@ }, "typeIdentity": { "message": "Identity" + }, + "viewPasswordHistory": { + "message": "View Password History" + }, + "generatePasswordHistory": { + "message": "Generated Passwords History" + }, + "back": { + "message": "Back" } } diff --git a/src/background.js b/src/background.js index e1a2d46e9cb..5bc8c38e173 100644 --- a/src/background.js +++ b/src/background.js @@ -45,7 +45,7 @@ var bg_isBackground = true, setIcon, refreshBadgeAndMenu); bg_syncService = new SyncService(bg_cipherService, bg_folderService, bg_userService, bg_apiService, bg_settingsService, bg_cryptoService, logout); - bg_passwordGenerationService = new PasswordGenerationService(); + bg_passwordGenerationService = new PasswordGenerationService(bg_constantsService, bg_utilsService, bg_cryptoService); bg_totpService = new TotpService(bg_constantsService); bg_autofillService = new AutofillService(bg_utilsService, bg_totpService, bg_tokenService, bg_cipherService); @@ -59,6 +59,7 @@ var bg_isBackground = true, bg_passwordGenerationService.getOptions().then(function (options) { var password = bg_passwordGenerationService.generatePassword(options); bg_utilsService.copyToClipboard(password); + bg_passwordGenerationService.addHistory(password); }); } else if (command === 'autofill_login') { @@ -193,6 +194,7 @@ var bg_isBackground = true, bg_passwordGenerationService.getOptions().then(function (options) { var password = bg_passwordGenerationService.generatePassword(options); bg_utilsService.copyToClipboard(password); + bg_passwordGenerationService.addHistory(password); }); } else if (info.parentMenuItemId === 'autofill' || info.parentMenuItemId === 'copy-username' || @@ -817,7 +819,8 @@ var bg_isBackground = true, bg_userService.clear(), bg_settingsService.clear(userId), bg_cipherService.clear(userId), - bg_folderService.clear(userId) + bg_folderService.clear(userId), + bg_passwordGenerationService.clear() ]).then(function () { chrome.runtime.sendMessage({ command: 'doneLoggingOut', expired: expired diff --git a/src/popup/app/config.js b/src/popup/app/config.js index 50c9178be8f..87b04802aba 100644 --- a/src/popup/app/config.js +++ b/src/popup/app/config.js @@ -171,6 +171,13 @@ data: { authorize: true }, params: { animation: null, addState: null, editState: null } }) + .state('passwordGeneratorHistory', { + url: '/history', + templateUrl: 'app/tools/views/toolsPasswordGeneratorHistory.html', + controller: 'toolsPasswordGeneratorHistoryController', + data: { authorize: true }, + params: { animation: null, addState: null, editState: null } + }) .state('export', { url: '/export', templateUrl: 'app/tools/views/toolsExport.html', diff --git a/src/popup/app/tools/toolsPasswordGeneratorController.js b/src/popup/app/tools/toolsPasswordGeneratorController.js index 7d8fbb0f4cf..a9498293b71 100644 --- a/src/popup/app/tools/toolsPasswordGeneratorController.js +++ b/src/popup/app/tools/toolsPasswordGeneratorController.js @@ -57,6 +57,7 @@ }; $scope.clipboardSuccess = function (e) { + passwordGenerationService.addHistory(e.text); $analytics.eventTrack('Copied Generated Password'); e.clearSelection(); toastr.info(i18nService.passwordCopied); @@ -79,6 +80,14 @@ dismiss(); }; + $scope.goHistory = function () { + $state.go('^.passwordGeneratorHistory', { + animation: 'in-slide-left', + addState: $stateParams.addState, + editState: $stateParams.editState + }); + }; + function dismiss() { if (addState) { $state.go('addCipher', { diff --git a/src/popup/app/tools/toolsPasswordGeneratorHistoryController.js b/src/popup/app/tools/toolsPasswordGeneratorHistoryController.js new file mode 100644 index 00000000000..b1627ad6972 --- /dev/null +++ b/src/popup/app/tools/toolsPasswordGeneratorHistoryController.js @@ -0,0 +1,31 @@ +angular + .module('bit.tools') + + .controller('toolsPasswordGeneratorHistoryController', function ( + $scope, $state, $stateParams, toastr, $analytics, i18nService, passwordGenerationService) { + $scope.i18n = i18nService; + + $scope.passwords = passwordGenerationService.getHistory(); + + $scope.clipboardError = function (e, password) { + toastr.info(i18n.browserNotSupportClipboard); + }; + + $scope.clipboardSuccess = function (e) { + $analytics.eventTrack('Copied Generated Password'); + e.clearSelection(); + toastr.info(i18nService.passwordCopied); + }; + + $scope.close = function () { + dismiss(); + }; + + function dismiss() { + $state.go('^.passwordGenerator', { + animation: 'out-slide-right', + addState: $stateParams.addState, + editState: $stateParams.editState + }); + } + }); diff --git a/src/popup/app/tools/views/toolsPasswordGenerator.html b/src/popup/app/tools/views/toolsPasswordGenerator.html index f474c273e27..c1be065a3bf 100644 --- a/src/popup/app/tools/views/toolsPasswordGenerator.html +++ b/src/popup/app/tools/views/toolsPasswordGenerator.html @@ -21,6 +21,10 @@ ngclipboard-success="clipboardSuccess(e)" data-clipboard-text="{{password}}"> {{i18n.copyPassword}} + + {{i18n.viewPasswordHistory}} + +
diff --git a/src/popup/app/tools/views/toolsPasswordGeneratorHistory.html b/src/popup/app/tools/views/toolsPasswordGeneratorHistory.html new file mode 100644 index 00000000000..f56b957c1bc --- /dev/null +++ b/src/popup/app/tools/views/toolsPasswordGeneratorHistory.html @@ -0,0 +1,27 @@ +
+ +
{{i18n.generatePasswordHistory}}
+
+
+
+
+
+
+
+ + + +
+ + {{password.password}} + + {{password.date | date}} +
+
+
+
+
diff --git a/src/popup/index.html b/src/popup/index.html index 4875c1e234b..c01419b0422 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -111,6 +111,7 @@ + diff --git a/src/popup/less/components.less b/src/popup/less/components.less index 9ea5a1cb333..b3e8e506fe5 100644 --- a/src/popup/less/components.less +++ b/src/popup/less/components.less @@ -428,6 +428,10 @@ } } + &.wrap { + overflow-wrap: break-word; + } + input:not([type="checkbox"]), select, textarea { border: none; width: 100%; diff --git a/src/services/constantsService.js b/src/services/constantsService.js index 25d5c19f309..fc2296b2906 100644 --- a/src/services/constantsService.js +++ b/src/services/constantsService.js @@ -9,6 +9,7 @@ function ConstantsService(i18nService) { enableAutoFillOnPageLoadKey: 'enableAutoFillOnPageLoad', lockOptionKey: 'lockOption', lastActiveKey: 'lastActive', + generatedPasswordHistory: 'generatedPasswordHistory', encType: { AesCbc256_B64: 0, AesCbc128_HmacSha256_B64: 1, diff --git a/src/services/passwordGenerationService.js b/src/services/passwordGenerationService.js index 3e353f06aa2..adcd148b955 100644 --- a/src/services/passwordGenerationService.js +++ b/src/services/passwordGenerationService.js @@ -1,10 +1,14 @@ -function PasswordGenerationService() { +function PasswordGenerationService(constantsService, utilsService, cryptoService) { this.optionsCache = null; + this.constantsService = constantsService; + this.utilsService = utilsService; + this.cryptoService = cryptoService; + this.history = []; - initPasswordGenerationService(); + initPasswordGenerationService(this); } -function initPasswordGenerationService() { +function initPasswordGenerationService(self) { var optionsKey = 'passwordGenerationOptions'; var defaultOptions = { length: 10, @@ -181,4 +185,85 @@ function initPasswordGenerationService() { return deferred.promise; }; + + // History + var key = self.constantsService.generatedPasswordHistory; + var MAX_PASSWORDS_IN_HISTORY = 10; + + self.utilsService + .getObjFromStorage(key) + .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 > MAX_PASSWORDS_IN_HISTORY) { + self.history.shift(); + } + + save(); + }; + + PasswordGenerationService.prototype.clear = function () { + self.history = []; + self.utilsService.removeFromStorage(key); + }; + + function save() { + return encryptHistory() + .then(function(history) { + return self.utilsService.saveObjToStorage(key, 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; + } + }