From ec49bf0cca756a0fd01ca8f5d4089c3b7a22bf5a Mon Sep 17 00:00:00 2001 From: azivner Date: Fri, 10 Nov 2017 22:55:19 -0500 Subject: [PATCH] server side encryption WIP --- public/javascripts/encryption.js | 38 +++++++++++++++++++++++++------- public/javascripts/init.js | 13 ++++++++++- public/javascripts/note_tree.js | 11 ++------- public/javascripts/tree_utils.js | 7 +----- routes/api/login.js | 10 +++++---- routes/api/tree.js | 13 ++++++----- routes/index.js | 7 +++--- services/data_encryption.js | 33 +++++++++++++++++++++++++++ services/password_encryption.js | 2 +- services/protected_session.js | 4 +++- views/index.ejs | 1 + 11 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 services/data_encryption.js diff --git a/public/javascripts/encryption.js b/public/javascripts/encryption.js index b0017d2aa..5cff94da6 100644 --- a/public/javascripts/encryption.js +++ b/public/javascripts/encryption.js @@ -11,6 +11,7 @@ const encryption = (function() { let passwordDerivedKeySalt = null; let encryptedDataKey = null; let encryptionSessionTimeout = null; + let protectedSessionId = null; $.ajax({ url: baseApiUrl + 'settings/all', @@ -109,17 +110,19 @@ const encryption = (function() { const password = encryptionPasswordEl.val(); encryptionPasswordEl.val(""); - const key = await getDataKey(password); - if (key === false) { - showError("Wrong password!"); + const response = await enterProtectedSession(password); + + if (!response.success) { + showError("Wrong password."); return; } + protectedSessionId = response.protectedSessionId; + initAjax(); + dialogEl.dialog("close"); - dataKey = key; - - decryptTreeItems(); + noteTree.reload(); if (encryptionDeferred !== null) { encryptionDeferred.resolve(); @@ -128,8 +131,26 @@ const encryption = (function() { } } + async function enterProtectedSession(password) { + return await $.ajax({ + url: baseApiUrl + 'login/protected', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + password: password + }), + error: () => showError("Error entering protected session.") + }); + } + + function getProtectedSessionId() { + return protectedSessionId; + } + function resetEncryptionSession() { - dataKey = null; + protectedSessionId = null; + + initAjax(); // most secure solution - guarantees nothing remained in memory // since this expires because user doesn't use the app, it shouldn't be disruptive @@ -425,6 +446,7 @@ const encryption = (function() { decryptNoteAndSendToServer, decryptNoteIfNecessary, encryptSubTree, - decryptSubTree + decryptSubTree, + getProtectedSessionId }; })(); \ No newline at end of file diff --git a/public/javascripts/init.js b/public/javascripts/init.js index a973ae93c..5f45588b0 100644 --- a/public/javascripts/init.js +++ b/public/javascripts/init.js @@ -110,4 +110,15 @@ function showAppIfHidden() { // Kick off the CSS transition loaderDiv.style.opacity = 0.0; } -} \ No newline at end of file +} + +function initAjax() { + $.ajaxSetup({ + headers: { + 'x-browser-id': browserId, + 'x-protected-session-id': encryption ? encryption.getProtectedSessionId() : null + } + }); +} + +initAjax(); \ No newline at end of file diff --git a/public/javascripts/note_tree.js b/public/javascripts/note_tree.js index 73211f98e..019829f9e 100644 --- a/public/javascripts/note_tree.js +++ b/public/javascripts/note_tree.js @@ -23,14 +23,12 @@ const noteTree = (function() { for (const note of notes) { glob.allNoteIds.push(note.note_id); - if (note.encryption > 0) { - note.title = "[encrypted]"; + note.title = note.note_title; + if (note.encryption > 0) { note.extraClasses = "encrypted"; } else { - note.title = note.note_title; - if (note.is_clone) { note.title += " (clone)"; } @@ -202,11 +200,6 @@ const noteTree = (function() { startNoteId = resp.start_note_id; treeLoadTime = resp.tree_load_time; - // add browser ID header to all AJAX requests - $.ajaxSetup({ - headers: { 'x-browser-id': resp.browser_id } - }); - if (document.location.hash) { startNoteId = document.location.hash.substr(1); // strip initial # } diff --git a/public/javascripts/tree_utils.js b/public/javascripts/tree_utils.js index 42d638377..7121c6d67 100644 --- a/public/javascripts/tree_utils.js +++ b/public/javascripts/tree_utils.js @@ -51,12 +51,7 @@ const treeUtils = (function() { const path = []; while (note) { - if (note.data.encryption > 0 && !encryption.isEncryptionAvailable()) { - path.push("[encrypted]"); - } - else { - path.push(note.title); - } + path.push(note.title); note = note.getParent(); } diff --git a/routes/api/login.js b/routes/api/login.js index 2c15ee07a..963e0f936 100644 --- a/routes/api/login.js +++ b/routes/api/login.js @@ -45,17 +45,19 @@ router.post('/sync', async (req, res, next) => { }); // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) -router.post('protected', auth.checkApiAuth, async (req, res, next) => { +router.post('/protected', auth.checkApiAuth, async (req, res, next) => { const password = req.body.password; if (!await password_encryption.verifyPassword(password)) { - return { + res.send({ success: false, message: "Given current password doesn't match hash" - }; + }); + + return; } - const decryptedDataKey = password_encryption.getDecryptedDataKey(password); + const decryptedDataKey = await password_encryption.getDecryptedDataKey(password); const protectedSessionId = protected_session.setDataKey(req, decryptedDataKey); diff --git a/routes/api/tree.js b/routes/api/tree.js index 2b4a3dfea..1ff4273d5 100644 --- a/routes/api/tree.js +++ b/routes/api/tree.js @@ -7,6 +7,8 @@ const options = require('../../services/options'); const utils = require('../../services/utils'); const auth = require('../../services/auth'); const log = require('../../services/log'); +const protected_session = require('../../services/protected_session'); +const data_encryption = require('../../services/data_encryption'); router.get('/', auth.checkApiAuth, async (req, res, next) => { const notes = await sql.getResults("select " @@ -24,7 +26,13 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => { const root_notes = []; const notes_map = {}; + const dataKey = protected_session.getDataKey(req); + for (const note of notes) { + if (note['encryption']) { + note.note_title = data_encryption.decrypt(dataKey, note.note_title); + } + note.children = []; if (!note.note_pid) { @@ -50,11 +58,6 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => { res.send({ notes: root_notes, start_note_id: await options.getOption('start_node'), - password_verification_salt: await options.getOption('password_verification_salt'), - password_derived_key_salt: await options.getOption('password_derived_key_salt'), - encrypted_data_key: await options.getOption('encrypted_data_key'), - encryption_session_timeout: await options.getOption('encryption_session_timeout'), - browser_id: utils.randomString(12), tree_load_time: utils.nowTimestamp() }); }); diff --git a/routes/index.js b/routes/index.js index fb6c2cf04..25758ad80 100644 --- a/routes/index.js +++ b/routes/index.js @@ -3,11 +3,12 @@ const express = require('express'); const router = express.Router(); const auth = require('../services/auth'); -const migration = require('../services/migration'); -const sql = require('../services/sql'); +const utils = require('../services/utils'); router.get('', auth.checkAuth, async (req, res, next) => { - res.render('index', {}); + res.render('index', { + browserId: utils.randomString(12) + }); }); module.exports = router; diff --git a/services/data_encryption.js b/services/data_encryption.js new file mode 100644 index 000000000..0a9a779d9 --- /dev/null +++ b/services/data_encryption.js @@ -0,0 +1,33 @@ +const protected_session = require('./protected_session'); +const utils = require('./utils'); +const aesjs = require('./aes'); + +function getProtectedSessionId(req) { + return req.headers['x-protected-session-id']; +} + +function getDataAes(dataKey) { + return new aesjs.ModeOfOperation.ctr(dataKey, new aesjs.Counter(5)); +} + +function decrypt(dataKey, encryptedBase64) { + if (!dataKey) { + return "[protected]"; + } + + const aes = getDataAes(dataKey); + + const encryptedBytes = utils.fromBase64(encryptedBase64); + + const decryptedBytes = aes.decrypt(encryptedBytes); + + const digest = decryptedBytes.slice(0, 4); + const payload = decryptedBytes.slice(4); + + return aesjs.utils.utf8.fromBytes(payload); +} + +module.exports = { + getProtectedSessionId, + decrypt +}; \ No newline at end of file diff --git a/services/password_encryption.js b/services/password_encryption.js index 276c9f4fa..8228048f1 100644 --- a/services/password_encryption.js +++ b/services/password_encryption.js @@ -16,7 +16,7 @@ function decryptDataKey(passwordDerivedKey, encryptedBase64) { const encryptedBytes = utils.fromBase64(encryptedBase64); const aes = getAes(passwordDerivedKey); - return aes.decrypt(encryptedBytes).slice(4); + return Array.from(aes.decrypt(encryptedBytes).slice(4)); } function encryptDataKey(passwordDerivedKey, plainText) { diff --git a/services/protected_session.js b/services/protected_session.js index 8ee57abb9..6096b863a 100644 --- a/services/protected_session.js +++ b/services/protected_session.js @@ -7,7 +7,9 @@ function setDataKey(req, decryptedDataKey) { return req.session.protectedSessionId; } -function getDataKey(req, protectedSessionId) { +function getDataKey(req) { + const protectedSessionId = req.headers['x-protected-session-id']; + if (protectedSessionId && req.session.protectedSessionId === protectedSessionId) { return req.session.decryptedDataKey; } diff --git a/views/index.ejs b/views/index.ejs index 93f2e0424..100a84033 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -226,6 +226,7 @@