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.
This commit is contained in:
Oscar Hinton 2017-10-17 22:42:05 +02:00 committed by Kyle Spearrin
parent 358fb9b277
commit f1262147a3
11 changed files with 186 additions and 5 deletions

View File

@ -945,5 +945,14 @@
},
"typeIdentity": {
"message": "Identity"
},
"viewPasswordHistory": {
"message": "View Password History"
},
"generatePasswordHistory": {
"message": "Generated Passwords History"
},
"back": {
"message": "Back"
}
}

View File

@ -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

View File

@ -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',

View File

@ -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', {

View File

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

View File

@ -21,6 +21,10 @@
ngclipboard-success="clipboardSuccess(e)" data-clipboard-text="{{password}}">
{{i18n.copyPassword}}
</a>
<a class="list-section-item text-primary" href="" ng-click="goHistory()">
{{i18n.viewPasswordHistory}}
<i class="fa fa-chevron-right fa-lg"></i>
</a>
</div>
</div>
<div class="list-section">

View File

@ -0,0 +1,27 @@
<div class="header">
<div class="left">
<a ng-click="close()" href="">{{i18n.back}}</a>
</div>
<div class="title">{{i18n.generatePasswordHistory}}</div>
</div>
<div class="content">
<div class="list" style="margin-top: 0;">
<div class="list-section" ng-if="passwords.length !== 0">
<div class="list-section-items">
<div class="list-section-item list-section-item-checkbox condensed wrap" ng-repeat="password in passwords | orderBy: 'date':true">
<div class="action-buttons">
<span class="btn-list" stop-prop stop-click title="{{i18n.copyPassword}}" ngclipboard
ngclipboard-error="clipboardError(e)" ngclipboard-success="clipboardSuccess(e, i18n.password)"
data-clipboard-text="{{password.password}}">
<i class="fa fa-lg fa-clipboard"></i>
</span>
</div>
<span class="text monospaced">
{{password.password}}
</span>
<span class="detail">{{password.date | date}}</span>
</div>
</div>
</div>
</div>
</div>

View File

@ -111,6 +111,7 @@
<script src="app/tools/toolsModule.js"></script>
<script src="app/tools/toolsController.js"></script>
<script src="app/tools/toolsPasswordGeneratorController.js"></script>
<script src="app/tools/toolsPasswordGeneratorHistoryController.js"></script>
<script src="app/tools/toolsExportController.js"></script>
<script src="app/lock/lockModule.js"></script>

View File

@ -428,6 +428,10 @@
}
}
&.wrap {
overflow-wrap: break-word;
}
input:not([type="checkbox"]), select, textarea {
border: none;
width: 100%;

View File

@ -9,6 +9,7 @@ function ConstantsService(i18nService) {
enableAutoFillOnPageLoadKey: 'enableAutoFillOnPageLoad',
lockOptionKey: 'lockOption',
lastActiveKey: 'lastActive',
generatedPasswordHistory: 'generatedPasswordHistory',
encType: {
AesCbc256_B64: 0,
AesCbc128_HmacSha256_B64: 1,

View File

@ -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;
}
}