diff --git a/migrations/0030__hello_world.js b/migrations/0030__hello_world.js new file mode 100644 index 000000000..15695bf87 --- /dev/null +++ b/migrations/0030__hello_world.js @@ -0,0 +1 @@ +module.exports = async () => console.log("heeeelllooo!!!"); \ No newline at end of file diff --git a/migrations/0031__change_encryption_to_CBC.js b/migrations/0031__change_encryption_to_CBC.js new file mode 100644 index 000000000..3d77805c0 --- /dev/null +++ b/migrations/0031__change_encryption_to_CBC.js @@ -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); +}; \ No newline at end of file diff --git a/package.json b/package.json index 526abab3e..d2d951e85 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node ./bin/www", "test-electron": "xo", - "rebuild-electron": "electron-rebuild", + "rebuild-electron": "electron-rebuild", "start-electron": "electron .", "build-electron": "electron-packager . --out=dist --asar --overwrite --all", "start-forge": "electron-forge start", @@ -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": { diff --git a/routes/api/note_history.js b/routes/api/note_history.js index 5989ac712..fd4fc7cae 100644 --- a/routes/api/note_history.js +++ b/routes/api/note_history.js @@ -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); } } diff --git a/routes/api/notes.js b/routes/api/notes.js index be9f0768d..aba03ade8 100644 --- a/routes/api/notes.js +++ b/routes/api/notes.js @@ -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({ diff --git a/routes/api/tree.js b/routes/api/tree.js index 23615daee..dc23ac8df 100644 --- a/routes/api/tree.js +++ b/routes/api/tree.js @@ -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 = []; diff --git a/services/data_encryption.js b/services/data_encryption.js index 6c7a0fec0..6889fc24f 100644 --- a/services/data_encryption.js +++ b/services/data_encryption.js @@ -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 }; \ No newline at end of file diff --git a/services/migration.js b/services/migration.js index c132d64f7..da7d2eceb 100644 --- a/services/migration.js +++ b/services/migration.js @@ -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 () => { - await sql.executeScript(migrationSql); + 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); }); diff --git a/services/notes.js b/services/notes.js index eaaa46704..25316cd53 100644 --- a/services/notes.js +++ b/services/notes.js @@ -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; } diff --git a/test/cbc_encryption.js b/test/cbc_encryption.js new file mode 100644 index 000000000..0ae3c9550 --- /dev/null +++ b/test/cbc_encryption.js @@ -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(); +}); \ No newline at end of file