From 2a8bc1a0b4b36f6a4c6cbb39bad84b63038f69a8 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 9 Mar 2017 17:39:54 -0600 Subject: [PATCH] 002 encryption type with ivs and auth hash --- .../app/frontend/models/local/itemParams.js | 8 +- .../javascripts/app/services/authManager.js | 12 +++ .../services/directives/views/accountMenu.js | 17 ++- .../app/services/extensionManager.js | 10 +- .../app/services/helpers/crypto.js | 45 +++++--- .../app/services/helpers/encryptionHelper.js | 100 ++++++++++++++---- .../javascripts/app/services/syncManager.js | 4 +- 7 files changed, 140 insertions(+), 56 deletions(-) diff --git a/app/assets/javascripts/app/frontend/models/local/itemParams.js b/app/assets/javascripts/app/frontend/models/local/itemParams.js index 396a51bd1..b1f571fc9 100644 --- a/app/assets/javascripts/app/frontend/models/local/itemParams.js +++ b/app/assets/javascripts/app/frontend/models/local/itemParams.js @@ -1,8 +1,8 @@ class ItemParams { - constructor(item, ek) { + constructor(item, keys) { this.item = item; - this.ek = ek; + this.keys = keys; } paramsForExportFile() { @@ -32,8 +32,8 @@ class ItemParams { var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at}; - if(this.ek) { - EncryptionHelper.encryptItem(itemCopy, this.ek); + if(this.keys) { + EncryptionHelper.encryptItem(itemCopy, this.keys, "002"); params.content = itemCopy.content; params.enc_item_key = itemCopy.enc_item_key; params.auth_hash = itemCopy.auth_hash; diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index 96f001166..120e8ef6a 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -32,6 +32,18 @@ angular.module('app.frontend') return JSON.parse(localStorage.getItem("auth_params")); } + this.keys = function() { + var keys = {mk: localStorage.getItem("mk")}; + if(!localStorage.getItem("encryptionKey")) { + keys = _.merge(keys, Neeto.crypto.generateKeysFromMasterKey(keys.mk)); + localStorage.setItem("encryptionKey", keys.encryptionKey); + localStorage.setItem("authKey", keys.authKey); + } else { + keys = _.merge(keys, {encryptionKey: localStorage.getItem("encryptionKey"), authKey: localStorage.getItem("authKey")}); + } + return keys; + } + this.getAuthParamsForEmail = function(url, email, callback) { var requestUrl = url + "/auth/params"; httpManager.getAbsolute(requestUrl, {email: email}, function(response){ diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index 7fd9ca4c1..03f63e9cd 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -16,7 +16,7 @@ class AccountMenu { $scope.syncStatus = syncManager.syncStatus; $scope.encryptionKey = function() { - return syncManager.masterKey; + return authManager.keys().mk; } $scope.serverPassword = function() { @@ -223,9 +223,8 @@ class AccountMenu { if(data.auth_params) { Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){ - var mk = keys.mk; try { - EncryptionHelper.decryptMultipleItems(data.items, mk, true); + EncryptionHelper.decryptMultipleItems(data.items, keys, true); // delete items enc_item_key since the user's actually key will do the encrypting once its passed off data.items.forEach(function(item){ item.enc_item_key = null; @@ -319,26 +318,26 @@ class AccountMenu { $scope.downloadDataArchive = function() { // download in Standard File format - var ek = $scope.archiveFormData.encrypted ? syncManager.masterKey : null; - var data = $scope.itemsData(ek); + var keys = $scope.archiveFormData.encrypted ? authManager.keys() : null; + var data = $scope.itemsData(keys); downloadData(data, `SN Archive - ${new Date()}.txt`); // download as zipped plain text files - if(!ek) { + if(!keys) { var notes = modelManager.allItemsMatchingTypes(["Note"]); downloadZippedNotes(notes); } } - $scope.itemsData = function(ek) { + $scope.itemsData = function(keys) { var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){ - var itemParams = new ItemParams(item, ek); + var itemParams = new ItemParams(item, keys); return itemParams.paramsForExportFile(); }.bind(this)); var data = {items: items} - if(ek) { + if(keys) { // auth params are only needed when encrypted with a standard file key data["auth_params"] = authManager.getAuthParams(); } diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index 4be7548b3..6bc189217 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -153,7 +153,7 @@ class ExtensionManager { this.httpManager.getAbsolute(action.url, {}, function(response){ action.error = false; var items = response.items || [response.item]; - EncryptionHelper.decryptMultipleItems(items, localStorage.getItem("mk")); + EncryptionHelper.decryptMultipleItems(items, this.authManager.keys()); items = this.modelManager.mapResponseItemsToLocalModels(items); for(var item of items) { item.setDirty(true); @@ -172,7 +172,7 @@ class ExtensionManager { this.httpManager.getAbsolute(action.url, {}, function(response){ action.error = false; - EncryptionHelper.decryptItem(response.item, localStorage.getItem("mk")); + EncryptionHelper.decryptItem(response.item, this.authManager.keys()); var item = this.modelManager.createItem(response.item); customCallback({item: item}); @@ -301,11 +301,11 @@ class ExtensionManager { } outgoingParamsForItem(item, extension) { - var ek = this.syncManager.masterKey; + var keys = this.authManager.keys(); if(!this.extensionUsesEncryptedData(extension)) { - ek = null; + keys = null; } - var itemParams = new ItemParams(item, ek); + var itemParams = new ItemParams(item, keys); return itemParams.paramsForExtension(); } diff --git a/app/assets/javascripts/app/services/helpers/crypto.js b/app/assets/javascripts/app/services/helpers/crypto.js index f2dbb675b..682a1c282 100644 --- a/app/assets/javascripts/app/services/helpers/crypto.js +++ b/app/assets/javascripts/app/services/helpers/crypto.js @@ -1,7 +1,7 @@ class SNCrypto { - generateRandomKey() { - return CryptoJS.lib.WordArray.random(512/8).toString(); + generateRandomKey(bits) { + return CryptoJS.lib.WordArray.random(bits/8).toString(); } generateUUID() { @@ -30,24 +30,30 @@ class SNCrypto { } } - decryptText(encrypted_content, key, iv, auth) { - var keyData = CryptoJS.enc.Hex.parse(key); + decryptText({ciphertextToAuth, contentCiphertext, encryptionKey, iv, authHash, authKey} = {}) { + if(authHash) { + var localAuthHash = Neeto.crypto.hmac256(ciphertextToAuth, authKey); + if(authHash !== localAuthHash) { + console.error("Auth hash does not match, returning null."); + return null; + } + } + var keyData = CryptoJS.enc.Hex.parse(encryptionKey); var ivData = CryptoJS.enc.Hex.parse(iv || ""); - var decrypted = CryptoJS.AES.decrypt(encrypted_content, keyData, { iv: ivData, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); + var decrypted = CryptoJS.AES.decrypt(contentCiphertext, keyData, { iv: ivData, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); } - encryptText(text, key) { + encryptText(text, key, iv) { var keyData = CryptoJS.enc.Hex.parse(key); - // items are encrypted with random keys; no two items are encrypted with same key, thus IV is not needed - var ivData = CryptoJS.enc.Hex.parse(""); + var ivData = CryptoJS.enc.Hex.parse(iv || ""); var encrypted = CryptoJS.AES.encrypt(text, keyData, { iv: ivData, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } generateRandomEncryptionKey() { - var salt = Neeto.crypto.generateRandomKey(); - var passphrase = Neeto.crypto.generateRandomKey(); + var salt = Neeto.crypto.generateRandomKey(512); + var passphrase = Neeto.crypto.generateRandomKey(512); return CryptoJS.PBKDF2(passphrase, salt, { keySize: 512/32 }).toString(); } @@ -83,28 +89,37 @@ class SNCrypto { return CryptoJS.HmacSHA256(messageData, keyData).toString(); } + generateKeysFromMasterKey(mk) { + var encryptionKey = Neeto.crypto.hmac256(mk, "e"); + var authKey = Neeto.crypto.hmac256(mk, "a"); + 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({pw: pw, mk: mk}); - }); + callback(_.merge({pw: pw, mk: mk}, this.generateKeysFromMasterKey(mk))); + }.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(); + 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]; - callback({pw: pw, mk: mk}, defaults); - }); + var encryptionKey = Neeto.crypto.hmac256(mk, "e"); + var authKey = Neeto.crypto.hmac256(mk, "a"); + + callback(_.merge({pw: pw, mk: mk}, this.generateKeysFromMasterKey(mk)), defaults); + }.bind(this)); } } diff --git a/app/assets/javascripts/app/services/helpers/encryptionHelper.js b/app/assets/javascripts/app/services/helpers/encryptionHelper.js index e0e171af5..58838c240 100644 --- a/app/assets/javascripts/app/services/helpers/encryptionHelper.js +++ b/app/assets/javascripts/app/services/helpers/encryptionHelper.js @@ -1,35 +1,93 @@ class EncryptionHelper { - static encryptItem(item, key) { - var item_key = Neeto.crypto.generateRandomEncryptionKey(); - item.enc_item_key = Neeto.crypto.encryptText(item_key, key); + static _private_encryptString(string, encryptionKey, authKey, version) { + var fullCiphertext, contentCiphertext; + if(version === "001") { + contentCiphertext = Neeto.crypto.encryptText(string, encryptionKey, null); + fullCiphertext = version + contentCiphertext; + } else { + var iv = Neeto.crypto.generateRandomKey(128); + contentCiphertext = Neeto.crypto.encryptText(string, encryptionKey, iv); + var ciphertextToAuth = [version, iv, contentCiphertext].join(":"); + var authHash = Neeto.crypto.hmac256(ciphertextToAuth, authKey); + fullCiphertext = [version, authHash, iv, contentCiphertext].join(":"); + } - var ek = Neeto.crypto.firstHalfOfKey(item_key); - var ak = Neeto.crypto.secondHalfOfKey(item_key); - var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.createContentJSONFromProperties()), ek); - var authHash = Neeto.crypto.hmac256(encryptedContent, ak); - - item.content = encryptedContent; - item.auth_hash = authHash; + return fullCiphertext; } - static decryptItem(item, key) { - var item_key = Neeto.crypto.decryptText(item.enc_item_key, key); + static encryptItem(item, keys, version) { + // encrypt item key + var item_key = Neeto.crypto.generateRandomEncryptionKey(); + if(version === "001") { + // legacy + item.enc_item_key = Neeto.crypto.encryptText(item_key, keys.mk, null); + } else { + item.enc_item_key = this._private_encryptString(item_key, keys.encryptionKey, keys.authKey, version); + } + // encrypt content var ek = Neeto.crypto.firstHalfOfKey(item_key); var ak = Neeto.crypto.secondHalfOfKey(item_key); - var authHash = Neeto.crypto.hmac256(item.content, ak); - if(authHash !== item.auth_hash || !item.auth_hash) { - console.log("Authentication hash does not match.") + var ciphertext = this._private_encryptString(JSON.stringify(item.createContentJSONFromProperties()), ek, ak, version); + if(version === "001") { + var authHash = Neeto.crypto.hmac256(ciphertext, ak); + item.auth_hash = authHash; + } + + item.content = ciphertext; + } + + static encryptionComponentsFromString(string, baseKey, encryptionKey, authKey) { + var encryptionVersion = string.substring(0, 3); + if(encryptionVersion === "001") { + return { + contentCiphertext: string.substring(3, string.length), + encryptionVersion: encryptionVersion, + ciphertextToAuth: string, + iv: null, + authHash: null, + encryptionKey: baseKey + } + } else { + let components = string.split(":"); + return { + encryptionVersion: components[0], + authHash: components[1], + iv: components[2], + contentCiphertext: components[3], + ciphertextToAuth: [components[0], components[2], components[3]].join(":"), + encryptionKey: encryptionKey, + authKey: authKey + } + } + } + + static decryptItem(item, keys) { + // decrypt encrypted key + var encryptedItemKey = item.enc_item_key; + if(!encryptedItemKey.startsWith("002")) { + // legacy encryption type, has no prefix + encryptedItemKey = "001" + encryptedItemKey; + } + var keyParams = this.encryptionComponentsFromString(encryptedItemKey, keys.mk, keys.encryptionKey, keys.authKey); + var item_key = Neeto.crypto.decryptText(keyParams); + + if(!item_key) { return; } - var content = Neeto.crypto.decryptText(item.content.substring(3, item.content.length), ek); + // 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 content = Neeto.crypto.decryptText(itemParams); + item.content = content; } - static decryptMultipleItems(items, key, throws) { - for (var item of items) { + static decryptMultipleItems(items, keys, throws) { + for (var item of items) { if(item.deleted == true) { continue; } @@ -37,9 +95,9 @@ class EncryptionHelper { var isString = typeof item.content === 'string' || item.content instanceof String; if(isString) { try { - if(item.content.substring(0, 3) == "001" && item.enc_item_key) { + if((item.content.startsWith("001") || item.content.startsWith("002")) && item.enc_item_key) { // is encrypted - this.decryptItem(item, key); + this.decryptItem(item, keys); } else { // is base64 encoded item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)) @@ -52,7 +110,7 @@ class EncryptionHelper { continue; } } - } + } } } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 54d4c18d8..c08668210 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -164,7 +164,7 @@ class SyncManager { var params = {}; params.limit = 150; params.items = _.map(subItems, function(item){ - var itemParams = new ItemParams(item, localStorage.getItem("mk")); + var itemParams = new ItemParams(item, this.authManager.keys()); itemParams.additionalFields = options.additionalFields; return itemParams.paramsForSync(); }.bind(this)); @@ -252,7 +252,7 @@ class SyncManager { } handleItemsResponse(responseItems, omitFields) { - EncryptionHelper.decryptMultipleItems(responseItems, localStorage.getItem("mk")); + EncryptionHelper.decryptMultipleItems(responseItems, this.authManager.keys()); return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); }