mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-29 01:06:36 +01:00 
			
		
		
		
	changing from AES-256-CTR to AES-128-CBC for note encryption
This commit is contained in:
		
							
								
								
									
										1
									
								
								migrations/0030__hello_world.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								migrations/0030__hello_world.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| module.exports = async () => console.log("heeeelllooo!!!"); | ||||
							
								
								
									
										56
									
								
								migrations/0031__change_encryption_to_CBC.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								migrations/0031__change_encryption_to_CBC.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| const sql = require('../services/sql'); | ||||
| const data_encryption = require('../services/data_encryption'); | ||||
| const password_encryption = require('../services/password_encryption'); | ||||
| const my_scrypt = require('../services/my_scrypt'); | ||||
| const readline = require('readline'); | ||||
|  | ||||
| const cl = readline.createInterface(process.stdin, process.stdout); | ||||
|  | ||||
| function question(q) { | ||||
|     return new Promise( (res, rej) => { | ||||
|         cl.question( q, answer => { | ||||
|             res(answer); | ||||
|         }) | ||||
|     }); | ||||
| } | ||||
|  | ||||
| module.exports = async () => { | ||||
|     const password = await question("Enter password: "); | ||||
|     const dataKey = await password_encryption.getDecryptedDataKey(password); | ||||
|  | ||||
|     const protectedNotes = await sql.getResults("SELECT * FROM notes WHERE is_protected = 1"); | ||||
|  | ||||
|     for (const note of protectedNotes) { | ||||
|         console.log("Encrypted: ", note.note_title); | ||||
|  | ||||
|         const decryptedTitle = data_encryption.decrypt(dataKey, note.note_title); | ||||
|  | ||||
|         console.log("Decrypted title: ", decryptedTitle); | ||||
|  | ||||
|         note.note_title = data_encryption.encryptCbc(dataKey, "0" + note.note_id, decryptedTitle); | ||||
|  | ||||
|         const decryptedText = data_encryption.decrypt(dataKey, note.note_text); | ||||
|         note.note_text = data_encryption.encryptCbc(dataKey, "1" + note.note_id, decryptedText); | ||||
|  | ||||
|         await sql.execute("UPDATE notes SET note_title = ?, note_text = ? WHERE note_id = ?", [note.note_title, note.note_text, note.note_id]); | ||||
|     } | ||||
|  | ||||
|     const protectedNotesHistory = await sql.getResults("SELECT * FROM notes_history WHERE is_protected = 1"); | ||||
|  | ||||
|     for (const noteHistory of protectedNotesHistory) { | ||||
|         const decryptedTitle = data_encryption.decrypt(dataKey, noteHistory.note_title); | ||||
|         noteHistory.note_title = data_encryption.encryptCbc(dataKey, "0" + noteHistory.note_history_id, decryptedTitle); | ||||
|  | ||||
|         const decryptedText = data_encryption.decrypt(dataKey, noteHistory.note_text); | ||||
|         noteHistory.note_text = data_encryption.encryptCbc(dataKey, "1" + noteHistory.note_history_id, decryptedText); | ||||
|  | ||||
|         await sql.execute("UPDATE notes SET note_title = ?, note_text = ? WHERE note_id = ?", [noteHistory.note_title, noteHistory.note_text, noteHistory.note_history_id]); | ||||
|     } | ||||
|  | ||||
|     const passwordDerivedKey = await my_scrypt.getPasswordDerivedKey(password); | ||||
|  | ||||
|     // trimming to 128bits (for AES-128) | ||||
|     const trimmedDataKey = dataKey.slice(0, 16); | ||||
|  | ||||
|     await password_encryption.encryptDataKey(passwordDerivedKey, trimmedDataKey); | ||||
| }; | ||||
| @@ -40,6 +40,7 @@ | ||||
|     "electron-packager": "^8.0.0", | ||||
|     "electron-prebuilt-compile": "1.8.2-beta.2", | ||||
|     "electron-rebuild": "^1.6.0", | ||||
|     "tape": "^4.8.0", | ||||
|     "xo": "^0.18.0" | ||||
|   }, | ||||
|   "config": { | ||||
|   | ||||
| @@ -15,8 +15,8 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => { | ||||
|  | ||||
|     for (const hist of history) { | ||||
|         if (hist.is_protected) { | ||||
|             hist.note_title = data_encryption.decrypt(dataKey, hist.note_title); | ||||
|             hist.note_text = data_encryption.decrypt(dataKey, hist.note_text); | ||||
|             hist.note_title = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(hist.note_history_id), hist.note_title); | ||||
|             hist.note_text = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(hist.note_history_id), hist.note_text); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -21,8 +21,8 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => { | ||||
|     if (detail.is_protected) { | ||||
|         const dataKey = protected_session.getDataKey(req); | ||||
|  | ||||
|         detail.note_title = data_encryption.decrypt(dataKey, detail.note_title); | ||||
|         detail.note_text = data_encryption.decrypt(dataKey, detail.note_text); | ||||
|         detail.note_title = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(noteId), detail.note_title); | ||||
|         detail.note_text = data_encryption.decryptCbc(dataKey, data_encryption.noteTextIv(noteId), detail.note_text); | ||||
|     } | ||||
|  | ||||
|     res.send({ | ||||
|   | ||||
| @@ -28,7 +28,7 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => { | ||||
|  | ||||
|     for (const note of notes) { | ||||
|         if (note.is_protected) { | ||||
|             note.note_title = data_encryption.decrypt(dataKey, note.note_title); | ||||
|             note.note_title = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(note.note_id), note.note_title); | ||||
|         } | ||||
|  | ||||
|         note.children = []; | ||||
|   | ||||
| @@ -12,6 +12,15 @@ function getDataAes(dataKey) { | ||||
|     return new aesjs.ModeOfOperation.ctr(dataKey, new aesjs.Counter(5)); | ||||
| } | ||||
|  | ||||
| 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 decrypt(dataKey, encryptedBase64) { | ||||
|     if (!dataKey) { | ||||
|         return "[protected]"; | ||||
| @@ -54,21 +63,78 @@ function encrypt(dataKey, plainText) { | ||||
|     return utils.toBase64(encryptedBytes); | ||||
| } | ||||
|  | ||||
| function shaArray(content) { | ||||
|     // we use this as simple checksum and don't rely on its security so SHA-1 is good enough | ||||
|     return crypto.createHash('sha1').update(content).digest('base64'); | ||||
| } | ||||
|  | ||||
| function sha256Array(content) { | ||||
|     return crypto.createHash('sha256').update(content).digest(); | ||||
| } | ||||
|  | ||||
| function arraysIdentical(a, b) { | ||||
|     let i = a.length; | ||||
|     if (i !== b.length) return false; | ||||
|     while (i--) { | ||||
|         if (a[i] !== b[i]) return false; | ||||
| function pad(data) { | ||||
|     let padded = Array.from(data); | ||||
|  | ||||
|     if (data.length >= 16) { | ||||
|         padded = padded.slice(0, 16); | ||||
|     } | ||||
|     return true; | ||||
|     else { | ||||
|         padded = padded.concat(Array(16 - padded.length).fill(0)); | ||||
|     } | ||||
|  | ||||
|     return Buffer.from(padded); | ||||
| } | ||||
|  | ||||
| function encryptCbc(dataKey, iv, plainText) { | ||||
|     if (!dataKey) { | ||||
|         throw new Error("No data key!"); | ||||
|     } | ||||
|  | ||||
|     const cipher = crypto.createCipheriv('aes-128-cbc', pad(dataKey), pad(iv)); | ||||
|  | ||||
|     const digest = shaArray(plainText).slice(0, 4); | ||||
|  | ||||
|     const digestWithPayload = digest + plainText; | ||||
|  | ||||
|     const encryptedData = cipher.update(digestWithPayload, 'utf8', 'base64') + cipher.final('base64'); | ||||
|  | ||||
|     return encryptedData; | ||||
| } | ||||
|  | ||||
| function decryptCbc(dataKey, iv, cipherText) { | ||||
|     if (!dataKey) { | ||||
|         return "[protected]"; | ||||
|     } | ||||
|  | ||||
|     const decipher = crypto.createDecipheriv('aes-128-cbc', pad(dataKey), pad(iv)); | ||||
|     const decryptedBytes  = decipher.update(cipherText, 'base64', 'utf-8') + decipher.final('utf-8'); | ||||
|  | ||||
|     const digest = decryptedBytes.slice(0, 4); | ||||
|     const payload = decryptedBytes.slice(4); | ||||
|  | ||||
|     const computedDigest = shaArray(payload).slice(0, 4); | ||||
|  | ||||
|     if (!arraysIdentical(digest, computedDigest)) { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     return payload; | ||||
| } | ||||
|  | ||||
| function noteTitleIv(iv) { | ||||
|     return "0" + iv; | ||||
| } | ||||
|  | ||||
| function noteTextIv(iv) { | ||||
|     return "1" + iv; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     getProtectedSessionId, | ||||
|     decrypt, | ||||
|     encrypt | ||||
|     encrypt, | ||||
|     encryptCbc, | ||||
|     decryptCbc, | ||||
|     noteTitleIv, | ||||
|     noteTextIv | ||||
| }; | ||||
| @@ -4,8 +4,8 @@ const options = require('./options'); | ||||
| const fs = require('fs-extra'); | ||||
| const log = require('./log'); | ||||
|  | ||||
| const APP_DB_VERSION = 29; | ||||
| const MIGRATIONS_DIR = "./migrations"; | ||||
| const APP_DB_VERSION = 31; | ||||
| const MIGRATIONS_DIR = "migrations"; | ||||
|  | ||||
| async function migrate() { | ||||
|     const migrations = []; | ||||
| @@ -16,18 +16,20 @@ async function migrate() { | ||||
|     const currentDbVersion = parseInt(await options.getOption('db_version')); | ||||
|  | ||||
|     fs.readdirSync(MIGRATIONS_DIR).forEach(file => { | ||||
|         const match = file.match(/([0-9]{4})__([a-zA-Z0-9_ ]+)\.sql/); | ||||
|         const match = file.match(/([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)/); | ||||
|  | ||||
|         if (match) { | ||||
|             const dbVersion = parseInt(match[1]); | ||||
|  | ||||
|             if (dbVersion > currentDbVersion) { | ||||
|                 const name = match[2]; | ||||
|                 const type = match[3]; | ||||
|  | ||||
|                 const migrationRecord = { | ||||
|                     dbVersion: dbVersion, | ||||
|                     name: name, | ||||
|                     file: file | ||||
|                     file: file, | ||||
|                     type: type | ||||
|                 }; | ||||
|  | ||||
|                 migrations.push(migrationRecord); | ||||
| @@ -38,13 +40,26 @@ async function migrate() { | ||||
|     migrations.sort((a, b) => a.dbVersion - b.dbVersion); | ||||
|  | ||||
|     for (const mig of migrations) { | ||||
|         const migrationSql = fs.readFileSync(MIGRATIONS_DIR + "/" + mig.file).toString('utf8'); | ||||
|  | ||||
|         try { | ||||
|             log.info("Attempting migration to version " + mig.dbVersion + " with script: " + migrationSql); | ||||
|             log.info("Attempting migration to version " + mig.dbVersion); | ||||
|  | ||||
|             await sql.doInTransaction(async () => { | ||||
|                 if (mig.type === 'sql') { | ||||
|                     const migrationSql = fs.readFileSync(MIGRATIONS_DIR + "/" + mig.file).toString('utf8'); | ||||
|  | ||||
|                     console.log("Migration with SQL script: " + migrationSql); | ||||
|  | ||||
|                     await sql.executeScript(migrationSql); | ||||
|                 } | ||||
|                 else if (mig.type === 'js') { | ||||
|                     console.log("Migration with JS module"); | ||||
|  | ||||
|                     const migrationModule = require("../" + MIGRATIONS_DIR + "/" + mig.file); | ||||
|                     await migrationModule(); | ||||
|                 } | ||||
|                 else { | ||||
|                     throw new Error("Unknown migration type " + mig.type); | ||||
|                 } | ||||
|  | ||||
|                 await options.setOption("db_version", mig.dbVersion); | ||||
|             }); | ||||
|   | ||||
| @@ -65,8 +65,8 @@ async function createNewNote(parentNoteId, note, browserId) { | ||||
| } | ||||
|  | ||||
| async function encryptNote(note, ctx) { | ||||
|     note.detail.note_title = data_encryption.encrypt(ctx.getDataKey(), note.detail.note_title); | ||||
|     note.detail.note_text = data_encryption.encrypt(ctx.getDataKey(), note.detail.note_text); | ||||
|     note.detail.note_title = data_encryption.encryptCbc(ctx.getDataKey(), data_encryption.noteTitleIv(note.detail.note_id), note.detail.note_title); | ||||
|     note.detail.note_text = data_encryption.encryptCbc(ctx.getDataKey(), data_encryption.noteTextIv(note.detail.note_id), note.detail.note_text); | ||||
| } | ||||
|  | ||||
| async function protectNoteRecursively(noteId, dataKey, protect) { | ||||
| @@ -85,15 +85,15 @@ async function protectNote(note, dataKey, protect) { | ||||
|     let changed = false; | ||||
|  | ||||
|     if (protect && !note.is_protected) { | ||||
|         note.note_title = data_encryption.encrypt(dataKey, note.note_title); | ||||
|         note.note_text = data_encryption.encrypt(dataKey, note.note_text); | ||||
|         note.note_title = data_encryption.encryptCbc(dataKey, data_encryption.noteTitleIv(note.note_id), note.note_title); | ||||
|         note.note_text = data_encryption.encryptCbc(dataKey, data_encryption.noteTextIv(note.note_id), note.note_text); | ||||
|         note.is_protected = true; | ||||
|  | ||||
|         changed = true; | ||||
|     } | ||||
|     else if (!protect && note.is_protected) { | ||||
|         note.note_title = data_encryption.decrypt(dataKey, note.note_title); | ||||
|         note.note_text = data_encryption.decrypt(dataKey, note.note_text); | ||||
|         note.note_title = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(note.note_id), note.note_title); | ||||
|         note.note_text = data_encryption.decryptCbc(dataKey, data_encryption.noteTextIv(note.note_id), note.note_text); | ||||
|         note.is_protected = false; | ||||
|  | ||||
|         changed = true; | ||||
| @@ -116,13 +116,13 @@ async function protectNoteHistory(noteId, dataKey, protect) { | ||||
|  | ||||
|     for (const history of historyToChange) { | ||||
|         if (protect) { | ||||
|             history.note_title = data_encryption.encrypt(dataKey, history.note_title); | ||||
|             history.note_text = data_encryption.encrypt(dataKey, history.note_text); | ||||
|             history.note_title = data_encryption.encryptCbc(dataKey, data_encryption.noteTitleIv(history.note_history_id), history.note_title); | ||||
|             history.note_text = data_encryption.encryptCbc(dataKey, data_encryption.noteTextIv(history.note_history_id), history.note_text); | ||||
|             history.is_protected = true; | ||||
|         } | ||||
|         else { | ||||
|             history.note_title = data_encryption.decrypt(dataKey, history.note_title); | ||||
|             history.note_text = data_encryption.decrypt(dataKey, history.note_text); | ||||
|             history.note_title = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(history.note_history_id), history.note_title); | ||||
|             history.note_text = data_encryption.decryptCbc(dataKey, data_encryption.noteTextIv(history.note_history_id), history.note_text); | ||||
|             history.is_protected = false; | ||||
|         } | ||||
|  | ||||
|   | ||||
							
								
								
									
										14
									
								
								test/cbc_encryption.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								test/cbc_encryption.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| const test = require('tape'); | ||||
| const data_encryption = require('../services/data_encryption'); | ||||
|  | ||||
| test('encrypt & decrypt', t => { | ||||
|     const dataKey = [1,2,3]; | ||||
|     const iv = [4,5,6]; | ||||
|     const plainText = "Hello World!"; | ||||
|  | ||||
|     const cipherText = data_encryption.encryptCbc(dataKey, iv, plainText); | ||||
|     const decodedPlainText = data_encryption.decryptCbc(dataKey, iv, cipherText); | ||||
|  | ||||
|     t.equal(decodedPlainText, plainText); | ||||
|     t.end(); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user