mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	encryption code separated into its own file
This commit is contained in:
		| @@ -156,6 +156,7 @@ | |||||||
|     <script src="stat/js/note.js"></script> |     <script src="stat/js/note.js"></script> | ||||||
|     <script src="stat/js/notecase2html.js"></script> |     <script src="stat/js/notecase2html.js"></script> | ||||||
|     <script src="stat/js/html2notecase.js"></script> |     <script src="stat/js/html2notecase.js"></script> | ||||||
|  |     <script src="stat/js/encryption.js"></script> | ||||||
|     <script src="stat/js/utils.js"></script> |     <script src="stat/js/utils.js"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
							
								
								
									
										189
									
								
								static/js/encryption.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								static/js/encryption.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | let globalEncryptionCallback = null; | ||||||
|  |  | ||||||
|  | function handleEncryption(requireEncryption, modal, callback) { | ||||||
|  |     if (requireEncryption && globalEncryptionKey === null) { | ||||||
|  |         globalEncryptionCallback = callback; | ||||||
|  |  | ||||||
|  |         if (!modal) { | ||||||
|  |             $("#noteDetailWrapper").hide(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $("#encryptionPasswordDialog").dialog({ | ||||||
|  |             modal: modal, | ||||||
|  |             width: 400, | ||||||
|  |             open: function() { | ||||||
|  |                 if (!modal) { | ||||||
|  |                     // dialog steals focus for itself, which is not what we want for non-modal (viewing) | ||||||
|  |                     getNodeByKey(globalNote.detail.note_id).setFocus(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         callback(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // currently not configurable | ||||||
|  | const globalEncryptionKeyTimeToLive = 10 * 60 * 1000; // in milliseconds | ||||||
|  |  | ||||||
|  | let globalEncryptionKey = null; | ||||||
|  | let globalLastEncryptionOperationDate = null; | ||||||
|  |  | ||||||
|  | function deriveEncryptionKey(password) { | ||||||
|  |     // why this is done is explained here: https://github.com/ricmoo/scrypt-js - "Encoding notes" | ||||||
|  |     const normalizedPassword = password.normalize('NFKC'); | ||||||
|  |     const salt = "dc73b57736511340f132e4b5521d178afa6311c45e0c25e6a9339038507852a6"; | ||||||
|  |  | ||||||
|  |     const passwordBuffer = new buffer.SlowBuffer(normalizedPassword); | ||||||
|  |     const saltBuffer = new buffer.SlowBuffer(salt); | ||||||
|  |  | ||||||
|  |     // this settings take ~500ms on my laptop | ||||||
|  |     const N = 16384, r = 16, p = 1; | ||||||
|  |     // 32 byte key - AES 256 | ||||||
|  |     const dkLen = 32; | ||||||
|  |  | ||||||
|  |     const startedDate = new Date(); | ||||||
|  |  | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |         scrypt(passwordBuffer, saltBuffer, N, r, p, dkLen, function (error, progress, key) { | ||||||
|  |             if (error) { | ||||||
|  |                 console.log("Error: " + error); | ||||||
|  |  | ||||||
|  |                 reject(); | ||||||
|  |             } | ||||||
|  |             else if (key) { | ||||||
|  |                 console.log("Computation took " + (new Date().getTime() - startedDate.getTime()) + "ms"); | ||||||
|  |  | ||||||
|  |                 $.ajax({ | ||||||
|  |                     url: baseUrl + 'password/verify', | ||||||
|  |                     type: 'POST', | ||||||
|  |                     data: JSON.stringify({ | ||||||
|  |                         password: sha256(key) | ||||||
|  |                     }), | ||||||
|  |                     contentType: "application/json", | ||||||
|  |                     success: function (result) { | ||||||
|  |                         if (result.valid) { | ||||||
|  |                             resolve(key); | ||||||
|  |                         } | ||||||
|  |                         else { | ||||||
|  |                             alert("Wrong password"); | ||||||
|  |  | ||||||
|  |                             reject(); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 // update UI with progress complete | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $("#encryptionPasswordForm").submit(function() { | ||||||
|  |     const password = $("#encryptionPassword").val(); | ||||||
|  |     $("#encryptionPassword").val(""); | ||||||
|  |  | ||||||
|  |     deriveEncryptionKey(password).then(key => { | ||||||
|  |         $("#noteDetailWrapper").show(); | ||||||
|  |         $("#encryptionPasswordDialog").dialog("close"); | ||||||
|  |  | ||||||
|  |         globalEncryptionKey = key; | ||||||
|  |  | ||||||
|  |         if (globalEncryptionCallback !== null) { | ||||||
|  |             globalEncryptionCallback(); | ||||||
|  |  | ||||||
|  |             globalEncryptionCallback = null; | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | function getAes() { | ||||||
|  |     globalLastEncryptionOperationDate = new Date(); | ||||||
|  |  | ||||||
|  |     setTimeout(function() { | ||||||
|  |         if (new Date().getTime() - globalLastEncryptionOperationDate.getTime() > globalEncryptionKeyTimeToLive) { | ||||||
|  |             globalEncryptionKey = null; | ||||||
|  |  | ||||||
|  |             if (globalNote.detail.encryption > 0) { | ||||||
|  |                 loadNote(globalNote.detail.note_id); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }, globalEncryptionKeyTimeToLive + 1000); | ||||||
|  |  | ||||||
|  |     return new aesjs.ModeOfOperation.ctr(globalEncryptionKey, new aesjs.Counter(5)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function encryptNoteIfNecessary(note) { | ||||||
|  |     if (note.detail.encryption === 0) { | ||||||
|  |         return note; | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         return encryptNote(note); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function encryptNote(note) { | ||||||
|  |     const aes = getAes(); | ||||||
|  |     const noteJson = note.detail.note_text; | ||||||
|  |  | ||||||
|  |     const noteBytes = aesjs.utils.utf8.toBytes(noteJson); | ||||||
|  |  | ||||||
|  |     const encryptedBytes = aes.encrypt(noteBytes); | ||||||
|  |  | ||||||
|  |     // To print or store the binary data, you may convert it to hex | ||||||
|  |     note.detail.note_text = uint8ToBase64(encryptedBytes); | ||||||
|  |  | ||||||
|  |     return note; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function encryptNoteAndSendToServer() { | ||||||
|  |     handleEncryption(true, true, () => { | ||||||
|  |         const note = globalNote; | ||||||
|  |  | ||||||
|  |         updateNoteFromInputs(note); | ||||||
|  |  | ||||||
|  |         encryptNote(note); | ||||||
|  |  | ||||||
|  |         note.detail.encryption = 1; | ||||||
|  |  | ||||||
|  |         saveNoteToServer(note); | ||||||
|  |  | ||||||
|  |         setNoteBackgroundIfEncrypted(note); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function decryptNoteAndSendToServer() { | ||||||
|  |     handleEncryption(true, true, () => { | ||||||
|  |         const note = globalNote; | ||||||
|  |  | ||||||
|  |         updateNoteFromInputs(note); | ||||||
|  |  | ||||||
|  |         note.detail.encryption = 0; | ||||||
|  |  | ||||||
|  |         saveNoteToServer(note); | ||||||
|  |  | ||||||
|  |         setNoteBackgroundIfEncrypted(note); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function decryptNoteIfNecessary(note) { | ||||||
|  |     if (note.detail.encryption === 1) { | ||||||
|  |         return decryptNote(note.detail.note_text); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         return note.detail.note_text; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function decryptNote(encryptedBase64) { | ||||||
|  |     const aes = getAes(); | ||||||
|  |     const encryptedBytes = base64ToUint8Array(encryptedBase64); | ||||||
|  |  | ||||||
|  |     const decryptedBytes = aes.decrypt(encryptedBytes); | ||||||
|  |  | ||||||
|  |     return aesjs.utils.utf8.fromBytes(decryptedBytes); | ||||||
|  | } | ||||||
| @@ -91,7 +91,7 @@ $(document).ready(function() { | |||||||
|     $(".note-editable").attr("tabindex", 2); |     $(".note-editable").attr("tabindex", 2); | ||||||
| }); | }); | ||||||
|    |    | ||||||
| var globalNote; | let globalNote; | ||||||
|  |  | ||||||
| function createNewTopLevelNote() { | function createNewTopLevelNote() { | ||||||
|     let rootNode = globalTree.fancytree("getRootNode"); |     let rootNode = globalTree.fancytree("getRootNode"); | ||||||
| @@ -154,32 +154,6 @@ function setNoteBackgroundIfEncrypted(note) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| let globalEncryptionCallback = null; |  | ||||||
|  |  | ||||||
| function handleEncryption(requireEncryption, modal, callback) { |  | ||||||
|     if (requireEncryption && globalEncryptionKey === null) { |  | ||||||
|         globalEncryptionCallback = callback; |  | ||||||
|  |  | ||||||
|         if (!modal) { |  | ||||||
|             $("#noteDetailWrapper").hide(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $("#encryptionPasswordDialog").dialog({ |  | ||||||
|             modal: modal, |  | ||||||
|             width: 400, |  | ||||||
|             open: function() { |  | ||||||
|                 if (!modal) { |  | ||||||
|                     // dialog steals focus for itself, which is not what we want for non-modal (viewing) |  | ||||||
|                     getNodeByKey(globalNote.detail.note_id).setFocus(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|     else { |  | ||||||
|         callback(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function loadNote(noteId) { | function loadNote(noteId) { | ||||||
|     $.get(baseUrl + 'notes/' + noteId).then(function(note) { |     $.get(baseUrl + 'notes/' + noteId).then(function(note) { | ||||||
|         globalNote = note; |         globalNote = note; | ||||||
| @@ -241,184 +215,3 @@ function addRecentNote(noteTreeId, noteContentId) { | |||||||
|     }, 1500); |     }, 1500); | ||||||
| } | } | ||||||
|  |  | ||||||
| function deriveEncryptionKey(password) { |  | ||||||
|     // why this is done is explained here: https://github.com/ricmoo/scrypt-js - "Encoding notes" |  | ||||||
|     const normalizedPassword = password.normalize('NFKC'); |  | ||||||
|     const salt = "dc73b57736511340f132e4b5521d178afa6311c45e0c25e6a9339038507852a6"; |  | ||||||
|  |  | ||||||
|     const passwordBuffer = new buffer.SlowBuffer(normalizedPassword); |  | ||||||
|     const saltBuffer = new buffer.SlowBuffer(salt); |  | ||||||
|  |  | ||||||
|     // this settings take ~500ms on my laptop |  | ||||||
|     const N = 16384, r = 16, p = 1; |  | ||||||
|     // 32 byte key - AES 256 |  | ||||||
|     const dkLen = 32; |  | ||||||
|  |  | ||||||
|     const startedDate = new Date(); |  | ||||||
|  |  | ||||||
|     return new Promise((resolve, reject) => { |  | ||||||
|         scrypt(passwordBuffer, saltBuffer, N, r, p, dkLen, function (error, progress, key) { |  | ||||||
|             if (error) { |  | ||||||
|                 console.log("Error: " + error); |  | ||||||
|  |  | ||||||
|                 reject(); |  | ||||||
|             } |  | ||||||
|             else if (key) { |  | ||||||
|                 console.log("Computation took " + (new Date().getTime() - startedDate.getTime()) + "ms"); |  | ||||||
|  |  | ||||||
|                 $.ajax({ |  | ||||||
|                     url: baseUrl + 'password/verify', |  | ||||||
|                     type: 'POST', |  | ||||||
|                     data: JSON.stringify({ |  | ||||||
|                         password: sha256(key) |  | ||||||
|                     }), |  | ||||||
|                     contentType: "application/json", |  | ||||||
|                     success: function (result) { |  | ||||||
|                         if (result.valid) { |  | ||||||
|                             resolve(key); |  | ||||||
|                         } |  | ||||||
|                         else { |  | ||||||
|                             alert("Wrong password"); |  | ||||||
|  |  | ||||||
|                             reject(); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 // update UI with progress complete |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // currently not configurable |  | ||||||
| const globalEncryptionKeyTimeToLive = 10 * 60 * 1000; // in milliseconds |  | ||||||
|  |  | ||||||
| let globalEncryptionKey = null; |  | ||||||
| let globalLastEncryptionOperationDate = null; |  | ||||||
|  |  | ||||||
| $("#encryptionPasswordForm").submit(function() { |  | ||||||
|     const password = $("#encryptionPassword").val(); |  | ||||||
|     $("#encryptionPassword").val(""); |  | ||||||
|  |  | ||||||
|     deriveEncryptionKey(password).then(key => { |  | ||||||
|         $("#noteDetailWrapper").show(); |  | ||||||
|         $("#encryptionPasswordDialog").dialog("close"); |  | ||||||
|  |  | ||||||
|         globalEncryptionKey = key; |  | ||||||
|  |  | ||||||
|         if (globalEncryptionCallback !== null) { |  | ||||||
|             globalEncryptionCallback(); |  | ||||||
|  |  | ||||||
|             globalEncryptionCallback = null; |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| function getAes() { |  | ||||||
|     globalLastEncryptionOperationDate = new Date(); |  | ||||||
|  |  | ||||||
|     setTimeout(function() { |  | ||||||
|         if (new Date().getTime() - globalLastEncryptionOperationDate.getTime() > globalEncryptionKeyTimeToLive) { |  | ||||||
|             globalEncryptionKey = null; |  | ||||||
|  |  | ||||||
|             if (globalNote.detail.encryption > 0) { |  | ||||||
|                 loadNote(globalNote.detail.note_id); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }, globalEncryptionKeyTimeToLive + 1000); |  | ||||||
|  |  | ||||||
|     return new aesjs.ModeOfOperation.ctr(globalEncryptionKey, new aesjs.Counter(5)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function encryptNoteIfNecessary(note) { |  | ||||||
|     if (note.detail.encryption === 0) { |  | ||||||
|         return note; |  | ||||||
|     } |  | ||||||
|     else { |  | ||||||
|         return encryptNote(note); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function encryptNote(note) { |  | ||||||
|     const aes = getAes(); |  | ||||||
|     const noteJson = note.detail.note_text; |  | ||||||
|  |  | ||||||
|     const noteBytes = aesjs.utils.utf8.toBytes(noteJson); |  | ||||||
|  |  | ||||||
|     const encryptedBytes = aes.encrypt(noteBytes); |  | ||||||
|  |  | ||||||
|     // To print or store the binary data, you may convert it to hex |  | ||||||
|     note.detail.note_text = uint8ToBase64(encryptedBytes); |  | ||||||
|  |  | ||||||
|     return note; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function encryptNoteAndSendToServer() { |  | ||||||
|     handleEncryption(true, true, () => { |  | ||||||
|         const note = globalNote; |  | ||||||
|  |  | ||||||
|         updateNoteFromInputs(note); |  | ||||||
|  |  | ||||||
|         encryptNote(note); |  | ||||||
|  |  | ||||||
|         note.detail.encryption = 1; |  | ||||||
|  |  | ||||||
|         saveNoteToServer(note); |  | ||||||
|  |  | ||||||
|         setNoteBackgroundIfEncrypted(note); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function decryptNoteAndSendToServer() { |  | ||||||
|     handleEncryption(true, true, () => { |  | ||||||
|         const note = globalNote; |  | ||||||
|  |  | ||||||
|         updateNoteFromInputs(note); |  | ||||||
|  |  | ||||||
|         note.detail.encryption = 0; |  | ||||||
|  |  | ||||||
|         saveNoteToServer(note); |  | ||||||
|  |  | ||||||
|         setNoteBackgroundIfEncrypted(note); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function decryptNoteIfNecessary(note) { |  | ||||||
|     if (note.detail.encryption === 1) { |  | ||||||
|         return decryptNote(note.detail.note_text); |  | ||||||
|     } |  | ||||||
|     else { |  | ||||||
|         return note.detail.note_text; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function decryptNote(encryptedBase64) { |  | ||||||
|     const aes = getAes(); |  | ||||||
|     const encryptedBytes = base64ToUint8Array(encryptedBase64); |  | ||||||
|  |  | ||||||
|     const decryptedBytes = aes.decrypt(encryptedBytes); |  | ||||||
|  |  | ||||||
|     return aesjs.utils.utf8.fromBytes(decryptedBytes); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function uint8ToBase64(u8Arr) { |  | ||||||
|     const CHUNK_SIZE = 0x8000; //arbitrary number |  | ||||||
|     const length = u8Arr.length; |  | ||||||
|     let index = 0; |  | ||||||
|     let result = ''; |  | ||||||
|     let slice; |  | ||||||
|     while (index < length) { |  | ||||||
|         slice = u8Arr.subarray(index, Math.min(index + CHUNK_SIZE, length)); |  | ||||||
|         result += String.fromCharCode.apply(null, slice); |  | ||||||
|         index += CHUNK_SIZE; |  | ||||||
|     } |  | ||||||
|     return btoa(result); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function base64ToUint8Array(base64encoded) { |  | ||||||
|     return new Uint8Array(atob(base64encoded).split("").map(function(c) { return c.charCodeAt(0); })); |  | ||||||
| } |  | ||||||
| @@ -12,4 +12,22 @@ function error(str) { | |||||||
|     error.show(); |     error.show(); | ||||||
|     error.html(str); |     error.html(str); | ||||||
|     error.fadeOut(10000); |     error.fadeOut(10000); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function uint8ToBase64(u8Arr) { | ||||||
|  |     const CHUNK_SIZE = 0x8000; //arbitrary number | ||||||
|  |     const length = u8Arr.length; | ||||||
|  |     let index = 0; | ||||||
|  |     let result = ''; | ||||||
|  |     let slice; | ||||||
|  |     while (index < length) { | ||||||
|  |         slice = u8Arr.subarray(index, Math.min(index + CHUNK_SIZE, length)); | ||||||
|  |         result += String.fromCharCode.apply(null, slice); | ||||||
|  |         index += CHUNK_SIZE; | ||||||
|  |     } | ||||||
|  |     return btoa(result); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function base64ToUint8Array(base64encoded) { | ||||||
|  |     return new Uint8Array(atob(base64encoded).split("").map(function(c) { return c.charCodeAt(0); })); | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user