From f9a56036332e936aef0aa0c8efc2a1fa24d29399 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 12 Jul 2017 16:20:13 -0500 Subject: [PATCH] 002 + verification updates --- .../app/frontend/models/local/itemParams.js | 13 +-- .../javascripts/app/services/authManager.js | 89 ++++++++++++++----- .../services/directives/views/accountMenu.js | 69 +++++++++++++- .../{functional => views}/permissionsModal.js | 0 .../app/services/extensionManager.js | 2 +- .../app/services/helpers/crypto.js | 41 ++++----- .../app/services/helpers/cryptojs.js | 26 ++---- .../app/services/helpers/encryptionHelper.js | 46 +++++++--- .../app/services/helpers/webcrypto.js | 39 ++++---- .../javascripts/app/services/httpManager.js | 6 +- .../javascripts/app/services/syncManager.js | 6 +- .../directives/account-menu.html.haml | 55 +++++++++--- .../templates/frontend/editor.html.haml | 8 +- app/assets/templates/frontend/notes.html.haml | 1 + 14 files changed, 280 insertions(+), 121 deletions(-) rename app/assets/javascripts/app/services/directives/{functional => views}/permissionsModal.js (100%) diff --git a/app/assets/javascripts/app/frontend/models/local/itemParams.js b/app/assets/javascripts/app/frontend/models/local/itemParams.js index 61c8b814a..dba1e7806 100644 --- a/app/assets/javascripts/app/frontend/models/local/itemParams.js +++ b/app/assets/javascripts/app/frontend/models/local/itemParams.js @@ -1,8 +1,9 @@ class ItemParams { - constructor(item, keys) { + constructor(item, keys, version) { this.item = item; this.keys = keys; + this.version = version; } paramsForExportFile() { @@ -16,7 +17,7 @@ class ItemParams { } paramsForLocalStorage() { - this.additionalFields = ["updated_at", "dirty"]; + this.additionalFields = ["updated_at", "dirty", "errorDecrypting"]; this.forExportFile = true; return this.__params(); } @@ -26,17 +27,17 @@ class ItemParams { } __params() { - let encryptionVersion = "001"; + + console.log("Encrypting with version: ", this.version); console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy) var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at}; - if(this.keys && !this.item.doNotEncrypt()) { - var encryptedParams = EncryptionHelper.encryptItem(this.item, this.keys, encryptionVersion); + var encryptedParams = EncryptionHelper.encryptItem(this.item, this.keys, this.version); _.merge(params, encryptedParams); - if(encryptionVersion !== "001") { + if(this.version !== "001") { params.auth_hash = null; } } diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index c83b06116..810b3bafa 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -7,11 +7,11 @@ angular.module('app.frontend') return domain; } - this.$get = function($rootScope, httpManager, modelManager, dbManager) { - return new AuthManager($rootScope, httpManager, modelManager, dbManager); + this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager) { + return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager); } - function AuthManager($rootScope, httpManager, modelManager, dbManager) { + function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager) { var userData = localStorage.getItem("user"); if(userData) { @@ -45,17 +45,19 @@ angular.module('app.frontend') if(!mk) { return null; } - var keys = {mk: mk}; - if(!localStorage.getItem("encryptionKey")) { - _.merge(keys, Neeto.crypto.generateKeysFromMasterKey(keys.mk)); - localStorage.setItem("encryptionKey", keys.encryptionKey); - localStorage.setItem("authKey", keys.authKey); - } else { - _.merge(keys, {encryptionKey: localStorage.getItem("encryptionKey"), authKey: localStorage.getItem("authKey")}); - } + var keys = {mk: mk, ak: localStorage.getItem("ak")}; return keys; } + this.encryptionVersion = function() { + var keys = this.keys(); + if(keys && keys.ak) { + return "002"; + } else { + return "001"; + } + } + this.getAuthParamsForEmail = function(url, email, callback) { var requestUrl = url + "/auth/params"; httpManager.getAbsolute(requestUrl, {email: email}, function(response){ @@ -80,7 +82,7 @@ angular.module('app.frontend') this.login = function(url, email, password, callback) { this.getAuthParamsForEmail(url, email, function(authParams){ - if(!authParams) { + if(!authParams.pw_cost) { callback({error : {message: "Unable to get authentication parameters."}}); return; } @@ -94,12 +96,41 @@ angular.module('app.frontend') return; } + Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){ + + var uploadVTagOnCompletion = false; + var localVTag = Neeto.crypto.calculateVerificationTag(authParams.pw_cost, authParams.pw_salt, keys.ak); + + if(authParams.pw_auth) { + // verify auth params + if(localVTag !== authParams.pw_auth) { + alert("Invalid server verification tag; aborting login. Learn more at standardnotes.org/verification."); + $timeout(function(){ + callback({error: true, didDisplayAlert: true}); + }) + return; + } else { + console.log("Verification tag success."); + } + } else { + // either user has not uploaded pw_auth, or server is attempting to bypass authentication + if(confirm("Unable to locate verification tag for server. If this is your first time seeing this message and your account was created before July 2017, press OK to upload verification tag. If your account was created after July 2017, or if you've already seen this message, press cancel to abort login. Learn more at standardnotes.org/verification.")) { + // upload verification tag on completion + uploadVTagOnCompletion = true; + } else { + return; + } + } + var requestUrl = url + "/auth/sign_in"; var params = {password: keys.pw, email: email}; httpManager.postAbsolute(requestUrl, params, function(response){ - this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw); + this.handleAuthResponse(response, email, url, authParams, keys); callback(response); + if(uploadVTagOnCompletion) { + this.uploadVerificationTag(localVTag, authParams); + } }.bind(this), function(response){ console.error("Error logging in", response); callback(response); @@ -109,28 +140,46 @@ angular.module('app.frontend') }.bind(this)) } - this.handleAuthResponse = function(response, email, url, authParams, mk, pw) { + this.uploadVerificationTag = function(tag, authParams) { + var requestUrl = localStorage.getItem("server") + "/auth/update"; + var params = {pw_auth: tag}; + + httpManager.postAbsolute(requestUrl, params, function(response){ + _.merge(authParams, params); + localStorage.setItem("auth_params", JSON.stringify(authParams)); + alert("Your verification tag was successfully uploaded."); + }.bind(this), function(response){ + alert("There was an error uploading your verification tag."); + }) + } + + this.handleAuthResponse = function(response, email, url, authParams, keys) { try { if(url) { localStorage.setItem("server", url); } localStorage.setItem("user", JSON.stringify(response.user)); - localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); - localStorage.setItem("mk", mk); - localStorage.setItem("pw", pw); + localStorage.setItem("auth_params", JSON.stringify(authParams)); localStorage.setItem("jwt", response.token); + this.saveKeys(keys); } catch(e) { dbManager.displayOfflineAlert(); } } + this.saveKeys = function(keys) { + localStorage.setItem("pw", keys.pw); + localStorage.setItem("mk", keys.mk); + localStorage.setItem("ak", keys.ak); + } + this.register = function(url, email, password, callback) { Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){ var requestUrl = url + "/auth"; var params = _.merge({password: keys.pw, email: email}, authParams); httpManager.postAbsolute(requestUrl, params, function(response){ - this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw); + this.handleAuthResponse(response, email, url, authParams, keys); callback(response); }.bind(this), function(response){ console.error("Registration error", response); @@ -144,8 +193,8 @@ angular.module('app.frontend') var requestUrl = localStorage.getItem("server") + "/auth/change_pw"; var params = _.merge({new_password: keys.pw}, authParams); - httpManager.postAbsolute(requestUrl, params, function(response){ - this.handleAuthResponse(response, email, null, authParams, keys.mk, keys.pw); + httpManager.postAbsolute(requestUrl, params, function(response) { + this.handleAuthResponse(response, email, null, authParams, keys); callback(response); }.bind(this), function(response){ var error = response; diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index 168b4f4f0..fde6786e2 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -20,6 +20,10 @@ class AccountMenu { return authManager.keys().mk; } + $scope.authKey = function() { + return authManager.keys().ak; + } + $scope.serverPassword = function() { return syncManager.serverPassword; } @@ -341,7 +345,7 @@ class AccountMenu { $scope.itemsData = function(keys) { var items = _.map(modelManager.allItems, function(item){ - var itemParams = new ItemParams(item, keys); + var itemParams = new ItemParams(item, keys, authManager.encryptionVersion()); return itemParams.paramsForExportFile(); }.bind(this)); @@ -356,6 +360,69 @@ class AccountMenu { return data; } + + + // Advanced + + $scope.reencryptPressed = function() { + if(!confirm("Are you sure you want to re-encrypt and sync all your items? This is useful when updates are made to our encryption specification. You should have been instructed to come here from our website.")) { + return; + } + + if(!confirm("It is highly recommended that you download a backup of your data before proceeding. Press cancel to go back. Note that this procedure can take some time, depending on the number of items you have. Do not close the app during process.")) { + return; + } + + modelManager.setAllItemsDirty(); + syncManager.sync(function(response){ + if(response.error) { + alert("There was an error re-encrypting your items. You should try syncing again. If all else fails, you should restore your notes from backup.") + return; + } + + $timeout(function(){ + alert("Your items have been successfully re-encrypted and synced. You must sign out of all other signed in applications (mobile, desktop, web) and sign in again, or else you may corrupt your data.") + $scope.newPasswordData = {}; + }, 1000) + }); + + } + + + + // 002 Update + + $scope.securityUpdateAvailable = function() { + // whether user needs to upload pw_auth + return !authManager.getAuthParams().pw_auth; + } + + $scope.clickedSecurityUpdate = function() { + if(!$scope.securityUpdateData) { + $scope.securityUpdateData = {}; + } + $scope.securityUpdateData.showForm = true; + } + + $scope.submitSecurityUpdateForm = function() { + $scope.securityUpdateData.processing = true; + var authParams = authManager.getAuthParams(); + + Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: $scope.securityUpdateData.password}, authParams), function(keys){ + if(keys.mk !== authManager.keys().mk) { + alert("Invalid password. Please try again."); + $timeout(function(){ + $scope.securityUpdateData.processing = false; + }) + return; + } + + var tag = Neeto.crypto.calculateVerificationTag(authParams.pw_cost, authParams.pw_salt, keys.ak); + authManager.uploadVerificationTag(tag, authParams); + authManager.saveKeys(keys); + }); + } + } } diff --git a/app/assets/javascripts/app/services/directives/functional/permissionsModal.js b/app/assets/javascripts/app/services/directives/views/permissionsModal.js similarity index 100% rename from app/assets/javascripts/app/services/directives/functional/permissionsModal.js rename to app/assets/javascripts/app/services/directives/views/permissionsModal.js diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index 18ba23e5a..5f2c15f6a 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -319,7 +319,7 @@ class ExtensionManager { if(!this.extensionUsesEncryptedData(extension)) { keys = null; } - var itemParams = new ItemParams(item, keys); + var itemParams = new ItemParams(item, keys, this.authManager.encryptionVersion()); return itemParams.paramsForExtension(); } diff --git a/app/assets/javascripts/app/services/helpers/crypto.js b/app/assets/javascripts/app/services/helpers/crypto.js index 42228ee75..7a7e3b9ad 100644 --- a/app/assets/javascripts/app/services/helpers/crypto.js +++ b/app/assets/javascripts/app/services/helpers/crypto.js @@ -93,35 +93,26 @@ class SNCrypto { return result; } - generateKeysFromMasterKey(mk) { - var encryptionKey = Neeto.crypto.hmac256(mk, CryptoJS.enc.Utf8.parse("e").toString(CryptoJS.enc.Hex)); - var authKey = Neeto.crypto.hmac256(mk, CryptoJS.enc.Utf8.parse("a").toString(CryptoJS.enc.Hex)); - return {encryptionKey: encryptionKey, authKey: authKey}; - } - - computeEncryptionKeysForUser({password, pw_salt, pw_func, pw_alg, pw_cost, pw_key_size} = {}, callback) { - this.generateSymmetricKeyPair({password: password, pw_salt: pw_salt, - pw_func: pw_func, pw_alg: pw_alg, pw_cost: pw_cost, pw_key_size: pw_key_size}, function(keys){ - var pw = keys[0]; - var mk = keys[1]; - - callback(_.merge({pw: pw, mk: mk}, this.generateKeysFromMasterKey(mk))); + computeEncryptionKeysForUser({password, pw_salt, pw_cost} = {}, callback) { + this.generateSymmetricKeyPair({password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){ + callback({pw: keys[0], mk: keys[1], ak: keys[2]}); }.bind(this)); } - generateInitialEncryptionKeysForUser({email, password} = {}, callback) { - var defaults = this.defaultPasswordGenerationParams(); - var {pw_func, pw_alg, pw_key_size, pw_cost} = defaults; - var pw_nonce = this.generateRandomKey(512); - var pw_salt = this.sha1(email + "SN" + pw_nonce); - _.merge(defaults, {pw_salt: pw_salt, pw_nonce: pw_nonce}) - this.generateSymmetricKeyPair(_.merge({email: email, password: password, pw_salt: pw_salt}, defaults), function(keys){ - var pw = keys[0]; - var mk = keys[1]; + calculateVerificationTag(cost, salt, ak) { + return Neeto.crypto.hmac256([cost, salt].join(":"), ak); + } - callback(_.merge({pw: pw, mk: mk}, this.generateKeysFromMasterKey(mk)), defaults); - }.bind(this)); - } + generateInitialEncryptionKeysForUser({email, password} = {}, callback) { + var pw_cost = this.defaultPasswordGenerationCost(); + var pw_nonce = this.generateRandomKey(512); + var pw_salt = this.sha1([email, pw_nonce].join(":")); + this.generateSymmetricKeyPair({email: email, password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){ + var ak = keys[2]; + var pw_auth = this.calculateVerificationTag(pw_cost, pw_salt, ak); + callback({pw: keys[0], mk: keys[1], ak: ak}, {pw_auth: pw_auth, pw_salt: pw_salt, pw_cost: pw_cost}); + }.bind(this)); + } } export { SNCrypto } diff --git a/app/assets/javascripts/app/services/helpers/cryptojs.js b/app/assets/javascripts/app/services/helpers/cryptojs.js index 71017fef3..170c4f82f 100644 --- a/app/assets/javascripts/app/services/helpers/cryptojs.js +++ b/app/assets/javascripts/app/services/helpers/cryptojs.js @@ -1,27 +1,19 @@ class SNCryptoJS extends SNCrypto { /** Generates two deterministic keys based on one input */ - generateSymmetricKeyPair({password, pw_salt, pw_func, pw_alg, pw_cost, pw_key_size} = {}, callback) { - var algMapping = { - "sha256" : CryptoJS.algo.SHA256, - "sha512" : CryptoJS.algo.SHA512 - } - var fnMapping = { - "pbkdf2" : CryptoJS.PBKDF2 - } - - var alg = algMapping[pw_alg]; - var kdf = fnMapping[pw_func]; - var output = kdf(password, pw_salt, { keySize: pw_key_size/32, hasher: alg, iterations: pw_cost }).toString(); + generateSymmetricKeyPair({password, pw_salt, pw_cost} = {}, callback) { + var output = CryptoJS.PBKDF2(password, pw_salt, { keySize: 768/32, hasher: CryptoJS.algo.SHA512, iterations: pw_cost }).toString(); var outputLength = output.length; - var firstHalf = output.slice(0, outputLength/2); - var secondHalf = output.slice(outputLength/2, outputLength); - callback([firstHalf, secondHalf]) + var splitLength = outputLength/3; + var firstThird = output.slice(0, splitLength); + var secondThird = output.slice(splitLength, splitLength * 2); + var thirdThird = output.slice(splitLength * 2, splitLength * 3); + callback([firstThird, secondThird, thirdThird]) } - defaultPasswordGenerationParams() { - return {pw_func: "pbkdf2", pw_alg: "sha512", pw_key_size: 512, pw_cost: 3000}; + defaultPasswordGenerationCost() { + return 3000; } } diff --git a/app/assets/javascripts/app/services/helpers/encryptionHelper.js b/app/assets/javascripts/app/services/helpers/encryptionHelper.js index 88fdb9225..521af1991 100644 --- a/app/assets/javascripts/app/services/helpers/encryptionHelper.js +++ b/app/assets/javascripts/app/services/helpers/encryptionHelper.js @@ -1,6 +1,6 @@ class EncryptionHelper { - static _private_encryptString(string, encryptionKey, authKey, version) { + static _private_encryptString(string, encryptionKey, authKey, uuid, version) { var fullCiphertext, contentCiphertext; if(version === "001") { contentCiphertext = Neeto.crypto.encryptText(string, encryptionKey, null); @@ -8,9 +8,9 @@ class EncryptionHelper { } else { var iv = Neeto.crypto.generateRandomKey(128); contentCiphertext = Neeto.crypto.encryptText(string, encryptionKey, iv); - var ciphertextToAuth = [version, iv, contentCiphertext].join(":"); + var ciphertextToAuth = [version, uuid, iv, contentCiphertext].join(":"); var authHash = Neeto.crypto.hmac256(ciphertextToAuth, authKey); - fullCiphertext = [version, authHash, iv, contentCiphertext].join(":"); + fullCiphertext = [version, authHash, uuid, iv, contentCiphertext].join(":"); } return fullCiphertext; @@ -24,13 +24,13 @@ class EncryptionHelper { // legacy params.enc_item_key = Neeto.crypto.encryptText(item_key, keys.mk, null); } else { - params.enc_item_key = this._private_encryptString(item_key, keys.encryptionKey, keys.authKey, version); + params.enc_item_key = this._private_encryptString(item_key, keys.mk, keys.ak, item.uuid, version); } // encrypt content var ek = Neeto.crypto.firstHalfOfKey(item_key); var ak = Neeto.crypto.secondHalfOfKey(item_key); - var ciphertext = this._private_encryptString(JSON.stringify(item.createContentJSONFromProperties()), ek, ak, version); + var ciphertext = this._private_encryptString(JSON.stringify(item.createContentJSONFromProperties()), ek, ak, item.uuid, version); if(version === "001") { var authHash = Neeto.crypto.hmac256(ciphertext, ak); params.auth_hash = authHash; @@ -40,7 +40,7 @@ class EncryptionHelper { return params; } - static encryptionComponentsFromString(string, baseKey, encryptionKey, authKey) { + static encryptionComponentsFromString(string, encryptionKey, authKey) { var encryptionVersion = string.substring(0, 3); if(encryptionVersion === "001") { return { @@ -49,7 +49,7 @@ class EncryptionHelper { ciphertextToAuth: string, iv: null, authHash: null, - encryptionKey: baseKey, + encryptionKey: encryptionKey, authKey: authKey } } else { @@ -57,9 +57,10 @@ class EncryptionHelper { return { encryptionVersion: components[0], authHash: components[1], - iv: components[2], - contentCiphertext: components[3], - ciphertextToAuth: [components[0], components[2], components[3]].join(":"), + uuid: components[2], + iv: components[3], + contentCiphertext: components[4], + ciphertextToAuth: [components[0], components[2], components[3], components[4]].join(":"), encryptionKey: encryptionKey, authKey: authKey } @@ -75,21 +76,41 @@ class EncryptionHelper { encryptedItemKey = "001" + encryptedItemKey; requiresAuth = false; } - var keyParams = this.encryptionComponentsFromString(encryptedItemKey, keys.mk, keys.encryptionKey, keys.authKey); + var keyParams = this.encryptionComponentsFromString(encryptedItemKey, keys.mk, keys.ak); + + // return if uuid in auth hash does not match item uuid. Signs of tampering. + if(keyParams.uuid && keyParams.uuid !== item.uuid) { + item.errorDecrypting = true; + return; + } + var item_key = Neeto.crypto.decryptText(keyParams, requiresAuth); if(!item_key) { + item.errorDecrypting = true; return; } // decrypt content var ek = Neeto.crypto.firstHalfOfKey(item_key); var ak = Neeto.crypto.secondHalfOfKey(item_key); - var itemParams = this.encryptionComponentsFromString(item.content, ek, ek, ak); + var itemParams = this.encryptionComponentsFromString(item.content, ek, ak); + + // return if uuid in auth hash does not match item uuid. Signs of tampering. + if(itemParams.uuid && itemParams.uuid !== item.uuid) { + item.errorDecrypting = true; + return; + } + if(!itemParams.authHash) { + // legacy 001 itemParams.authHash = item.auth_hash; } + var content = Neeto.crypto.decryptText(itemParams, true); + if(!content) { + item.errorDecrypting = true; + } item.content = content; } @@ -110,6 +131,7 @@ class EncryptionHelper { item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)) } } catch (e) { + item.errorDecrypting = true; if(throws) { throw e; } diff --git a/app/assets/javascripts/app/services/helpers/webcrypto.js b/app/assets/javascripts/app/services/helpers/webcrypto.js index 50f6ac28e..2695ae50a 100644 --- a/app/assets/javascripts/app/services/helpers/webcrypto.js +++ b/app/assets/javascripts/app/services/helpers/webcrypto.js @@ -5,17 +5,19 @@ class SNCryptoWeb extends SNCrypto { /** Overrides */ - defaultPasswordGenerationParams() { - return {pw_func: "pbkdf2", pw_alg: "sha512", pw_key_size: 512, pw_cost: 5000}; + defaultPasswordGenerationCost() { + return 10000; } /** Generates two deterministic keys based on one input */ - generateSymmetricKeyPair({password, pw_salt, pw_func, pw_alg, pw_cost, pw_key_size} = {}, callback) { - this.stretchPassword({password: password, pw_func: pw_func, pw_alg: pw_alg, pw_salt: pw_salt, pw_cost: pw_cost, pw_key_size: pw_key_size}, function(output){ + generateSymmetricKeyPair({password, pw_salt, pw_cost} = {}, callback) { + this.stretchPassword({password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(output){ var outputLength = output.length; - var firstHalf = output.slice(0, outputLength/2); - var secondHalf = output.slice(outputLength/2, outputLength); - callback([firstHalf, secondHalf]); + var splitLength = outputLength/3; + var firstThird = output.slice(0, splitLength); + var secondThird = output.slice(splitLength, splitLength * 2); + var thirdThird = output.slice(splitLength * 2, splitLength * 3); + callback([firstThird, secondThird, thirdThird]) }) } @@ -23,9 +25,9 @@ class SNCryptoWeb extends SNCrypto { Internal */ - stretchPassword({password, pw_salt, pw_cost, pw_func, pw_alg, pw_key_size} = {}, callback) { + stretchPassword({password, pw_salt, pw_cost} = {}, callback) { - this.webCryptoImportKey(password, pw_func, function(key){ + this.webCryptoImportKey(password, function(key){ if(!key) { console.log("Key is null, unable to continue"); @@ -33,7 +35,7 @@ class SNCryptoWeb extends SNCrypto { return; } - this.webCryptoDeriveBits({key: key, pw_func: pw_func, pw_alg: pw_alg, pw_salt: pw_salt, pw_cost: pw_cost, pw_key_size: pw_key_size}, function(key){ + this.webCryptoDeriveBits({key: key, pw_salt: pw_salt, pw_cost: pw_cost}, function(key){ if(!key) { callback(null); return; @@ -45,11 +47,11 @@ class SNCryptoWeb extends SNCrypto { }.bind(this)) } - webCryptoImportKey(input, pw_func, callback) { + webCryptoImportKey(input, callback) { subtleCrypto.importKey( "raw", this.stringToArrayBuffer(input), - {name: pw_func.toUpperCase()}, + {name: "PBKDF2"}, false, ["deriveBits"] ) @@ -62,21 +64,16 @@ class SNCryptoWeb extends SNCrypto { }); } - webCryptoDeriveBits({key, pw_func, pw_alg, pw_salt, pw_cost, pw_key_size} = {}, callback) { - var algMapping = { - "sha256" : "SHA-256", - "sha512" : "SHA-512", - } - var alg = algMapping[pw_alg]; + webCryptoDeriveBits({key, pw_salt, pw_cost} = {}, callback) { subtleCrypto.deriveBits( { - "name": pw_func.toUpperCase(), + "name": "PBKDF2", salt: this.stringToArrayBuffer(pw_salt), iterations: pw_cost, - hash: {name: alg}, + hash: {name: "SHA-512"}, }, key, - pw_key_size + 768 ) .then(function(bits){ var key = this.arrayBufferToHexString(new Uint8Array(bits)); diff --git a/app/assets/javascripts/app/services/httpManager.js b/app/assets/javascripts/app/services/httpManager.js index a6a63dad2..b0c06ff2a 100644 --- a/app/assets/javascripts/app/services/httpManager.js +++ b/app/assets/javascripts/app/services/httpManager.js @@ -16,6 +16,10 @@ class HttpManager { this.httpRequest("post", url, params, onsuccess, onerror); } + patchAbsolute(url, params, onsuccess, onerror) { + this.httpRequest("patch", url, params, onsuccess, onerror); + } + getAbsolute(url, params, onsuccess, onerror) { this.httpRequest("get", url, params, onsuccess, onerror); } @@ -54,7 +58,7 @@ class HttpManager { this.setAuthHeadersForRequest(xmlhttp); xmlhttp.setRequestHeader('Content-type', 'application/json'); - if(verb == "post") { + if(verb == "post" || verb == "patch") { xmlhttp.send(JSON.stringify(params)); } else { xmlhttp.send(); diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 4389350e5..1a58dd348 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -24,7 +24,7 @@ class SyncManager { writeItemsToLocalStorage(items, offlineOnly, callback) { var params = items.map(function(item) { - var itemParams = new ItemParams(item, null); + var itemParams = new ItemParams(item, null, this.authManager.encryptionVersion()); itemParams = itemParams.paramsForLocalStorage(); if(offlineOnly) { delete itemParams.dirty; @@ -142,7 +142,7 @@ class SyncManager { sync(callback, options = {}) { var allDirtyItems = this.modelManager.getDirtyItems(); - + if(this.syncStatus.syncOpInProgress) { this.repeatOnCompletion = true; if(callback) { @@ -189,7 +189,7 @@ class SyncManager { var params = {}; params.limit = 150; params.items = _.map(subItems, function(item){ - var itemParams = new ItemParams(item, this.authManager.keys()); + var itemParams = new ItemParams(item, this.authManager.keys(), this.authManager.encryptionVersion()); itemParams.additionalFields = options.additionalFields; return itemParams.paramsForSync(); }.bind(this)); diff --git a/app/assets/templates/frontend/directives/account-menu.html.haml b/app/assets/templates/frontend/directives/account-menu.html.haml index 3b11e3609..b99849a54 100644 --- a/app/assets/templates/frontend/directives/account-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-menu.html.haml @@ -54,15 +54,7 @@ %span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}} %p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}} - %a.block.mt-15{"href" => "{{dashboardURL()}}", "target" => "_blank"} Data Dashboard - %a.block.mt-5{"ng-click" => "showCredentials = !showCredentials"} Show Credentials - %section.gray-bg.mt-10.medium-padding{"ng-if" => "showCredentials"} - %label.block - Encryption key: - .wrap.normal.mt-1.selectable {{encryptionKey()}} - %label.block.mt-5.mb-0 - Server password: - .wrap.normal.mt-1.selectable {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}} + %a.block.mt-5{"ng-click" => "newPasswordData.changePassword = !newPasswordData.changePassword"} Change Password %section.gray-bg.mt-10.medium-padding{"ng-if" => "newPasswordData.changePassword"} @@ -81,6 +73,45 @@ %button.ui-button.block{"ng-click" => "submitPasswordChange()"} Submit %p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}} + + + + %a.block.mt-5{"ng-click" => "showAdvanced = !showAdvanced"} Advanced + %div{"ng-if" => "showAdvanced"} + %a.block.mt-15{"href" => "{{dashboardURL()}}", "target" => "_blank"} Data Dashboard + %a.block.mt-5{"ng-click" => "reencryptPressed()"} Re-encrypt All Items + %a.block.mt-5{"ng-click" => "showCredentials = !showCredentials"} Show Credentials + %section.gray-bg.mt-10.medium-padding{"ng-if" => "showCredentials"} + %label.block + Encryption key: + .wrap.normal.mt-1.selectable {{encryptionKey()}} + %label.block.mt-5.mb-0 + Server password: + .wrap.normal.mt-1.selectable {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}} + %label.block.mt-5.mb-0 + Authentication key: + .wrap.normal.mt-1.selectable {{authKey() ? authKey() : 'Not available. Sign out then sign back in to compute.'}} + + + %div{"ng-if" => "securityUpdateAvailable()"} + %a.block.mt-5{"ng-click" => "clickedSecurityUpdate()"} Security Update Available + %section.gray-bg.mt-10.medium-padding{"ng-if" => "securityUpdateData.showForm"} + %p + A new security feature is available that adds an additional level of verification to your sign-ins. + This feature assures that when you login, the server cannot tamper or modify your authentication parameters. + %a{"href" => "https://standardnotes.org/verification", "target" => "_blank"} Learn more. + %div.mt-10{"ng-if" => "!securityUpdateData.processing"} + %p.bold Enter your password to update: + %form.mt-5 + %input.form-control{:type => 'password', "ng-model" => "securityUpdateData.password", "placeholder" => "Enter password"} + %button.ui-button.block{"ng-click" => "submitSecurityUpdateForm()"} Update + %div.mt-5{"ng-if" => "securityUpdateData.processing"} + %p.blue Processing... + + + + + .medium-v-space %h4 Local Encryption @@ -106,9 +137,9 @@ .fake-link Import Data from Archive %div{"ng-if" => "importData.requestPassword"} - Enter the account password associated with the import file. - %input{:type => 'password', "ng-model" => "importData.password"} - %button{"ng-click" => "submitImportPassword()"} Decrypt & Import + %p Enter the account password associated with the import file. + %input.form-control.mt-5{:type => 'password', "ng-model" => "importData.password"} + %button.standard.ui-button.block.blue.mt-5{"ng-click" => "submitImportPassword()"} Decrypt & Import %p.mt-5{"ng-if" => "user"} Notes are downloaded in the Standard File format, which allows you to re-import back into this app easily. To download as plain text files, choose "Decrypted". diff --git a/app/assets/templates/frontend/editor.html.haml b/app/assets/templates/frontend/editor.html.haml index d4ac75b73..5fa5e4d77 100644 --- a/app/assets/templates/frontend/editor.html.haml +++ b/app/assets/templates/frontend/editor.html.haml @@ -1,5 +1,5 @@ .section.editor{"ng-class" => "{'fullscreen' : ctrl.fullscreen}"} - #editor-title-bar.section-title-bar{"ng-if" => "ctrl.note", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"} + #editor-title-bar.section-title-bar{"ng-if" => "ctrl.note && !ctrl.note.errorDecrypting", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"} .title %input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)", "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()", @@ -32,12 +32,16 @@ %label{"ng-click" => "ctrl.showExtensions = !ctrl.showExtensions; ctrl.showMenu = false; ctrl.showEditorMenu = false;"} Actions %contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"} - .editor-content{"ng-if" => "ctrl.noteReady", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"} + .editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"} %iframe#editor-iframe{"ng-if" => "ctrl.editor && !ctrl.editor.systemEditor", "ng-src" => "{{ctrl.editor.url | trusted}}", "frameBorder" => "0", "style" => "width: 100%;"} Loading %textarea.editable#note-text-editor{"ng-if" => "!ctrl.editor || ctrl.editor.systemEditor", "ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-model" => "ctrl.note.text", "ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()"} + + %section.section{"ng-if" => "ctrl.note.errorDecrypting"} + %p.medium-padding{"style" => "padding-top: 0 !important;"} There was an error decrypting this item. Ensure you are running the latest version of this app, then sign out and sign back in to try again. + #editor-pane-component-stack .component.component-stack-border{"ng-repeat" => "component in ctrl.componentStack", "ng-if" => "component.active", "ng-show" => "!component.ignoreEvents", "id" => "{{'component-' + component.uuid}}", "ng-mouseover" => "component.showExit = true", "ng-mouseleave" => "component.showExit = false"} .exit-button.body-text-color{"ng-if" => "component.showExit", "ng-click" => "ctrl.disableComponent(component)"} × diff --git a/app/assets/templates/frontend/notes.html.haml b/app/assets/templates/frontend/notes.html.haml index 689a34bad..3ee469702 100644 --- a/app/assets/templates/frontend/notes.html.haml +++ b/app/assets/templates/frontend/notes.html.haml @@ -25,6 +25,7 @@ .note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | orderBy: ctrl.sortBy:true | limitTo:ctrl.notesToDisplay))", "ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"} %strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy + %strong.red.medium{"ng-if" => "note.errorDecrypting"} Error decrypting .name{"ng-if" => "note.title"} {{note.title}} .note-preview