mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	encryption converted to module
This commit is contained in:
		| @@ -60,10 +60,10 @@ const contextMenuSetup = { | |||||||
|             noteEditor.createNote(node, node.key, 'into'); |             noteEditor.createNote(node, node.key, 'into'); | ||||||
|         } |         } | ||||||
|         else if (ui.cmd === "encryptSubTree") { |         else if (ui.cmd === "encryptSubTree") { | ||||||
|             encryptSubTree(node.key); |             encryption.encryptSubTree(node.key); | ||||||
|         } |         } | ||||||
|         else if (ui.cmd === "decryptSubTree") { |         else if (ui.cmd === "decryptSubTree") { | ||||||
|             decryptSubTree(node.key); |             encryption.decryptSubTree(node.key); | ||||||
|         } |         } | ||||||
|         else if (ui.cmd === "cut") { |         else if (ui.cmd === "cut") { | ||||||
|             cut(node); |             cut(node); | ||||||
|   | |||||||
| @@ -57,8 +57,8 @@ const noteHistory = (function() { | |||||||
|         let noteText = historyItem.note_text; |         let noteText = historyItem.note_text; | ||||||
|  |  | ||||||
|         if (historyItem.encryption > 0) { |         if (historyItem.encryption > 0) { | ||||||
|             noteTitle = decryptString(noteTitle); |             noteTitle = encryption.decryptString(noteTitle); | ||||||
|             noteText = decryptString(noteText); |             noteText = encryption.decryptString(noteText); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         titleEl.html(noteTitle); |         titleEl.html(noteTitle); | ||||||
|   | |||||||
| @@ -86,19 +86,17 @@ settings.addModule((function() { | |||||||
|             success: result => { |             success: result => { | ||||||
|                 if (result.success) { |                 if (result.success) { | ||||||
|                     // encryption password changed so current encryption session is invalid and needs to be cleared |                     // encryption password changed so current encryption session is invalid and needs to be cleared | ||||||
|                     resetEncryptionSession(); |                     encryption.resetEncryptionSession(); | ||||||
|  |  | ||||||
|                     glob.encryptedDataKey = result.new_encrypted_data_key; |                     encryption.setEncryptedDataKey(result.new_encrypted_data_key); | ||||||
|  |  | ||||||
|                     alert("Password has been changed."); |                     message("Password has been changed."); | ||||||
|  |  | ||||||
|                     $("#settings-dialog").dialog('close'); |  | ||||||
|                 } |                 } | ||||||
|                 else { |                 else { | ||||||
|                     alert(result.message); |                     message(result.message); | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             error: () => alert("Error occurred during changing password.") |             error: () => error("Error occurred during changing password.") | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         return false; |         return false; | ||||||
| @@ -122,7 +120,7 @@ settings.addModule((function() { | |||||||
|         const encryptionTimeout = encryptionTimeoutEl.val(); |         const encryptionTimeout = encryptionTimeoutEl.val(); | ||||||
|  |  | ||||||
|         settings.saveSettings(settingName, encryptionTimeout).then(() => { |         settings.saveSettings(settingName, encryptionTimeout).then(() => { | ||||||
|             glob.encryptionSessionTimeout = encryptionTimeout; |             encryption.setEncryptionSessionTimeout(encryptionTimeout); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         return false; |         return false; | ||||||
|   | |||||||
| @@ -1,417 +1,455 @@ | |||||||
| glob.encryptionDeferred = null; | const encryption = (function() { | ||||||
|  |     const dialogEl = $("#encryption-password-dialog"); | ||||||
|  |     const encryptionPasswordFormEl = $("#encryption-password-form"); | ||||||
|  |     const encryptionPasswordEl = $("#encryption-password"); | ||||||
|  |  | ||||||
| function handleEncryption(requireEncryption, modal) { |     let encryptionDeferred = null; | ||||||
|     const dfd = $.Deferred(); |     let dataKey = null; | ||||||
|  |     let lastEncryptionOperationDate = null; | ||||||
|  |     let encryptionSalt = null; | ||||||
|  |     let encryptedDataKey = null; | ||||||
|  |     let encryptionSessionTimeout = null; | ||||||
|  |  | ||||||
|     if (requireEncryption && glob.dataKey === null) { |     function setEncryptionSalt(encSalt) { | ||||||
|         glob.encryptionDeferred = dfd; |         encryptionSalt = encSalt; | ||||||
|  |     } | ||||||
|  |  | ||||||
|         $("#encryption-password-dialog").dialog({ |     function setEncryptedDataKey(encDataKey) { | ||||||
|             modal: modal, |         encryptedDataKey = encDataKey; | ||||||
|             width: 400, |     } | ||||||
|             open: () => { |  | ||||||
|                 if (!modal) { |     function setEncryptionSessionTimeout(encSessTimeout) { | ||||||
|                     // dialog steals focus for itself, which is not what we want for non-modal (viewing) |         encryptionSessionTimeout = encSessTimeout; | ||||||
|                     getNodeByKey(noteEditor.getCurrentNoteId()).setFocus(); |     } | ||||||
|  |  | ||||||
|  |     function ensureEncryptionIsAvailable(requireEncryption, modal) { | ||||||
|  |         const dfd = $.Deferred(); | ||||||
|  |  | ||||||
|  |         if (requireEncryption && dataKey === null) { | ||||||
|  |             encryptionDeferred = dfd; | ||||||
|  |  | ||||||
|  |             dialogEl.dialog({ | ||||||
|  |                 modal: modal, | ||||||
|  |                 width: 400, | ||||||
|  |                 open: () => { | ||||||
|  |                     if (!modal) { | ||||||
|  |                         // dialog steals focus for itself, which is not what we want for non-modal (viewing) | ||||||
|  |                         getNodeByKey(noteEditor.getCurrentNoteId()).setFocus(); | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             dfd.resolve(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return dfd.promise(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function getDataKey(password) { | ||||||
|  |         return computeScrypt(password, encryptionSalt, (key, resolve, reject) => { | ||||||
|  |             const dataKeyAes = getDataKeyAes(key); | ||||||
|  |  | ||||||
|  |             const decryptedDataKey = decrypt(dataKeyAes, encryptedDataKey); | ||||||
|  |  | ||||||
|  |             if (decryptedDataKey === false) { | ||||||
|  |                 reject("Wrong password."); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             resolve(decryptedDataKey); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     else { |  | ||||||
|         dfd.resolve(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return dfd.promise(); |     function computeScrypt(password, salt, callback) { | ||||||
| } |         const normalizedPassword = password.normalize('NFKC'); | ||||||
|  |         const passwordBuffer = new buffer.SlowBuffer(normalizedPassword); | ||||||
|  |         const saltBuffer = new buffer.SlowBuffer(salt); | ||||||
|  |  | ||||||
| glob.dataKey = null; |         // this settings take ~500ms on my laptop | ||||||
| glob.lastEncryptionOperationDate = null; |         const N = 16384, r = 8, p = 1; | ||||||
|  |         // 32 byte key - AES 256 | ||||||
|  |         const dkLen = 32; | ||||||
|  |  | ||||||
| function getDataKey(password) { |         const startedDate = new Date(); | ||||||
|     return computeScrypt(password, glob.encryptionSalt, (key, resolve, reject) => { |  | ||||||
|         const dataKeyAes = getDataKeyAes(key); |  | ||||||
|  |  | ||||||
|         const decryptedDataKey = decrypt(dataKeyAes, glob.encryptedDataKey); |         return new Promise((resolve, reject) => { | ||||||
|  |             scrypt(passwordBuffer, saltBuffer, N, r, p, dkLen, (error, progress, key) => { | ||||||
|  |                 if (error) { | ||||||
|  |                     console.log("Error: " + error); | ||||||
|  |  | ||||||
|         if (decryptedDataKey === false) { |                     reject(); | ||||||
|             reject("Wrong password."); |                 } | ||||||
|         } |                 else if (key) { | ||||||
|  |                     console.log("Computation took " + (new Date().getTime() - startedDate.getTime()) + "ms"); | ||||||
|  |  | ||||||
|         resolve(decryptedDataKey); |                     callback(key, resolve, reject); | ||||||
|     }); |                 } | ||||||
| } |                 else { | ||||||
|  |                     // update UI with progress complete | ||||||
| function computeScrypt(password, salt, callback) { |                 } | ||||||
|     const normalizedPassword = password.normalize('NFKC'); |             }); | ||||||
|     const passwordBuffer = new buffer.SlowBuffer(normalizedPassword); |  | ||||||
|     const saltBuffer = new buffer.SlowBuffer(salt); |  | ||||||
|  |  | ||||||
|     // this settings take ~500ms on my laptop |  | ||||||
|     const N = 16384, r = 8, 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, (error, progress, key) => { |  | ||||||
|             if (error) { |  | ||||||
|                 console.log("Error: " + error); |  | ||||||
|  |  | ||||||
|                 reject(); |  | ||||||
|             } |  | ||||||
|             else if (key) { |  | ||||||
|                 console.log("Computation took " + (new Date().getTime() - startedDate.getTime()) + "ms"); |  | ||||||
|  |  | ||||||
|                 callback(key, resolve, reject); |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 // update UI with progress complete |  | ||||||
|             } |  | ||||||
|         }); |         }); | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function decryptTreeItems() { |  | ||||||
|     if (!isEncryptionAvailable()) { |  | ||||||
|         return; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     for (const noteId of glob.allNoteIds) { |     function decryptTreeItems() { | ||||||
|         const note = getNodeByKey(noteId); |         if (!isEncryptionAvailable()) { | ||||||
|  |             return; | ||||||
|         if (note.data.encryption > 0) { |  | ||||||
|             const title = decryptString(note.data.note_title); |  | ||||||
|  |  | ||||||
|             note.setTitle(title); |  | ||||||
|         } |         } | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $("#encryption-password-form").submit(() => { |  | ||||||
|     const password = $("#encryption-password").val(); |  | ||||||
|     $("#encryption-password").val(""); |  | ||||||
|  |  | ||||||
|     getDataKey(password).then(key => { |  | ||||||
|         $("#encryption-password-dialog").dialog("close"); |  | ||||||
|  |  | ||||||
|         glob.dataKey = key; |  | ||||||
|  |  | ||||||
|         decryptTreeItems(); |  | ||||||
|  |  | ||||||
|         if (glob.encryptionDeferred !== null) { |  | ||||||
|             glob.encryptionDeferred.resolve(); |  | ||||||
|  |  | ||||||
|             glob.encryptionDeferred = null; |  | ||||||
|         } |  | ||||||
|     }) |  | ||||||
|     .catch(reason => { |  | ||||||
|         console.log(reason); |  | ||||||
|  |  | ||||||
|         error(reason); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| function resetEncryptionSession() { |  | ||||||
|     glob.dataKey = null; |  | ||||||
|  |  | ||||||
|     if (noteEditor.getCurrentNote().detail.encryption > 0) { |  | ||||||
|         noteEditor.loadNoteToEditor(noteEditor.getCurrentNoteId()); |  | ||||||
|  |  | ||||||
|         for (const noteId of glob.allNoteIds) { |         for (const noteId of glob.allNoteIds) { | ||||||
|             const note = getNodeByKey(noteId); |             const note = getNodeByKey(noteId); | ||||||
|  |  | ||||||
|             if (note.data.encryption > 0) { |             if (note.data.encryption > 0) { | ||||||
|                 note.setTitle("[encrypted]"); |                 const title = decryptString(note.data.note_title); | ||||||
|  |  | ||||||
|  |                 note.setTitle(title); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } |  | ||||||
|  |  | ||||||
| setInterval(() => { |     encryptionPasswordFormEl.submit(() => { | ||||||
|     if (glob.lastEncryptionOperationDate !== null && new Date().getTime() - glob.lastEncryptionOperationDate.getTime() > glob.encryptionSessionTimeout * 1000) { |         const password = encryptionPasswordEl.val(); | ||||||
|         resetEncryptionSession(); |         encryptionPasswordEl.val(""); | ||||||
|  |  | ||||||
|  |         getDataKey(password).then(key => { | ||||||
|  |             dialogEl.dialog("close"); | ||||||
|  |  | ||||||
|  |             dataKey = key; | ||||||
|  |  | ||||||
|  |             decryptTreeItems(); | ||||||
|  |  | ||||||
|  |             if (encryptionDeferred !== null) { | ||||||
|  |                 encryptionDeferred.resolve(); | ||||||
|  |  | ||||||
|  |                 encryptionDeferred = null; | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |             .catch(reason => { | ||||||
|  |                 console.log(reason); | ||||||
|  |  | ||||||
|  |                 error(reason); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     function resetEncryptionSession() { | ||||||
|  |         dataKey = null; | ||||||
|  |  | ||||||
|  |         if (noteEditor.getCurrentNote().detail.encryption > 0) { | ||||||
|  |             noteEditor.loadNoteToEditor(noteEditor.getCurrentNoteId()); | ||||||
|  |  | ||||||
|  |             for (const noteId of glob.allNoteIds) { | ||||||
|  |                 const note = getNodeByKey(noteId); | ||||||
|  |  | ||||||
|  |                 if (note.data.encryption > 0) { | ||||||
|  |                     note.setTitle("[encrypted]"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| }, 5000); |  | ||||||
|  |  | ||||||
| function isEncryptionAvailable() { |     setInterval(() => { | ||||||
|     return glob.dataKey !== null; |         if (lastEncryptionOperationDate !== null && new Date().getTime() - lastEncryptionOperationDate.getTime() > encryptionSessionTimeout * 1000) { | ||||||
| } |             resetEncryptionSession(); | ||||||
|  |         } | ||||||
|  |     }, 5000); | ||||||
|  |  | ||||||
| function getDataAes() { |     function isEncryptionAvailable() { | ||||||
|     glob.lastEncryptionOperationDate = new Date(); |         return dataKey !== null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return new aesjs.ModeOfOperation.ctr(glob.dataKey, new aesjs.Counter(5)); |     function getDataAes() { | ||||||
| } |         lastEncryptionOperationDate = new Date(); | ||||||
|  |  | ||||||
| function getDataKeyAes(key) { |         return new aesjs.ModeOfOperation.ctr(dataKey, new aesjs.Counter(5)); | ||||||
|     return new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); |     } | ||||||
| } |  | ||||||
|  |     function getDataKeyAes(key) { | ||||||
|  |         return new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function encryptNoteIfNecessary(note) { | ||||||
|  |         if (note.detail.encryption === 0) { | ||||||
|  |             return note; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             return encryptNote(note); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function encryptString(str) { | ||||||
|  |         return encrypt(getDataAes(), str); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function encrypt(aes, str) { | ||||||
|  |         const payload = aesjs.utils.utf8.toBytes(str); | ||||||
|  |         const digest = sha256Array(payload).slice(0, 4); | ||||||
|  |  | ||||||
|  |         const digestWithPayload = concat(digest, payload); | ||||||
|  |  | ||||||
|  |         const encryptedBytes = aes.encrypt(digestWithPayload); | ||||||
|  |  | ||||||
|  |         return uint8ToBase64(encryptedBytes); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function decryptString(encryptedBase64) { | ||||||
|  |         const decryptedBytes = decrypt(getDataAes(), encryptedBase64); | ||||||
|  |  | ||||||
|  |         return aesjs.utils.utf8.fromBytes(decryptedBytes); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function decrypt(aes, encryptedBase64) { | ||||||
|  |         const encryptedBytes = base64ToUint8Array(encryptedBase64); | ||||||
|  |  | ||||||
|  |         const decryptedBytes = aes.decrypt(encryptedBytes); | ||||||
|  |  | ||||||
|  |         const digest = decryptedBytes.slice(0, 4); | ||||||
|  |         const payload = decryptedBytes.slice(4); | ||||||
|  |  | ||||||
|  |         const hashArray = sha256Array(payload); | ||||||
|  |  | ||||||
|  |         const computedDigest = hashArray.slice(0, 4); | ||||||
|  |  | ||||||
|  |         if (!arraysIdentical(digest, computedDigest)) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return payload; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function concat(a, b) { | ||||||
|  |         const result = []; | ||||||
|  |  | ||||||
|  |         for (let key in a) { | ||||||
|  |             result.push(a[key]); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (let key in b) { | ||||||
|  |             result.push(b[key]); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function sha256Array(content) { | ||||||
|  |         const hash = sha256.create(); | ||||||
|  |         hash.update(content); | ||||||
|  |         return hash.array(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function arraysIdentical(a, b) { | ||||||
|  |         let i = a.length; | ||||||
|  |         if (i !== b.length) return false; | ||||||
|  |         while (i--) { | ||||||
|  |             if (a[i] !== b[i]) return false; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function encryptNote(note) { | ||||||
|  |         note.detail.note_title = encryptString(note.detail.note_title); | ||||||
|  |         note.detail.note_text = encryptString(note.detail.note_text); | ||||||
|  |  | ||||||
|  |         note.detail.encryption = 1; | ||||||
|  |  | ||||||
| function encryptNoteIfNecessary(note) { |  | ||||||
|     if (note.detail.encryption === 0) { |  | ||||||
|         return note; |         return note; | ||||||
|     } |     } | ||||||
|     else { |  | ||||||
|         return encryptNote(note); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function encryptString(str) { |     async function encryptNoteAndSendToServer() { | ||||||
|     return encrypt(getDataAes(), str); |         await ensureEncryptionIsAvailable(true, true); | ||||||
| } |  | ||||||
|  |  | ||||||
| function encrypt(aes, str) { |         const note = noteEditor.getCurrentNote(); | ||||||
|     const payload = aesjs.utils.utf8.toBytes(str); |  | ||||||
|     const digest = sha256Array(payload).slice(0, 4); |  | ||||||
|  |  | ||||||
|     const digestWithPayload = concat(digest, payload); |         noteEditor.updateNoteFromInputs(note); | ||||||
|  |  | ||||||
|     const encryptedBytes = aes.encrypt(digestWithPayload); |         encryptNote(note); | ||||||
|  |  | ||||||
|     return uint8ToBase64(encryptedBytes); |         await noteEditor.saveNoteToServer(note); | ||||||
| } |  | ||||||
|  |  | ||||||
| function decryptString(encryptedBase64) { |         await changeEncryptionOnNoteHistory(note.detail.note_id, true); | ||||||
|     const decryptedBytes = decrypt(getDataAes(), encryptedBase64); |  | ||||||
|  |  | ||||||
|     return aesjs.utils.utf8.fromBytes(decryptedBytes); |         noteEditor.setNoteBackgroundIfEncrypted(note); | ||||||
| } |  | ||||||
|  |  | ||||||
| function decrypt(aes, encryptedBase64) { |  | ||||||
|     const encryptedBytes = base64ToUint8Array(encryptedBase64); |  | ||||||
|  |  | ||||||
|     const decryptedBytes = aes.decrypt(encryptedBytes); |  | ||||||
|  |  | ||||||
|     const digest = decryptedBytes.slice(0, 4); |  | ||||||
|     const payload = decryptedBytes.slice(4); |  | ||||||
|  |  | ||||||
|     const hashArray = sha256Array(payload); |  | ||||||
|  |  | ||||||
|     const computedDigest = hashArray.slice(0, 4); |  | ||||||
|  |  | ||||||
|     if (!arraysIdentical(digest, computedDigest)) { |  | ||||||
|         return false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return payload; |     async function changeEncryptionOnNoteHistory(noteId, encrypt) { | ||||||
| } |         const result = await $.ajax({ | ||||||
|  |             url: baseApiUrl + 'notes-history/' + noteId + "?encryption=" + (encrypt ? 0 : 1), | ||||||
| function concat(a, b) { |             type: 'GET', | ||||||
|     const result = []; |             error: () => error("Error getting note history.") | ||||||
|  |  | ||||||
|     for (let key in a) { |  | ||||||
|         result.push(a[key]); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     for (let key in b) { |  | ||||||
|         result.push(b[key]); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function sha256Array(content) { |  | ||||||
|     const hash = sha256.create(); |  | ||||||
|     hash.update(content); |  | ||||||
|     return hash.array(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function arraysIdentical(a, b) { |  | ||||||
|     let i = a.length; |  | ||||||
|     if (i !== b.length) return false; |  | ||||||
|     while (i--) { |  | ||||||
|         if (a[i] !== b[i]) return false; |  | ||||||
|     } |  | ||||||
|     return true; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function encryptNote(note) { |  | ||||||
|     note.detail.note_title = encryptString(note.detail.note_title); |  | ||||||
|     note.detail.note_text = encryptString(note.detail.note_text); |  | ||||||
|  |  | ||||||
|     note.detail.encryption = 1; |  | ||||||
|  |  | ||||||
|     return note; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function encryptNoteAndSendToServer() { |  | ||||||
|     await handleEncryption(true, true); |  | ||||||
|  |  | ||||||
|     const note = noteEditor.getCurrentNote(); |  | ||||||
|  |  | ||||||
|     noteEditor.updateNoteFromInputs(note); |  | ||||||
|  |  | ||||||
|     encryptNote(note); |  | ||||||
|  |  | ||||||
|     await noteEditor.saveNoteToServer(note); |  | ||||||
|  |  | ||||||
|     await changeEncryptionOnNoteHistory(note.detail.note_id, true); |  | ||||||
|  |  | ||||||
|     noteEditor.setNoteBackgroundIfEncrypted(note); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function changeEncryptionOnNoteHistory(noteId, encrypt) { |  | ||||||
|     const result = await $.ajax({ |  | ||||||
|         url: baseApiUrl + 'notes-history/' + noteId + "?encryption=" + (encrypt ? 0 : 1), |  | ||||||
|         type: 'GET', |  | ||||||
|         error: () => error("Error getting note history.") |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     for (const row of result) { |  | ||||||
|         if (encrypt) { |  | ||||||
|             row.note_title = encryptString(row.note_title); |  | ||||||
|             row.note_text = encryptString(row.note_text); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             row.note_title = decryptString(row.note_title); |  | ||||||
|             row.note_text = decryptString(row.note_text); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         row.encryption = encrypt ? 1 : 0; |  | ||||||
|  |  | ||||||
|         await $.ajax({ |  | ||||||
|             url: baseApiUrl + 'notes-history', |  | ||||||
|             type: 'PUT', |  | ||||||
|             contentType: 'application/json', |  | ||||||
|             data: JSON.stringify(row), |  | ||||||
|             error: () => error("Error de/encrypting note history.") |  | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         console.log('Note history ' + row.note_history_id + ' de/encrypted'); |         for (const row of result) { | ||||||
|     } |             if (encrypt) { | ||||||
| } |                 row.note_title = encryptString(row.note_title); | ||||||
|  |                 row.note_text = encryptString(row.note_text); | ||||||
| async function decryptNoteAndSendToServer() { |  | ||||||
|     await handleEncryption(true, true); |  | ||||||
|  |  | ||||||
|     const note = noteEditor.getCurrentNote(); |  | ||||||
|  |  | ||||||
|     noteEditor.updateNoteFromInputs(note); |  | ||||||
|  |  | ||||||
|     note.detail.encryption = 0; |  | ||||||
|  |  | ||||||
|     await noteEditor.saveNoteToServer(note); |  | ||||||
|  |  | ||||||
|     await changeEncryptionOnNoteHistory(note.detail.note_id, false); |  | ||||||
|  |  | ||||||
|     noteEditor.setNoteBackgroundIfEncrypted(note); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function decryptNoteIfNecessary(note) { |  | ||||||
|     if (note.detail.encryption > 0) { |  | ||||||
|         decryptNote(note); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function decryptNote(note) { |  | ||||||
|     note.detail.note_title = decryptString(note.detail.note_title); |  | ||||||
|     note.detail.note_text = decryptString(note.detail.note_text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function encryptSubTree(noteId) { |  | ||||||
|     await handleEncryption(true, true); |  | ||||||
|  |  | ||||||
|     updateSubTreeRecursively(noteId, note => { |  | ||||||
|         if (note.detail.encryption === null || note.detail.encryption === 0) { |  | ||||||
|             encryptNote(note); |  | ||||||
|  |  | ||||||
|             note.detail.encryption = 1; |  | ||||||
|  |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|         note => { |  | ||||||
|             if (note.detail.note_id === noteEditor.getCurrentNoteId()) { |  | ||||||
|                 noteEditor.loadNoteToEditor(note.detail.note_id); |  | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|                 noteEditor.setTreeBasedOnEncryption(note); |                 row.note_title = decryptString(row.note_title); | ||||||
|             } |                 row.note_text = decryptString(row.note_text); | ||||||
|         }); |  | ||||||
|  |  | ||||||
|     message("Encryption finished."); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function decryptSubTree(noteId) { |  | ||||||
|     await handleEncryption(true, true); |  | ||||||
|  |  | ||||||
|     updateSubTreeRecursively(noteId, note => { |  | ||||||
|         if (note.detail.encryption === 1) { |  | ||||||
|             decryptNote(note); |  | ||||||
|  |  | ||||||
|             note.detail.encryption = 0; |  | ||||||
|  |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|         note => { |  | ||||||
|             if (note.detail.note_id === noteEditor.getCurrentNoteId()) { |  | ||||||
|                 noteEditor.loadNoteToEditor(note.detail.note_id); |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 noteEditor.setTreeBasedOnEncryption(note); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|     message("Decryption finished."); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function updateSubTreeRecursively(noteId, updateCallback, successCallback) { |  | ||||||
|     updateNoteSynchronously(noteId, updateCallback, successCallback); |  | ||||||
|  |  | ||||||
|     const node = getNodeByKey(noteId); |  | ||||||
|     if (!node || !node.getChildren()) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     for (const child of node.getChildren()) { |  | ||||||
|         updateSubTreeRecursively(child.key, updateCallback, successCallback); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function updateNoteSynchronously(noteId, updateCallback, successCallback) { |  | ||||||
|     $.ajax({ |  | ||||||
|         url: baseApiUrl + 'notes/' + noteId, |  | ||||||
|         type: 'GET', |  | ||||||
|         async: false, |  | ||||||
|         success: note => { |  | ||||||
|             const needSave = updateCallback(note); |  | ||||||
|  |  | ||||||
|             if (!needSave) { |  | ||||||
|                 return; |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             for (const link of note.links) { |             row.encryption = encrypt ? 1 : 0; | ||||||
|                 delete link.type; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             $.ajax({ |             await $.ajax({ | ||||||
|                 url: baseApiUrl + 'notes/' + noteId, |                 url: baseApiUrl + 'notes-history', | ||||||
|                 type: 'PUT', |                 type: 'PUT', | ||||||
|                 data: JSON.stringify(note), |                 contentType: 'application/json', | ||||||
|                 contentType: "application/json", |                 data: JSON.stringify(row), | ||||||
|                 async: false, |                 error: () => error("Error de/encrypting note history.") | ||||||
|                 success: () => { |             }); | ||||||
|                     if (successCallback) { |  | ||||||
|                         successCallback(note); |             console.log('Note history ' + row.note_history_id + ' de/encrypted'); | ||||||
|                     } |         } | ||||||
|                 }, |     } | ||||||
|                 error: () => { |  | ||||||
|                     console.log("Updating " + noteId + " failed."); |     async function decryptNoteAndSendToServer() { | ||||||
|  |         await ensureEncryptionIsAvailable(true, true); | ||||||
|  |  | ||||||
|  |         const note = noteEditor.getCurrentNote(); | ||||||
|  |  | ||||||
|  |         noteEditor.updateNoteFromInputs(note); | ||||||
|  |  | ||||||
|  |         note.detail.encryption = 0; | ||||||
|  |  | ||||||
|  |         await noteEditor.saveNoteToServer(note); | ||||||
|  |  | ||||||
|  |         await changeEncryptionOnNoteHistory(note.detail.note_id, false); | ||||||
|  |  | ||||||
|  |         noteEditor.setNoteBackgroundIfEncrypted(note); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function decryptNoteIfNecessary(note) { | ||||||
|  |         if (note.detail.encryption > 0) { | ||||||
|  |             decryptNote(note); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function decryptNote(note) { | ||||||
|  |         note.detail.note_title = decryptString(note.detail.note_title); | ||||||
|  |         note.detail.note_text = decryptString(note.detail.note_text); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function encryptSubTree(noteId) { | ||||||
|  |         await ensureEncryptionIsAvailable(true, true); | ||||||
|  |  | ||||||
|  |         updateSubTreeRecursively(noteId, note => { | ||||||
|  |                 if (note.detail.encryption === null || note.detail.encryption === 0) { | ||||||
|  |                     encryptNote(note); | ||||||
|  |  | ||||||
|  |                     note.detail.encryption = 1; | ||||||
|  |  | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             note => { | ||||||
|  |                 if (note.detail.note_id === noteEditor.getCurrentNoteId()) { | ||||||
|  |                     noteEditor.loadNoteToEditor(note.detail.note_id); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     noteEditor.setTreeBasedOnEncryption(note); | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
|         }, |  | ||||||
|         error: () => { |         message("Encryption finished."); | ||||||
|             console.log("Reading " + noteId + " failed."); |     } | ||||||
|  |  | ||||||
|  |     async function decryptSubTree(noteId) { | ||||||
|  |         await ensureEncryptionIsAvailable(true, true); | ||||||
|  |  | ||||||
|  |         updateSubTreeRecursively(noteId, note => { | ||||||
|  |                 if (note.detail.encryption === 1) { | ||||||
|  |                     decryptNote(note); | ||||||
|  |  | ||||||
|  |                     note.detail.encryption = 0; | ||||||
|  |  | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             note => { | ||||||
|  |                 if (note.detail.note_id === noteEditor.getCurrentNoteId()) { | ||||||
|  |                     noteEditor.loadNoteToEditor(note.detail.note_id); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     noteEditor.setTreeBasedOnEncryption(note); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |         message("Decryption finished."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function updateSubTreeRecursively(noteId, updateCallback, successCallback) { | ||||||
|  |         updateNoteSynchronously(noteId, updateCallback, successCallback); | ||||||
|  |  | ||||||
|  |         const node = getNodeByKey(noteId); | ||||||
|  |         if (!node || !node.getChildren()) { | ||||||
|  |             return; | ||||||
|         } |         } | ||||||
|     }); |  | ||||||
| } |         for (const child of node.getChildren()) { | ||||||
|  |             updateSubTreeRecursively(child.key, updateCallback, successCallback); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function updateNoteSynchronously(noteId, updateCallback, successCallback) { | ||||||
|  |         $.ajax({ | ||||||
|  |             url: baseApiUrl + 'notes/' + noteId, | ||||||
|  |             type: 'GET', | ||||||
|  |             async: false, | ||||||
|  |             success: note => { | ||||||
|  |                 const needSave = updateCallback(note); | ||||||
|  |  | ||||||
|  |                 if (!needSave) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 for (const link of note.links) { | ||||||
|  |                     delete link.type; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 $.ajax({ | ||||||
|  |                     url: baseApiUrl + 'notes/' + noteId, | ||||||
|  |                     type: 'PUT', | ||||||
|  |                     data: JSON.stringify(note), | ||||||
|  |                     contentType: "application/json", | ||||||
|  |                     async: false, | ||||||
|  |                     success: () => { | ||||||
|  |                         if (successCallback) { | ||||||
|  |                             successCallback(note); | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     error: () => { | ||||||
|  |                         console.log("Updating " + noteId + " failed."); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             }, | ||||||
|  |             error: () => { | ||||||
|  |                 console.log("Reading " + noteId + " failed."); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |         setEncryptionSalt, | ||||||
|  |         setEncryptedDataKey, | ||||||
|  |         setEncryptionSessionTimeout, | ||||||
|  |         ensureEncryptionIsAvailable, | ||||||
|  |         decryptTreeItems, | ||||||
|  |         resetEncryptionSession, | ||||||
|  |         isEncryptionAvailable, | ||||||
|  |         encryptNoteIfNecessary, | ||||||
|  |         encryptString, | ||||||
|  |         decryptString, | ||||||
|  |         encryptNoteAndSendToServer, | ||||||
|  |         decryptNoteAndSendToServer, | ||||||
|  |         decryptNoteIfNecessary, | ||||||
|  |         encryptSubTree, | ||||||
|  |         decryptSubTree | ||||||
|  |     }; | ||||||
|  | })(); | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| const noteEditor = (function() { | const noteEditor = (function() { | ||||||
|     const noteTitleEl = $("#note-title"); |     const noteTitleEl = $("#note-title"); | ||||||
|     const noteDetailEl = $('#note-detail'); |     const noteDetailEl = $('#note-detail'); | ||||||
|     const noteEditableEl = $(".note-editable"); |  | ||||||
|     const encryptButton = $("#encrypt-button"); |     const encryptButton = $("#encrypt-button"); | ||||||
|     const decryptButton = $("#decrypt-button"); |     const decryptButton = $("#decrypt-button"); | ||||||
|     const noteDetailWrapperEl = $("#note-detail-wrapper"); |     const noteDetailWrapperEl = $("#note-detail-wrapper"); | ||||||
| @@ -44,7 +43,7 @@ const noteEditor = (function() { | |||||||
|  |  | ||||||
|         updateNoteFromInputs(note); |         updateNoteFromInputs(note); | ||||||
|  |  | ||||||
|         encryptNoteIfNecessary(note); |         encryption.encryptNoteIfNecessary(note); | ||||||
|  |  | ||||||
|         await saveNoteToServer(note); |         await saveNoteToServer(note); | ||||||
|     } |     } | ||||||
| @@ -112,12 +111,12 @@ const noteEditor = (function() { | |||||||
|     async function createNote(node, parentKey, target, encryption) { |     async function createNote(node, parentKey, target, encryption) { | ||||||
|         // if encryption isn't available (user didn't enter password yet), then note is created as unencrypted |         // if encryption isn't available (user didn't enter password yet), then note is created as unencrypted | ||||||
|         // but this is quite weird since user doesn't see where the note is being created so it shouldn't occur often |         // but this is quite weird since user doesn't see where the note is being created so it shouldn't occur often | ||||||
|         if (!encryption || !isEncryptionAvailable()) { |         if (!encryption || !encryption.isEncryptionAvailable()) { | ||||||
|             encryption = 0; |             encryption = 0; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const newNoteName = "new note"; |         const newNoteName = "new note"; | ||||||
|         const newNoteNameEncryptedIfNecessary = encryption > 0 ? encryptString(newNoteName) : newNoteName; |         const newNoteNameEncryptedIfNecessary = encryption > 0 ? encryption.encryptString(newNoteName) : newNoteName; | ||||||
|  |  | ||||||
|         const result = await $.ajax({ |         const result = await $.ajax({ | ||||||
|             url: baseApiUrl + 'notes/' + parentKey + '/children' , |             url: baseApiUrl + 'notes/' + parentKey + '/children' , | ||||||
| @@ -163,12 +162,12 @@ const noteEditor = (function() { | |||||||
|  |  | ||||||
|     function setNoteBackgroundIfEncrypted(note) { |     function setNoteBackgroundIfEncrypted(note) { | ||||||
|         if (note.detail.encryption > 0) { |         if (note.detail.encryption > 0) { | ||||||
|             noteEditableEl.addClass("encrypted"); |             $(".note-editable").addClass("encrypted"); | ||||||
|             encryptButton.hide(); |             encryptButton.hide(); | ||||||
|             decryptButton.show(); |             decryptButton.show(); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             noteEditableEl.removeClass("encrypted"); |             $(".note-editable").removeClass("encrypted"); | ||||||
|             encryptButton.show(); |             encryptButton.show(); | ||||||
|             decryptButton.hide(); |             decryptButton.hide(); | ||||||
|         } |         } | ||||||
| @@ -187,7 +186,7 @@ const noteEditor = (function() { | |||||||
|             noteTitleEl.focus().select(); |             noteTitleEl.focus().select(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         await handleEncryption(note.detail.encryption > 0, false); |         await encryption.ensureEncryptionIsAvailable(note.detail.encryption > 0, false); | ||||||
|  |  | ||||||
|         noteDetailWrapperEl.show(); |         noteDetailWrapperEl.show(); | ||||||
|  |  | ||||||
| @@ -199,7 +198,7 @@ const noteEditor = (function() { | |||||||
|  |  | ||||||
|         encryptionPasswordEl.val(''); |         encryptionPasswordEl.val(''); | ||||||
|  |  | ||||||
|         decryptNoteIfNecessary(note); |         encryption.decryptNoteIfNecessary(note); | ||||||
|  |  | ||||||
|         noteTitleEl.val(note.detail.note_title); |         noteTitleEl.val(note.detail.note_title); | ||||||
|  |  | ||||||
| @@ -222,11 +221,11 @@ const noteEditor = (function() { | |||||||
|     async function loadNote(noteId) { |     async function loadNote(noteId) { | ||||||
|         const note = await $.get(baseApiUrl + 'notes/' + noteId); |         const note = await $.get(baseApiUrl + 'notes/' + noteId); | ||||||
|  |  | ||||||
|         if (note.detail.encryption > 0 && !isEncryptionAvailable()) { |         if (note.detail.encryption > 0 && !encryption.isEncryptionAvailable()) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         decryptNoteIfNecessary(note); |         encryption.decryptNoteIfNecessary(note); | ||||||
|  |  | ||||||
|         return note; |         return note; | ||||||
|     } |     } | ||||||
| @@ -245,7 +244,7 @@ const noteEditor = (function() { | |||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         // so that tab jumps from note title (which has tabindex 1) |         // so that tab jumps from note title (which has tabindex 1) | ||||||
|         noteEditableEl.attr("tabindex", 2); |         $(".note-editable").attr("tabindex", 2); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     setInterval(saveNoteIfChanged, 5000); |     setInterval(saveNoteIfChanged, 5000); | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ async function checkStatus() { | |||||||
|         // this will also reload the note content |         // this will also reload the note content | ||||||
|         await glob.tree.fancytree('getTree').reload(treeResp.notes); |         await glob.tree.fancytree('getTree').reload(treeResp.notes); | ||||||
|  |  | ||||||
|         decryptTreeItems(); |         encryption.decryptTreeItems(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $("#changesToPushCount").html(resp.changesToPushCount); |     $("#changesToPushCount").html(resp.changesToPushCount); | ||||||
|   | |||||||
| @@ -81,9 +81,6 @@ function setExpandedToServer(note_id, is_expanded) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| glob.encryptionSalt; |  | ||||||
| glob.encryptionSessionTimeout; |  | ||||||
| glob.encryptedDataKey; |  | ||||||
| glob.treeLoadTime; | glob.treeLoadTime; | ||||||
|  |  | ||||||
| function initFancyTree(notes, startNoteId) { | function initFancyTree(notes, startNoteId) { | ||||||
| @@ -181,9 +178,9 @@ function loadTree() { | |||||||
|     return $.get(baseApiUrl + 'tree').then(resp => { |     return $.get(baseApiUrl + 'tree').then(resp => { | ||||||
|         const notes = resp.notes; |         const notes = resp.notes; | ||||||
|         let startNoteId = resp.start_note_id; |         let startNoteId = resp.start_note_id; | ||||||
|         glob.encryptionSalt = resp.password_derived_key_salt; |         encryption.setEncryptionSalt(resp.password_derived_key_salt); | ||||||
|         glob.encryptionSessionTimeout = resp.encryption_session_timeout; |         encryption.setEncryptionSessionTimeout(resp.encryption_session_timeout); | ||||||
|         glob.encryptedDataKey = resp.encrypted_data_key; |         encryption.setEncryptedDataKey(resp.encrypted_data_key); | ||||||
|         glob.treeLoadTime = resp.tree_load_time; |         glob.treeLoadTime = resp.tree_load_time; | ||||||
|  |  | ||||||
|         // add browser ID header to all AJAX requests |         // add browser ID header to all AJAX requests | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ function getFullName(noteId) { | |||||||
|     const path = []; |     const path = []; | ||||||
|  |  | ||||||
|     while (note) { |     while (note) { | ||||||
|         if (note.data.encryption > 0 && !isEncryptionAvailable()) { |         if (note.data.encryption > 0 && !encryption.isEncryptionAvailable()) { | ||||||
|             path.push("[encrypted]"); |             path.push("[encrypted]"); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ | |||||||
|  |  | ||||||
|       <div class="hide-toggle" style="grid-area: title;"> |       <div class="hide-toggle" style="grid-area: title;"> | ||||||
|         <div style="display: flex; align-items: center;"> |         <div style="display: flex; align-items: center;"> | ||||||
|           <a onclick="encryptNoteAndSendToServer()" |           <a onclick="encryption.encryptNoteAndSendToServer()" | ||||||
|              title="Encrypt the note so that password will be required to view the note" |              title="Encrypt the note so that password will be required to view the note" | ||||||
|              class="icon-action" |              class="icon-action" | ||||||
|              id="encrypt-button" |              id="encrypt-button" | ||||||
| @@ -72,7 +72,7 @@ | |||||||
|             <img src="images/icons/lock.png" alt="Encrypt note"/> |             <img src="images/icons/lock.png" alt="Encrypt note"/> | ||||||
|           </a> |           </a> | ||||||
|  |  | ||||||
|           <a onclick="decryptNoteAndSendToServer()" |           <a onclick="encryption.decryptNoteAndSendToServer()" | ||||||
|              title="Decrypt note permamently so that password will not be required to access this note in the future" |              title="Decrypt note permamently so that password will not be required to access this note in the future" | ||||||
|              class="icon-action" |              class="icon-action" | ||||||
|              id="decrypt-button" |              id="decrypt-button" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user