mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	server side encryption WIP
This commit is contained in:
		| @@ -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 | ||||
|     }; | ||||
| })(); | ||||
| @@ -111,3 +111,14 @@ function showAppIfHidden() { | ||||
|         loaderDiv.style.opacity = 0.0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function initAjax() { | ||||
|     $.ajaxSetup({ | ||||
|         headers: { | ||||
|             'x-browser-id': browserId, | ||||
|             'x-protected-session-id': encryption ? encryption.getProtectedSessionId() : null | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| initAjax(); | ||||
| @@ -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 # | ||||
|             } | ||||
|   | ||||
| @@ -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); | ||||
|             } | ||||
|  | ||||
|             note = note.getParent(); | ||||
|         } | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
							
								
								
									
										33
									
								
								services/data_encryption.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								services/data_encryption.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| }; | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   | ||||
| @@ -226,6 +226,7 @@ | ||||
|  | ||||
|     <script type="text/javascript"> | ||||
|       const baseApiUrl = 'api/'; | ||||
|       const browserId = '<%= browserId %>'; | ||||
|     </script> | ||||
|  | ||||
|     <!-- Required for correct loading of scripts in Electron --> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user