From 2024233d69088e9c2e290c51d04a082ac6e58ab8 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 27 May 2018 11:04:14 -0500 Subject: [PATCH] Actions handle error decrypting --- .../javascripts/app/controllers/footer.js | 261 +++++++++--------- .../app/directives/views/inputModal.js | 38 +++ .../app/services/actionsManager.js | 107 +++++-- .../directives/actions-menu.html.haml | 4 +- .../directives/input-modal.html.haml | 18 ++ .../directives/password-wizard.html.haml | 2 +- app/assets/templates/footer.html.haml | 2 +- 7 files changed, 270 insertions(+), 162 deletions(-) create mode 100644 app/assets/javascripts/app/directives/views/inputModal.js create mode 100644 app/assets/templates/directives/input-modal.html.haml diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js index aa8366524..d1fba2adf 100644 --- a/app/assets/javascripts/app/controllers/footer.js +++ b/app/assets/javascripts/app/controllers/footer.js @@ -25,154 +25,145 @@ angular.module('app') .controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager) { - this.securityUpdateAvailable = authManager.securityUpdateAvailable; - this.openSecurityUpdate = function() { - authManager.presentPasswordWizard("upgrade-security"); - } - - $rootScope.$on("reload-ext-data", () => { - if(this.reloadInProgress) { return; } - this.reloadInProgress = true; - - // A reload occurs when the extensions manager window is opened. We can close it after a delay - let extWindow = this.rooms.find((room) => {return room.package_info.identifier == nativeExtManager.extensionsManagerIdentifier}); - if(!extWindow) { - return; + this.securityUpdateAvailable = authManager.securityUpdateAvailable; + this.openSecurityUpdate = function() { + authManager.presentPasswordWizard("upgrade-security"); } - this.selectRoom(extWindow); + $rootScope.$on("reload-ext-data", () => { + if(this.reloadInProgress) { return; } + this.reloadInProgress = true; - $timeout(() => { - this.selectRoom(extWindow); - this.reloadInProgress = false; - $rootScope.$broadcast("ext-reload-complete"); - }, 2000) - }); - - this.getUser = function() { - return authManager.user; - } - - this.updateOfflineStatus = function() { - this.offline = authManager.offline(); - } - this.updateOfflineStatus(); - - if(this.offline && !passcodeManager.hasPasscode()) { - this.showAccountMenu = true; - } - - this.findErrors = function() { - this.error = syncManager.syncStatus.error; - } - this.findErrors(); - - this.onAuthSuccess = function() { - this.showAccountMenu = false; - }.bind(this) - - this.accountMenuPressed = function() { - this.showAccountMenu = !this.showAccountMenu; - this.closeAllRooms(); - } - - this.closeAccountMenu = () => { - this.showAccountMenu = false; - } - - this.hasPasscode = function() { - return passcodeManager.hasPasscode(); - } - - this.lockApp = function() { - $rootScope.lockApplication(); - } - - this.refreshData = function() { - this.isRefreshing = true; - syncManager.sync((response) => { - $timeout(function(){ - this.isRefreshing = false; - }.bind(this), 200) - if(response && response.error) { - alert("There was an error syncing. Please try again. If all else fails, log out and log back in."); - } else { - this.syncUpdated(); + // A reload occurs when the extensions manager window is opened. We can close it after a delay + let extWindow = this.rooms.find((room) => {return room.package_info.identifier == nativeExtManager.extensionsManagerIdentifier}); + if(!extWindow) { + return; } - }, {force: true}, "refreshData"); - } - this.syncUpdated = function() { - this.lastSyncDate = new Date(); - } + this.selectRoom(extWindow); - $rootScope.$on("new-update-available", function(version){ - $timeout(function(){ - // timeout calls apply() which is needed - this.onNewUpdateAvailable(); - }.bind(this)) - }.bind(this)) + $timeout(() => { + this.selectRoom(extWindow); + this.reloadInProgress = false; + $rootScope.$broadcast("ext-reload-complete"); + }, 2000) + }); - this.onNewUpdateAvailable = function() { - this.newUpdateAvailable = true; - } - - this.clickedNewUpdateAnnouncement = function() { - this.newUpdateAvailable = false; - alert("A new update is ready to install. Please use the top-level 'Updates' menu to manage installation.") - } - - - /* Rooms */ - - this.componentManager = componentManager; - this.rooms = []; - - modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => { - var incomingRooms = allItems.filter((candidate) => {return candidate.area == "rooms"}); - this.rooms = _.uniq(this.rooms.concat(incomingRooms)).filter((candidate) => {return !candidate.deleted}); - }); - - componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => { - // RIP: There used to be code here that checked if component.active was true, and if so, displayed the component. - // However, we no longer want to persist active state for footer extensions. If you open Extensions on one computer, - // it shouldn't open on another computer. Active state should only be persisted for persistent extensions, like Folders. - }, actionHandler: (component, action, data) => { - if(action == "set-size") { - component.setLastSize(data); + this.getUser = function() { + return authManager.user; } - }, focusHandler: (component, focused) => { - if(component.isEditor() && focused) { + + this.updateOfflineStatus = function() { + this.offline = authManager.offline(); + } + this.updateOfflineStatus(); + + if(this.offline && !passcodeManager.hasPasscode()) { + this.showAccountMenu = true; + } + + this.findErrors = function() { + this.error = syncManager.syncStatus.error; + } + this.findErrors(); + + this.onAuthSuccess = function() { + this.showAccountMenu = false; + }.bind(this) + + this.accountMenuPressed = function() { + this.showAccountMenu = !this.showAccountMenu; + this.closeAllRooms(); + } + + this.closeAccountMenu = () => { + this.showAccountMenu = false; + } + + this.hasPasscode = function() { + return passcodeManager.hasPasscode(); + } + + this.lockApp = function() { + $rootScope.lockApplication(); + } + + this.refreshData = function() { + this.isRefreshing = true; + syncManager.sync((response) => { + $timeout(function(){ + this.isRefreshing = false; + }.bind(this), 200) + if(response && response.error) { + alert("There was an error syncing. Please try again. If all else fails, log out and log back in."); + } else { + this.syncUpdated(); + } + }, {force: true}, "refreshData"); + } + + this.syncUpdated = function() { + this.lastSyncDate = new Date(); + } + + $rootScope.$on("new-update-available", function(version){ + $timeout(function(){ + // timeout calls apply() which is needed + this.onNewUpdateAvailable(); + }.bind(this)) + }.bind(this)) + + this.onNewUpdateAvailable = function() { + this.newUpdateAvailable = true; + } + + this.clickedNewUpdateAnnouncement = function() { + this.newUpdateAvailable = false; + alert("A new update is ready to install. Please use the top-level 'Updates' menu to manage installation.") + } + + + /* Rooms */ + + this.componentManager = componentManager; + this.rooms = []; + + modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => { + var incomingRooms = allItems.filter((candidate) => {return candidate.area == "rooms"}); + this.rooms = _.uniq(this.rooms.concat(incomingRooms)).filter((candidate) => {return !candidate.deleted}); + }); + + componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => { + // RIP: There used to be code here that checked if component.active was true, and if so, displayed the component. + // However, we no longer want to persist active state for footer extensions. If you open Extensions on one computer, + // it shouldn't open on another computer. Active state should only be persisted for persistent extensions, like Folders. + }, actionHandler: (component, action, data) => { + if(action == "set-size") { + component.setLastSize(data); + } + }, focusHandler: (component, focused) => { + if(component.isEditor() && focused) { + this.closeAllRooms(); + this.closeAccountMenu(); + } + }}); + + $rootScope.$on("editorFocused", () => { this.closeAllRooms(); this.closeAccountMenu(); - } - }}); + }) - $rootScope.$on("editorFocused", () => { - this.closeAllRooms(); - this.closeAccountMenu(); - }) - - this.onRoomDismiss = function(room) { - room.showRoom = false; - } - - this.closeAllRooms = function() { - for(var room of this.rooms) { + this.onRoomDismiss = function(room) { room.showRoom = false; } - } - - this.selectRoom = function(room) { - room.showRoom = !room.showRoom; - } - - // 002 Update - - this.securityUpdateAvailable = function() { - var keys = authManager.keys() - return keys && !keys.ak; - } + this.closeAllRooms = function() { + for(var room of this.rooms) { + room.showRoom = false; + } + } + this.selectRoom = function(room) { + room.showRoom = !room.showRoom; + } }); diff --git a/app/assets/javascripts/app/directives/views/inputModal.js b/app/assets/javascripts/app/directives/views/inputModal.js new file mode 100644 index 000000000..59002ae01 --- /dev/null +++ b/app/assets/javascripts/app/directives/views/inputModal.js @@ -0,0 +1,38 @@ +class InputModal { + + constructor() { + this.restrict = "E"; + this.templateUrl = "directives/input-modal.html"; + this.scope = { + type: "=", + title: "=", + message: "=", + placeholder: "=", + callback: "&" + }; + } + + link($scope, el, attrs) { + $scope.el = el; + } + + controller($scope, modelManager, archiveManager, authManager, syncManager, $timeout) { + 'ngInject'; + + $scope.formData = {}; + + $scope.dismiss = function() { + $scope.el.remove(); + $scope.$destroy(); + } + + $scope.submit = function() { + $scope.callback()($scope.formData.input); + $scope.dismiss(); + } + + + } +} + +angular.module('app').directive('inputModal', () => new InputModal); diff --git a/app/assets/javascripts/app/services/actionsManager.js b/app/assets/javascripts/app/services/actionsManager.js index 176d4883d..21c8739a3 100644 --- a/app/assets/javascripts/app/services/actionsManager.js +++ b/app/assets/javascripts/app/services/actionsManager.js @@ -1,10 +1,16 @@ class ActionsManager { - constructor(httpManager, modelManager, authManager, syncManager) { - this.httpManager = httpManager; - this.modelManager = modelManager; - this.authManager = authManager; - this.syncManager = syncManager; + constructor(httpManager, modelManager, authManager, syncManager, $rootScope, $compile, $timeout) { + this.httpManager = httpManager; + this.modelManager = modelManager; + this.authManager = authManager; + this.syncManager = syncManager; + this.$rootScope = $rootScope; + this.$compile = $compile; + this.$timeout = $timeout; + + // Used when decrypting old items with new keys. This array is only kept in memory. + this.previousPasswords = []; } get extensions() { @@ -46,36 +52,83 @@ class ActionsManager { } } - executeAction(action, extension, item, callback) { + async executeAction(action, extension, item, callback) { - var customCallback = function(response) { + var customCallback = (response) => { action.running = false; - callback(response); + this.$timeout(() => { + callback(response); + }) } action.running = true; let decrypted = action.access_type == "decrypted"; + var triedPasswords = []; + + let handleResponseDecryption = async (response, keys, merge) => { + var item = response.item; + + await SFJS.itemTransformer.decryptItem(item, keys); + + if(!item.errorDecrypting) { + if(merge) { + var items = this.modelManager.mapResponseItemsToLocalModels([item], ModelManager.MappingSourceRemoteActionRetrieved); + for(var mappedItem of items) { + mappedItem.setDirty(true); + } + this.syncManager.sync(null); + customCallback({item: item}); + } else { + item = this.modelManager.createItem(item, true /* Dont notify observers */); + customCallback({item: item}); + } + return true; + } else { + // Error decrypting + if(!response.auth_params) { + // In some cases revisions were missing auth params. Instruct the user to email us to get this remedied. + alert("We were unable to decrypt this revision using your current keys, and this revision is missing metadata that would allow us to try different keys to decrypt it. This can likely be fixed with some manual intervention. Please email hello@standardnotes.org for assistance."); + return; + } + + // Try previous passwords + for(let passwordCandidate of this.previousPasswords) { + if(triedPasswords.includes(passwordCandidate)) { + continue; + } + triedPasswords.push(passwordCandidate); + + var keyResults = await SFJS.crypto.computeEncryptionKeysForUser(passwordCandidate, response.auth_params); + if(!keyResults) { + continue; + } + + var success = await handleResponseDecryption(response, keyResults, merge); + if(success) { + return true; + } + } + + this.presentPasswordModal((password) => { + this.previousPasswords.push(password); + handleResponseDecryption(response, keys, merge); + }); + + return false; + } + } + switch (action.verb) { case "get": { - this.httpManager.getAbsolute(action.url, {}, (response) => { action.error = false; - var items = response.items || [response.item]; - SFJS.itemTransformer.decryptMultipleItems(items, this.authManager.keys()).then(() => { - items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved); - for(var item of items) { - item.setDirty(true); - } - this.syncManager.sync(null); - customCallback({items: items}); - }) + handleResponseDecryption(response, this.authManager.keys(), true); }, (response) => { action.error = true; customCallback(null); }) - break; } @@ -83,10 +136,7 @@ class ActionsManager { this.httpManager.getAbsolute(action.url, {}, (response) => { action.error = false; - SFJS.itemTransformer.decryptItem(response.item, this.authManager.keys()).then(() => { - var item = this.modelManager.createItem(response.item, true /* Dont notify observers */); - customCallback({item: item}); - }) + handleResponseDecryption(response, this.authManager.keys(), false); }, (response) => { action.error = true; customCallback(null); @@ -148,6 +198,17 @@ class ActionsManager { }) } + presentPasswordModal(callback) { + + var scope = this.$rootScope.$new(true); + scope.type = "password"; + scope.title = "Decryption Assistance"; + scope.message = "Unable to decrypt this item with your current keys. Please enter your account password at the time of this revision."; + scope.callback = callback; + var el = this.$compile( "" )(scope); + angular.element(document.body).append(el); + } + } angular.module('app').service('actionsManager', ActionsManager); diff --git a/app/assets/templates/directives/actions-menu.html.haml b/app/assets/templates/directives/actions-menu.html.haml index 67f700294..166d479c9 100644 --- a/app/assets/templates/directives/actions-menu.html.haml +++ b/app/assets/templates/directives/actions-menu.html.haml @@ -20,7 +20,7 @@ access to this note. -.modal.medium-text{"ng-if" => "renderData.showRenderModal", "ng-click" => "$event.stopPropagation();"} +.modal.medium-text.medium{"ng-if" => "renderData.showRenderModal", "ng-click" => "$event.stopPropagation();"} .content .sn-component .panel @@ -29,4 +29,4 @@ %a.close-button.info{"ng-click" => "renderData.showRenderModal = false; $event.stopPropagation();"} Close .content.selectable %h2 {{renderData.title}} - %p.normal{"style" => "white-space: pre-wrap; font-family: monospace; font-size: 16px;"} {{renderData.text}} + %p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{renderData.text}} diff --git a/app/assets/templates/directives/input-modal.html.haml b/app/assets/templates/directives/input-modal.html.haml new file mode 100644 index 000000000..706e6ac97 --- /dev/null +++ b/app/assets/templates/directives/input-modal.html.haml @@ -0,0 +1,18 @@ +.modal.small.auto-height + .content + .sn-component + .panel + .header + %h1.title {{title}} + %a.close-button{"ng-click" => "dismiss()"} Close + .content + .panel-section + %p.panel-row {{message}} + .panel-row + .panel-column.stretch + %form{"ng-submit" => "submit()"} + %input.form-control{:type => '{{type}}', "ng-model" => "formData.input", "placeholder" => "{{placeholder}}", "sn-autofocus" => "true", "should-focus" => "true"} + + .footer + %a.right{"ng-click" => "submit()"} + Submit diff --git a/app/assets/templates/directives/password-wizard.html.haml b/app/assets/templates/directives/password-wizard.html.haml index 379d81d80..6ac3f3ee6 100644 --- a/app/assets/templates/directives/password-wizard.html.haml +++ b/app/assets/templates/directives/password-wizard.html.haml @@ -48,7 +48,7 @@ %div{"ng-if" => "step == 2"} %p.panel-row As a result of this process, your encryption keys will change. - Any devices on which you use Standard Notes will need to end their session. After this process completes, you'll be asked to sign back in. + Any devices on which you use Standard Notes will need to end their session. After this process completes, you will be asked to sign back in. %p.bold.panel-row.info-i Please sign out of all applications (excluding this one), including: %ul diff --git a/app/assets/templates/footer.html.haml b/app/assets/templates/footer.html.haml index f6e5f8434..c4dfd0171 100644 --- a/app/assets/templates/footer.html.haml +++ b/app/assets/templates/footer.html.haml @@ -22,7 +22,7 @@ .right - .item{"ng-if" => "ctrl.securityUpdateAvailable == true", "ng-click" => "ctrl.openSecurityUpdate()"} + .item{"ng-if" => "ctrl.securityUpdateAvailable", "ng-click" => "ctrl.openSecurityUpdate()"} %span.success.label Security update available. .item{"ng-if" => "ctrl.newUpdateAvailable == true", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}