mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +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-packager": "^8.0.0", | ||||||
|     "electron-prebuilt-compile": "1.8.2-beta.2", |     "electron-prebuilt-compile": "1.8.2-beta.2", | ||||||
|     "electron-rebuild": "^1.6.0", |     "electron-rebuild": "^1.6.0", | ||||||
|  |     "tape": "^4.8.0", | ||||||
|     "xo": "^0.18.0" |     "xo": "^0.18.0" | ||||||
|   }, |   }, | ||||||
|   "config": { |   "config": { | ||||||
|   | |||||||
| @@ -15,8 +15,8 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => { | |||||||
|  |  | ||||||
|     for (const hist of history) { |     for (const hist of history) { | ||||||
|         if (hist.is_protected) { |         if (hist.is_protected) { | ||||||
|             hist.note_title = data_encryption.decrypt(dataKey, hist.note_title); |             hist.note_title = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(hist.note_history_id), hist.note_title); | ||||||
|             hist.note_text = data_encryption.decrypt(dataKey, hist.note_text); |             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) { |     if (detail.is_protected) { | ||||||
|         const dataKey = protected_session.getDataKey(req); |         const dataKey = protected_session.getDataKey(req); | ||||||
|  |  | ||||||
|         detail.note_title = data_encryption.decrypt(dataKey, detail.note_title); |         detail.note_title = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(noteId), detail.note_title); | ||||||
|         detail.note_text = data_encryption.decrypt(dataKey, detail.note_text); |         detail.note_text = data_encryption.decryptCbc(dataKey, data_encryption.noteTextIv(noteId), detail.note_text); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => { | |||||||
|  |  | ||||||
|     for (const note of notes) { |     for (const note of notes) { | ||||||
|         if (note.is_protected) { |         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 = []; |         note.children = []; | ||||||
|   | |||||||
| @@ -12,6 +12,15 @@ function getDataAes(dataKey) { | |||||||
|     return new aesjs.ModeOfOperation.ctr(dataKey, new aesjs.Counter(5)); |     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) { | function decrypt(dataKey, encryptedBase64) { | ||||||
|     if (!dataKey) { |     if (!dataKey) { | ||||||
|         return "[protected]"; |         return "[protected]"; | ||||||
| @@ -54,21 +63,78 @@ function encrypt(dataKey, plainText) { | |||||||
|     return utils.toBase64(encryptedBytes); |     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) { | function sha256Array(content) { | ||||||
|     return crypto.createHash('sha256').update(content).digest(); |     return crypto.createHash('sha256').update(content).digest(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function arraysIdentical(a, b) { | function pad(data) { | ||||||
|     let i = a.length; |     let padded = Array.from(data); | ||||||
|     if (i !== b.length) return false; |  | ||||||
|     while (i--) { |     if (data.length >= 16) { | ||||||
|         if (a[i] !== b[i]) return false; |         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 = { | module.exports = { | ||||||
|     getProtectedSessionId, |     getProtectedSessionId, | ||||||
|     decrypt, |     decrypt, | ||||||
|     encrypt |     encrypt, | ||||||
|  |     encryptCbc, | ||||||
|  |     decryptCbc, | ||||||
|  |     noteTitleIv, | ||||||
|  |     noteTextIv | ||||||
| }; | }; | ||||||
| @@ -4,8 +4,8 @@ const options = require('./options'); | |||||||
| const fs = require('fs-extra'); | const fs = require('fs-extra'); | ||||||
| const log = require('./log'); | const log = require('./log'); | ||||||
|  |  | ||||||
| const APP_DB_VERSION = 29; | const APP_DB_VERSION = 31; | ||||||
| const MIGRATIONS_DIR = "./migrations"; | const MIGRATIONS_DIR = "migrations"; | ||||||
|  |  | ||||||
| async function migrate() { | async function migrate() { | ||||||
|     const migrations = []; |     const migrations = []; | ||||||
| @@ -16,18 +16,20 @@ async function migrate() { | |||||||
|     const currentDbVersion = parseInt(await options.getOption('db_version')); |     const currentDbVersion = parseInt(await options.getOption('db_version')); | ||||||
|  |  | ||||||
|     fs.readdirSync(MIGRATIONS_DIR).forEach(file => { |     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) { |         if (match) { | ||||||
|             const dbVersion = parseInt(match[1]); |             const dbVersion = parseInt(match[1]); | ||||||
|  |  | ||||||
|             if (dbVersion > currentDbVersion) { |             if (dbVersion > currentDbVersion) { | ||||||
|                 const name = match[2]; |                 const name = match[2]; | ||||||
|  |                 const type = match[3]; | ||||||
|  |  | ||||||
|                 const migrationRecord = { |                 const migrationRecord = { | ||||||
|                     dbVersion: dbVersion, |                     dbVersion: dbVersion, | ||||||
|                     name: name, |                     name: name, | ||||||
|                     file: file |                     file: file, | ||||||
|  |                     type: type | ||||||
|                 }; |                 }; | ||||||
|  |  | ||||||
|                 migrations.push(migrationRecord); |                 migrations.push(migrationRecord); | ||||||
| @@ -38,13 +40,26 @@ async function migrate() { | |||||||
|     migrations.sort((a, b) => a.dbVersion - b.dbVersion); |     migrations.sort((a, b) => a.dbVersion - b.dbVersion); | ||||||
|  |  | ||||||
|     for (const mig of migrations) { |     for (const mig of migrations) { | ||||||
|         const migrationSql = fs.readFileSync(MIGRATIONS_DIR + "/" + mig.file).toString('utf8'); |  | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             log.info("Attempting migration to version " + mig.dbVersion + " with script: " + migrationSql); |             log.info("Attempting migration to version " + mig.dbVersion); | ||||||
|  |  | ||||||
|             await sql.doInTransaction(async () => { |             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); |                     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); |                 await options.setOption("db_version", mig.dbVersion); | ||||||
|             }); |             }); | ||||||
|   | |||||||
| @@ -65,8 +65,8 @@ async function createNewNote(parentNoteId, note, browserId) { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function encryptNote(note, ctx) { | async function encryptNote(note, ctx) { | ||||||
|     note.detail.note_title = data_encryption.encrypt(ctx.getDataKey(), note.detail.note_title); |     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.encrypt(ctx.getDataKey(), note.detail.note_text); |     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) { | async function protectNoteRecursively(noteId, dataKey, protect) { | ||||||
| @@ -85,15 +85,15 @@ async function protectNote(note, dataKey, protect) { | |||||||
|     let changed = false; |     let changed = false; | ||||||
|  |  | ||||||
|     if (protect && !note.is_protected) { |     if (protect && !note.is_protected) { | ||||||
|         note.note_title = data_encryption.encrypt(dataKey, note.note_title); |         note.note_title = data_encryption.encryptCbc(dataKey, data_encryption.noteTitleIv(note.note_id), note.note_title); | ||||||
|         note.note_text = data_encryption.encrypt(dataKey, note.note_text); |         note.note_text = data_encryption.encryptCbc(dataKey, data_encryption.noteTextIv(note.note_id), note.note_text); | ||||||
|         note.is_protected = true; |         note.is_protected = true; | ||||||
|  |  | ||||||
|         changed = true; |         changed = true; | ||||||
|     } |     } | ||||||
|     else if (!protect && note.is_protected) { |     else if (!protect && 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.note_text = data_encryption.decrypt(dataKey, note.note_text); |         note.note_text = data_encryption.decryptCbc(dataKey, data_encryption.noteTextIv(note.note_id), note.note_text); | ||||||
|         note.is_protected = false; |         note.is_protected = false; | ||||||
|  |  | ||||||
|         changed = true; |         changed = true; | ||||||
| @@ -116,13 +116,13 @@ async function protectNoteHistory(noteId, dataKey, protect) { | |||||||
|  |  | ||||||
|     for (const history of historyToChange) { |     for (const history of historyToChange) { | ||||||
|         if (protect) { |         if (protect) { | ||||||
|             history.note_title = data_encryption.encrypt(dataKey, history.note_title); |             history.note_title = data_encryption.encryptCbc(dataKey, data_encryption.noteTitleIv(history.note_history_id), history.note_title); | ||||||
|             history.note_text = data_encryption.encrypt(dataKey, history.note_text); |             history.note_text = data_encryption.encryptCbc(dataKey, data_encryption.noteTextIv(history.note_history_id), history.note_text); | ||||||
|             history.is_protected = true; |             history.is_protected = true; | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             history.note_title = data_encryption.decrypt(dataKey, history.note_title); |             history.note_title = data_encryption.decryptCbc(dataKey, data_encryption.noteTitleIv(history.note_history_id), history.note_title); | ||||||
|             history.note_text = data_encryption.decrypt(dataKey, history.note_text); |             history.note_text = data_encryption.decryptCbc(dataKey, data_encryption.noteTextIv(history.note_history_id), history.note_text); | ||||||
|             history.is_protected = false; |             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