mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 13:01:31 +08:00 
			
		
		
		
	server side encryption WIP
This commit is contained in:
		
							parent
							
								
									8f1eedfe0d
								
							
						
					
					
						commit
						ec49bf0cca
					
				| @ -11,6 +11,7 @@ const encryption = (function() { | |||||||
|     let passwordDerivedKeySalt = null; |     let passwordDerivedKeySalt = null; | ||||||
|     let encryptedDataKey = null; |     let encryptedDataKey = null; | ||||||
|     let encryptionSessionTimeout = null; |     let encryptionSessionTimeout = null; | ||||||
|  |     let protectedSessionId = null; | ||||||
| 
 | 
 | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
|         url: baseApiUrl + 'settings/all', |         url: baseApiUrl + 'settings/all', | ||||||
| @ -109,17 +110,19 @@ const encryption = (function() { | |||||||
|         const password = encryptionPasswordEl.val(); |         const password = encryptionPasswordEl.val(); | ||||||
|         encryptionPasswordEl.val(""); |         encryptionPasswordEl.val(""); | ||||||
| 
 | 
 | ||||||
|         const key = await getDataKey(password); |         const response = await enterProtectedSession(password); | ||||||
|         if (key === false) { | 
 | ||||||
|             showError("Wrong password!"); |         if (!response.success) { | ||||||
|  |             showError("Wrong password."); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         protectedSessionId = response.protectedSessionId; | ||||||
|  |         initAjax(); | ||||||
|  | 
 | ||||||
|         dialogEl.dialog("close"); |         dialogEl.dialog("close"); | ||||||
| 
 | 
 | ||||||
|         dataKey = key; |         noteTree.reload(); | ||||||
| 
 |  | ||||||
|         decryptTreeItems(); |  | ||||||
| 
 | 
 | ||||||
|         if (encryptionDeferred !== null) { |         if (encryptionDeferred !== null) { | ||||||
|             encryptionDeferred.resolve(); |             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() { |     function resetEncryptionSession() { | ||||||
|         dataKey = null; |         protectedSessionId = null; | ||||||
|  | 
 | ||||||
|  |         initAjax(); | ||||||
| 
 | 
 | ||||||
|         // most secure solution - guarantees nothing remained in memory
 |         // most secure solution - guarantees nothing remained in memory
 | ||||||
|         // since this expires because user doesn't use the app, it shouldn't be disruptive
 |         // since this expires because user doesn't use the app, it shouldn't be disruptive
 | ||||||
| @ -425,6 +446,7 @@ const encryption = (function() { | |||||||
|         decryptNoteAndSendToServer, |         decryptNoteAndSendToServer, | ||||||
|         decryptNoteIfNecessary, |         decryptNoteIfNecessary, | ||||||
|         encryptSubTree, |         encryptSubTree, | ||||||
|         decryptSubTree |         decryptSubTree, | ||||||
|  |         getProtectedSessionId | ||||||
|     }; |     }; | ||||||
| })(); | })(); | ||||||
| @ -111,3 +111,14 @@ function showAppIfHidden() { | |||||||
|         loaderDiv.style.opacity = 0.0; |         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) { |         for (const note of notes) { | ||||||
|             glob.allNoteIds.push(note.note_id); |             glob.allNoteIds.push(note.note_id); | ||||||
| 
 | 
 | ||||||
|             if (note.encryption > 0) { |             note.title = note.note_title; | ||||||
|                 note.title = "[encrypted]"; |  | ||||||
| 
 | 
 | ||||||
|  |             if (note.encryption > 0) { | ||||||
|                 note.extraClasses = "encrypted"; |                 note.extraClasses = "encrypted"; | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|                 note.title = note.note_title; |  | ||||||
| 
 |  | ||||||
|                 if (note.is_clone) { |                 if (note.is_clone) { | ||||||
|                     note.title += " (clone)"; |                     note.title += " (clone)"; | ||||||
|                 } |                 } | ||||||
| @ -202,11 +200,6 @@ const noteTree = (function() { | |||||||
|             startNoteId = resp.start_note_id; |             startNoteId = resp.start_note_id; | ||||||
|             treeLoadTime = resp.tree_load_time; |             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) { |             if (document.location.hash) { | ||||||
|                 startNoteId = document.location.hash.substr(1); // strip initial #
 |                 startNoteId = document.location.hash.substr(1); // strip initial #
 | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -51,12 +51,7 @@ const treeUtils = (function() { | |||||||
|         const path = []; |         const path = []; | ||||||
| 
 | 
 | ||||||
|         while (note) { |         while (note) { | ||||||
|             if (note.data.encryption > 0 && !encryption.isEncryptionAvailable()) { |             path.push(note.title); | ||||||
|                 path.push("[encrypted]"); |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 path.push(note.title); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             note = note.getParent(); |             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)
 | // 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; |     const password = req.body.password; | ||||||
| 
 | 
 | ||||||
|     if (!await password_encryption.verifyPassword(password)) { |     if (!await password_encryption.verifyPassword(password)) { | ||||||
|         return { |         res.send({ | ||||||
|             success: false, |             success: false, | ||||||
|             message: "Given current password doesn't match hash" |             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); |     const protectedSessionId = protected_session.setDataKey(req, decryptedDataKey); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7,6 +7,8 @@ const options = require('../../services/options'); | |||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
| const log = require('../../services/log'); | 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) => { | router.get('/', auth.checkApiAuth, async (req, res, next) => { | ||||||
|     const notes = await sql.getResults("select " |     const notes = await sql.getResults("select " | ||||||
| @ -24,7 +26,13 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => { | |||||||
|     const root_notes = []; |     const root_notes = []; | ||||||
|     const notes_map = {}; |     const notes_map = {}; | ||||||
| 
 | 
 | ||||||
|  |     const dataKey = protected_session.getDataKey(req); | ||||||
|  | 
 | ||||||
|     for (const note of notes) { |     for (const note of notes) { | ||||||
|  |         if (note['encryption']) { | ||||||
|  |             note.note_title = data_encryption.decrypt(dataKey, note.note_title); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         note.children = []; |         note.children = []; | ||||||
| 
 | 
 | ||||||
|         if (!note.note_pid) { |         if (!note.note_pid) { | ||||||
| @ -50,11 +58,6 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => { | |||||||
|     res.send({ |     res.send({ | ||||||
|         notes: root_notes, |         notes: root_notes, | ||||||
|         start_note_id: await options.getOption('start_node'), |         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() |         tree_load_time: utils.nowTimestamp() | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -3,11 +3,12 @@ | |||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const auth = require('../services/auth'); | const auth = require('../services/auth'); | ||||||
| const migration = require('../services/migration'); | const utils = require('../services/utils'); | ||||||
| const sql = require('../services/sql'); |  | ||||||
| 
 | 
 | ||||||
| router.get('', auth.checkAuth, async (req, res, next) => { | router.get('', auth.checkAuth, async (req, res, next) => { | ||||||
|     res.render('index', {}); |     res.render('index', { | ||||||
|  |         browserId: utils.randomString(12) | ||||||
|  |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| module.exports = router; | 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 encryptedBytes = utils.fromBase64(encryptedBase64); | ||||||
| 
 | 
 | ||||||
|     const aes = getAes(passwordDerivedKey); |     const aes = getAes(passwordDerivedKey); | ||||||
|     return aes.decrypt(encryptedBytes).slice(4); |     return Array.from(aes.decrypt(encryptedBytes).slice(4)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function encryptDataKey(passwordDerivedKey, plainText) { | function encryptDataKey(passwordDerivedKey, plainText) { | ||||||
|  | |||||||
| @ -7,7 +7,9 @@ function setDataKey(req, decryptedDataKey) { | |||||||
|     return req.session.protectedSessionId; |     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) { |     if (protectedSessionId && req.session.protectedSessionId === protectedSessionId) { | ||||||
|         return req.session.decryptedDataKey; |         return req.session.decryptedDataKey; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -226,6 +226,7 @@ | |||||||
| 
 | 
 | ||||||
|     <script type="text/javascript"> |     <script type="text/javascript"> | ||||||
|       const baseApiUrl = 'api/'; |       const baseApiUrl = 'api/'; | ||||||
|  |       const browserId = '<%= browserId %>'; | ||||||
|     </script> |     </script> | ||||||
| 
 | 
 | ||||||
|     <!-- Required for correct loading of scripts in Electron --> |     <!-- Required for correct loading of scripts in Electron --> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 azivner
						azivner