From 091f4cff7f05bb8d8df1bf3bcd0473239279d8f2 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 13 Nov 2018 15:05:57 -0600 Subject: [PATCH] Autolock wip --- .../javascripts/app/controllers/notes.js | 29 +- .../app/directives/views/accountMenu.js | 19 +- .../javascripts/app/services/httpManager.js | 2 +- .../app/services/passcodeManager.js | 273 +++++++++++------- .../app/services/privilegesManager.js | 15 +- app/assets/stylesheets/app/_editor.scss | 11 +- app/assets/stylesheets/app/_modals.scss | 4 + .../directives/account-menu.html.haml | 7 +- .../privileges-management-modal.html.haml | 11 + app/assets/templates/footer.html.haml | 2 +- app/assets/templates/notes.html.haml | 4 +- 11 files changed, 258 insertions(+), 119 deletions(-) diff --git a/app/assets/javascripts/app/controllers/notes.js b/app/assets/javascripts/app/controllers/notes.js index 1c33e0d1b..6c0f54644 100644 --- a/app/assets/javascripts/app/controllers/notes.js +++ b/app/assets/javascripts/app/controllers/notes.js @@ -31,7 +31,8 @@ angular.module('app') } } }) - .controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, storageManager, desktopManager) { + .controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, + storageManager, desktopManager, privilegesManager) { this.panelController = {}; @@ -198,19 +199,31 @@ angular.module('app') } } - this.selectNote = function(note, viaClick = false) { + this.selectNote = async function(note, viaClick = false) { if(!note) { this.createNewNote(); return; } - this.selectedNote = note; - note.conflict_of = null; // clear conflict - this.selectionMade()(note); - this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0); + let run = () => { + $timeout(() => { + this.selectedNote = note; + note.conflict_of = null; // clear conflict + this.selectionMade()(note); + this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0); - if(viaClick && this.isFiltering()) { - desktopManager.searchText(this.noteFilter.text); + if(viaClick && this.isFiltering()) { + desktopManager.searchText(this.noteFilter.text); + } + }) + } + + if(note.locked && await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionViewLockedNotes)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionViewLockedNotes, () => { + run(); + }); + } else { + run(); } } diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index c59f7267d..5a6c17450 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -38,7 +38,6 @@ class AccountMenu { } $scope.canAddPasscode = !authManager.isEphemeralSession(); - $scope.syncStatus = syncManager.syncStatus; $scope.submitMfaForm = function() { @@ -375,6 +374,24 @@ class AccountMenu { Passcode Lock */ + $scope.passcodeAutoLockOptions = passcodeManager.getAutoLockIntervalOptions(); + + $scope.reloadAutoLockInterval = function() { + passcodeManager.getAutoLockInterval().then((interval) => { + $timeout(() => { + $scope.selectedAutoLockInterval = interval; + console.log("selectedAutoLockInterval", $scope.selectedAutoLockInterval); + }) + }) + } + + $scope.reloadAutoLockInterval(); + + $scope.selectAutoLockInterval = async function(interval) { + await passcodeManager.setAutoLockInterval(interval); + $scope.reloadAutoLockInterval(); + } + $scope.hasPasscode = function() { return passcodeManager.hasPasscode(); } diff --git a/app/assets/javascripts/app/services/httpManager.js b/app/assets/javascripts/app/services/httpManager.js index 637db6041..a3415b4b5 100644 --- a/app/assets/javascripts/app/services/httpManager.js +++ b/app/assets/javascripts/app/services/httpManager.js @@ -5,7 +5,7 @@ class HttpManager extends SFHttpManager { super($timeout); this.setJWTRequestHandler(async () => { - return storageManager.getItem("jwt");; + return storageManager.getItem("jwt"); }) } } diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js index 8d6fad7fa..0210ee96b 100644 --- a/app/assets/javascripts/app/services/passcodeManager.js +++ b/app/assets/javascripts/app/services/passcodeManager.js @@ -1,112 +1,183 @@ -angular.module('app') - .provider('passcodeManager', function () { +class PasscodeManager { - this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) { - return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager); - } + constructor(authManager, storageManager) { + document.addEventListener('visibilitychange', () => { + this.documentVisibilityChanged(document.visibilityState); + }); - function PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) { + this.authManager = authManager; + this.storageManager = storageManager; - this._hasPasscode = storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null; + this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null; this._locked = this._hasPasscode; - this.isLocked = function() { - return this._locked; - } + const MillisecondsPerSecond = 1000; + PasscodeManager.AutoLockIntervalNone = 0; + PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond; + PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond; + PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond; - this.hasPasscode = function() { - return this._hasPasscode; - } + PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey"; + } - this.keys = function() { - return this._keys; - } - - this.passcodeAuthParams = function() { - var authParams = JSON.parse(storageManager.getItemSync("offlineParams", StorageManager.Fixed)); - if(authParams && !authParams.version) { - var keys = this.keys(); - if(keys && keys.ak) { - // If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams. - authParams.version = "002"; - } else { - authParams.version = "001"; - } + getAutoLockIntervalOptions() { + return [ + { + value: PasscodeManager.AutoLockIntervalNone, + label: "None" + }, + { + value: PasscodeManager.AutoLockIntervalOneMinute, + label: "1 Min" + }, + { + value: PasscodeManager.AutoLockIntervalFiveMinutes, + label: "5 Min" + }, + { + value: PasscodeManager.AutoLockIntervalOneHour, + label: "1 Hr" } - return authParams; - } + ] + } - this.verifyPasscode = async function(passcode) { - return new Promise(async (resolve, reject) => { - var params = this.passcodeAuthParams(); - let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params); - if(keys.pw !== params.hash) { - resolve(false); - } else { - resolve(true); - } - }) - } - - this.unlock = function(passcode, callback) { - var params = this.passcodeAuthParams(); - SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => { - if(keys.pw !== params.hash) { - callback(false); - return; - } - - this._keys = keys; - this._authParams = params; - this.decryptLocalStorage(keys, params).then(() => { - this._locked = false; - callback(true); - }) - }); - } - - this.setPasscode = (passcode, callback) => { - var uuid = SFJS.crypto.generateUUIDSync(); - - SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => { - let keys = results.keys; - let authParams = results.authParams; - - authParams.hash = keys.pw; - this._keys = keys; - this._hasPasscode = true; - this._authParams = authParams; - - // Encrypting will initially clear localStorage - this.encryptLocalStorage(keys, authParams); - - // After it's cleared, it's safe to write to it - storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed); - callback(true); - }); - } - - this.changePasscode = (newPasscode, callback) => { - this.setPasscode(newPasscode, callback); - } - - this.clearPasscode = function() { - storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral - storageManager.removeItem("offlineParams", StorageManager.Fixed); - this._keys = null; - this._hasPasscode = false; - } - - this.encryptLocalStorage = function(keys, authParams) { - storageManager.setKeys(keys, authParams); - // Switch to Ephemeral storage, wiping Fixed storage - // Last argument is `force`, which we set to true because in the case of changing passcode - storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true); - } - - this.decryptLocalStorage = async function(keys, authParams) { - storageManager.setKeys(keys, authParams); - return storageManager.decryptStorage(); + documentVisibilityChanged(visbility) { + let visible = document.visibilityState == "visible"; + if(!visible) { + this.beginAutoLockTimer(); + } else { + this.cancelAutoLockTimer(); } } -}); + + async beginAutoLockTimer() { + console.log("beginAutoLockTimer"); + var interval = await this.getAutoLockInterval(); + this.lockTimeout = setTimeout(() => { + this.lockApplication(); + }, interval); + } + + cancelAutoLockTimer() { + console.log("cancelAutoLockTimer"); + clearTimeout(this.lockTimeout); + } + + lockApplication() { + console.log("lockApplication"); + window.location.reload(); + this.cancelAutoLockTimer(); + } + + isLocked() { + return this._locked; + } + + hasPasscode() { + return this._hasPasscode; + } + + keys() { + return this._keys; + } + + async setAutoLockInterval(interval) { + console.log("Set autolock interval", interval); + return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.Fixed); + } + + async getAutoLockInterval() { + let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.Fixed); + console.log("Got interval", interval); + return interval && JSON.parse(interval); + } + + passcodeAuthParams() { + var authParams = JSON.parse(this.storageManager.getItemSync("offlineParams", StorageManager.Fixed)); + if(authParams && !authParams.version) { + var keys = this.keys(); + if(keys && keys.ak) { + // If there's no version stored, and there's an ak, it has to be 002. Newer versions would have their version stored in authParams. + authParams.version = "002"; + } else { + authParams.version = "001"; + } + } + return authParams; + } + + async verifyPasscode(passcode) { + return new Promise(async (resolve, reject) => { + var params = this.passcodeAuthParams(); + let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params); + if(keys.pw !== params.hash) { + resolve(false); + } else { + resolve(true); + } + }) + } + + unlock(passcode, callback) { + var params = this.passcodeAuthParams(); + SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => { + if(keys.pw !== params.hash) { + callback(false); + return; + } + + this._keys = keys; + this._authParams = params; + this.decryptLocalStorage(keys, params).then(() => { + this._locked = false; + callback(true); + }) + }); + } + + setPasscode(passcode, callback) { + var uuid = SFJS.crypto.generateUUIDSync(); + + SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => { + let keys = results.keys; + let authParams = results.authParams; + + authParams.hash = keys.pw; + this._keys = keys; + this._hasPasscode = true; + this._authParams = authParams; + + // Encrypting will initially clear localStorage + this.encryptLocalStorage(keys, authParams); + + // After it's cleared, it's safe to write to it + this.storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed); + callback(true); + }); + } + + changePasscode(newPasscode, callback) { + this.setPasscode(newPasscode, callback); + } + + clearPasscode() { + this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral + this.storageManager.removeItem("offlineParams", StorageManager.Fixed); + this._keys = null; + this._hasPasscode = false; + } + + encryptLocalStorage(keys, authParams) { + this.storageManager.setKeys(keys, authParams); + // Switch to Ephemeral storage, wiping Fixed storage + // Last argument is `force`, which we set to true because in the case of changing passcode + this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true); + } + + async decryptLocalStorage(keys, authParams) { + this.storageManager.setKeys(keys, authParams); + return this.storageManager.decryptStorage(); + } +} + +angular.module('app').service('passcodeManager', PasscodeManager); diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index 18dd7c386..8736057e4 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -15,10 +15,14 @@ class PrivilegesManager { PrivilegesManager.ActionManageExtensions = "ActionManageExtensions"; PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup"; + PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes"; + PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges"; this.availableActions = [ PrivilegesManager.ActionManageExtensions, - PrivilegesManager.ActionDownloadBackup + PrivilegesManager.ActionDownloadBackup, + PrivilegesManager.ActionViewLockedNotes, + PrivilegesManager.ActionManagePrivileges ] this.availableCredentials = [ @@ -119,10 +123,19 @@ class PrivilegesManager { metadata[PrivilegesManager.ActionManageExtensions] = { label: "Manage Extensions" } + metadata[PrivilegesManager.ActionDownloadBackup] = { label: "Download Backups" }; + metadata[PrivilegesManager.ActionViewLockedNotes] = { + label: "View Locked Notes" + }; + + metadata[PrivilegesManager.ActionManagePrivileges] = { + label: "Manage Privileges" + } + return metadata[action]; } diff --git a/app/assets/stylesheets/app/_editor.scss b/app/assets/stylesheets/app/_editor.scss index 0d4f3b225..272791b94 100644 --- a/app/assets/stylesheets/app/_editor.scss +++ b/app/assets/stylesheets/app/_editor.scss @@ -1,4 +1,11 @@ $heading-height: 75px; + +#editor-column { + .locked { + opacity: 0.8; + } +} + .editor { flex: 1 50%; display: flex; @@ -7,10 +14,6 @@ $heading-height: 75px; background-color: white; } -.locked { - opacity: 0.8; -} - #editor-title-bar { width: 100%; diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss index ca1dac561..6d6278cbd 100644 --- a/app/assets/stylesheets/app/_modals.scss +++ b/app/assets/stylesheets/app/_modals.scss @@ -12,6 +12,10 @@ } } +#privileges-modal { + width: 700px; +} + #password-wizard { font-size: 16px; } diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml index ca512a6e4..9733c4a59 100644 --- a/app/assets/templates/directives/account-menu.html.haml +++ b/app/assets/templates/directives/account-menu.html.haml @@ -130,7 +130,12 @@ .horizontal-group %a.info{"ng-click" => "changePasscodePressed()"} Change Passcode %a.danger{"ng-click" => "removePasscodePressed()"} Remove Passcode - + .panel-row + .horizontal-group + %p Autolock: + %a.info{"ng-repeat" => "option in passcodeAutoLockOptions", "ng-click" => "selectAutoLockInterval(option.value)", + "ng-class" => "{'info bold' : option.value == selectedAutoLockInterval}"} + {{option.label}} .panel-section{"ng-if" => "!importData.loading"} diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml index 94292cf7e..e4f730e2d 100644 --- a/app/assets/templates/directives/privileges-management-modal.html.haml +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -20,3 +20,14 @@ %p {{displayInfoForAction(action)}} %th{"ng-repeat" => "credential in availableCredentials"} %input{"type" => "checkbox", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"} + .footer + %h2 About Privileges + %p + Privileges represent interface level authentication for accessing certain items and features. + Note that when your application is unlocked, your data exists in temporary memory in an unencrypted state. + Privileges are meant to protect against unwanted access in the event of an unlocked application, but do not affect data encryption state. + + %p + Privileges sync across your other devices (not including mobile); however, note that if you require + a "Local Passcode" privilege, and another device does not have a local passcode set up, the local passcode + requirement will be ignored on that device. diff --git a/app/assets/templates/footer.html.haml b/app/assets/templates/footer.html.haml index 8ba4a11cf..c2a8e5c93 100644 --- a/app/assets/templates/footer.html.haml +++ b/app/assets/templates/footer.html.haml @@ -39,6 +39,6 @@ .item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} .label Refresh - .item#lock-item{"ng-if" => "ctrl.hasPasscode()"} + .item#lock-item{"ng-if" => "ctrl.hasPasscode()", "title" => "Locks application and wipes unencrypted data from memory."} .label %i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"} diff --git a/app/assets/templates/notes.html.haml b/app/assets/templates/notes.html.haml index a1e48dd1b..46f1ab1a0 100644 --- a/app/assets/templates/notes.html.haml +++ b/app/assets/templates/notes.html.haml @@ -59,9 +59,11 @@ .faded {{note.savedTagsString || note.tagsString()}} .name{"ng-if" => "note.title"} + %span.locked.tinted{"ng-if" => "note.locked", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"} + %i.icon.ion-locked.medium-text {{note.title}} - .note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview"} + .note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview && !note.locked"} .html-preview{"ng-if" => "note.content.preview_html", "ng-bind-html" => "note.content.preview_html"} .plain-preview{"ng-if" => "!note.content.preview_html && note.content.preview_plain"} {{note.content.preview_plain}} .default-preview{"ng-if" => "!note.content.preview_html && !note.content.preview_plain"} {{note.text}}