diff --git a/db/migrations/0122__add_iv_to_columns.js b/db/migrations/0122__add_iv_to_columns.js new file mode 100644 index 000000000..b20cd53f8 --- /dev/null +++ b/db/migrations/0122__add_iv_to_columns.js @@ -0,0 +1,62 @@ +const sql = require('../../src/services/sql'); + +function prependIv(cipherText, ivText) { + const arr = ivText.split("").map(c => parseInt(c) || 0); + const iv = Buffer.from(arr); + const payload = Buffer.from(cipherText, 'base64'); + const complete = Buffer.concat([iv, payload]); + + return complete.toString('base64'); +} + +async function updateEncryptedDataKey() { + const encryptedDataKey = await sql.getValue("SELECT value FROM options WHERE name = 'encryptedDataKey'"); + const encryptedDataKeyIv = await sql.getValue("SELECT value FROM options WHERE name = 'encryptedDataKeyIv'"); + + const newEncryptedDataKey = prependIv(encryptedDataKey, encryptedDataKeyIv); + + await sql.execute("UPDATE options SET value = ? WHERE name = 'encryptedDataKey'", [newEncryptedDataKey]); + + await sql.execute("DELETE FROM options WHERE name = 'encryptedDataKeyIv'"); + await sql.execute("DELETE FROM sync WHERE entityName = 'options' AND entityId = 'encryptedDataKeyIv'"); +} + +async function updateNotes() { + const protectedNotes = await sql.getRows("SELECT noteId, title, content FROM notes WHERE isProtected = 1"); + + for (const note of protectedNotes) { + if (note.title !== null) { + note.title = prependIv(note.title, "0" + note.noteId); + } + + if (note.content !== null) { + note.content = prependIv(note.content, "1" + note.noteId); + } + + await sql.execute("UPDATE notes SET title = ?, content = ? WHERE noteId = ?", [note.title, note.content, note.noteId]); + } +} + +async function updateNoteRevisions() { + const protectedNoteRevisions = await sql.getRows("SELECT noteRevisionId, title, content FROM note_revisions WHERE isProtected = 1"); + + for (const noteRevision of protectedNoteRevisions) { + if (noteRevision.title !== null) { + noteRevision.title = prependIv(noteRevision.title, "0" + noteRevision.noteRevisionId); + } + + if (noteRevision.content !== null) { + noteRevision.content = prependIv(noteRevision.content, "1" + noteRevision.noteRevisionId); + } + + await sql.execute("UPDATE note_revisions SET title = ?, content = ? WHERE noteRevisionId = ?", [noteRevision.title, noteRevision.content, noteRevision.noteRevisionId]); + } +} + +module.exports = async () => { + await updateEncryptedDataKey(); + + await updateNotes(); + + await updateNoteRevisions(); +}; \ No newline at end of file diff --git a/src/services/app_info.js b/src/services/app_info.js index 781d8a712..39fd52cd8 100644 --- a/src/services/app_info.js +++ b/src/services/app_info.js @@ -4,8 +4,8 @@ const build = require('./build'); const packageJson = require('../../package'); const {TRILIUM_DATA_DIR} = require('./data_dir'); -const APP_DB_VERSION = 121; -const SYNC_VERSION = 3; +const APP_DB_VERSION = 122; +const SYNC_VERSION = 4; module.exports = { appVersion: packageJson.version, diff --git a/src/services/data_encryption.js b/src/services/data_encryption.js index fc92dd794..42a77488a 100644 --- a/src/services/data_encryption.js +++ b/src/services/data_encryption.js @@ -18,25 +18,29 @@ function shaArray(content) { } function pad(data) { - let padded = Array.from(data); + if (data.length > 16) { + data = data.slice(0, 16); + } + else if (data.length < 16) { + const zeros = Array(16 - data.length).fill(0); - if (data.length >= 16) { - padded = padded.slice(0, 16); + data = Buffer.concat([data, Buffer.from(zeros)]); } else { - padded = padded.concat(Array(16 - padded.length).fill(0)); + data = Buffer.from(data); } - return Buffer.from(padded); + return data; } -function encrypt(key, iv, plainText) { +function encrypt(key, plainText, ivLength = 13) { if (!key) { throw new Error("No data key!"); } const plainTextBuffer = Buffer.from(plainText); + const iv = crypto.randomBytes(ivLength); const cipher = crypto.createCipheriv('aes-128-cbc', pad(key), pad(iv)); const digest = shaArray(plainTextBuffer).slice(0, 4); @@ -45,17 +49,23 @@ function encrypt(key, iv, plainText) { const encryptedData = Buffer.concat([cipher.update(digestWithPayload), cipher.final()]); - return encryptedData.toString('base64'); + const encryptedDataWithIv = Buffer.concat([iv, encryptedData]); + + return encryptedDataWithIv.toString('base64'); } -function decrypt(key, iv, cipherText) { +function decrypt(key, cipherText, ivLength = 13) { if (!key) { return "[protected]"; } + const cipherTextBufferWithIv = Buffer.from(cipherText, 'base64'); + const iv = cipherTextBufferWithIv.slice(0, ivLength); + + const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength); + const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv)); - const cipherTextBuffer = Buffer.from(cipherText, 'base64'); const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]); const digest = decryptedBytes.slice(0, 4); @@ -70,8 +80,8 @@ function decrypt(key, iv, cipherText) { return payload; } -function decryptString(dataKey, iv, cipherText) { - const buffer = decrypt(dataKey, iv, cipherText); +function decryptString(dataKey, cipherText) { + const buffer = decrypt(dataKey, cipherText); const str = buffer.toString('utf-8'); @@ -84,26 +94,8 @@ function decryptString(dataKey, iv, cipherText) { return str; } -function noteTitleIv(iv) { - if (!iv) { - throw new Error("Empty iv!"); - } - - return "0" + iv; -} - -function noteContentIv(iv) { - if (!iv) { - throw new Error("Empty iv!"); - } - - return "1" + iv; -} - module.exports = { encrypt, decrypt, - decryptString, - noteTitleIv, - noteContentIv + decryptString }; \ No newline at end of file diff --git a/src/services/options_init.js b/src/services/options_init.js index ea0c7d1de..bbc2e7ba9 100644 --- a/src/services/options_init.js +++ b/src/services/options_init.js @@ -24,7 +24,6 @@ async function initSyncedOptions(username, password) { // passwordEncryptionService expects these options to already exist await optionService.createOption('encryptedDataKey', '', true); - await optionService.createOption('encryptedDataKeyIv', '', true); await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true); } diff --git a/src/services/password_encryption.js b/src/services/password_encryption.js index 6bc1831e7..903bf9e62 100644 --- a/src/services/password_encryption.js +++ b/src/services/password_encryption.js @@ -14,13 +14,7 @@ async function verifyPassword(password) { async function setDataKey(password, plainTextDataKey) { const passwordDerivedKey = await myScryptService.getPasswordDerivedKey(password); - const encryptedDataKeyIv = utils.randomString(16); - - await optionService.setOption('encryptedDataKeyIv', encryptedDataKeyIv); - - const buffer = Buffer.from(plainTextDataKey); - - const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, encryptedDataKeyIv, buffer); + const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, Buffer.from(plainTextDataKey)); await optionService.setOption('encryptedDataKey', newEncryptedDataKey); } @@ -28,10 +22,9 @@ async function setDataKey(password, plainTextDataKey) { async function getDataKey(password) { const passwordDerivedKey = await myScryptService.getPasswordDerivedKey(password); - const encryptedDataKeyIv = await optionService.getOption('encryptedDataKeyIv'); const encryptedDataKey = await optionService.getOption('encryptedDataKey'); - const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKeyIv, encryptedDataKey); + const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey, 16); return decryptedDataKey; } diff --git a/src/services/protected_session.js b/src/services/protected_session.js index 3c18a3956..41e240ba8 100644 --- a/src/services/protected_session.js +++ b/src/services/protected_session.js @@ -38,9 +38,7 @@ function decryptNoteTitle(noteId, encryptedTitle) { const dataKey = getDataKey(); try { - const iv = dataEncryptionService.noteTitleIv(noteId); - - return dataEncryptionService.decryptString(dataKey, iv, encryptedTitle); + return dataEncryptionService.decryptString(dataKey, encryptedTitle); } catch (e) { e.message = `Cannot decrypt note title for noteId=${noteId}: ` + e.message; @@ -57,17 +55,15 @@ function decryptNote(note) { try { if (note.title) { - note.title = dataEncryptionService.decryptString(dataKey, dataEncryptionService.noteTitleIv(note.noteId), note.title); + note.title = dataEncryptionService.decryptString(dataKey, note.title); } if (note.content) { - const contentIv = dataEncryptionService.noteContentIv(note.noteId); - - if (note.type === 'file') { - note.content = dataEncryptionService.decrypt(dataKey, contentIv, note.content); + if (note.type === 'file' || note.type === 'image') { + note.content = dataEncryptionService.decrypt(dataKey, note.content); } else { - note.content = dataEncryptionService.decryptString(dataKey, contentIv, note.content); + note.content = dataEncryptionService.decryptString(dataKey, note.content); } } } @@ -91,26 +87,26 @@ function decryptNoteRevision(hist) { } if (hist.title) { - hist.title = dataEncryptionService.decryptString(dataKey, dataEncryptionService.noteTitleIv(hist.noteRevisionId), hist.title); + hist.title = dataEncryptionService.decryptString(dataKey, hist.title); } if (hist.content) { - hist.content = dataEncryptionService.decryptString(dataKey, dataEncryptionService.noteContentIv(hist.noteRevisionId), hist.content); + hist.content = dataEncryptionService.decryptString(dataKey, hist.content); } } function encryptNote(note) { const dataKey = getDataKey(); - note.title = dataEncryptionService.encrypt(dataKey, dataEncryptionService.noteTitleIv(note.noteId), note.title); - note.content = dataEncryptionService.encrypt(dataKey, dataEncryptionService.noteContentIv(note.noteId), note.content); + note.title = dataEncryptionService.encrypt(dataKey, note.title); + note.content = dataEncryptionService.encrypt(dataKey, note.content); } function encryptNoteRevision(revision) { const dataKey = getDataKey(); - revision.title = dataEncryptionService.encrypt(dataKey, dataEncryptionService.noteTitleIv(revision.noteRevisionId), revision.title); - revision.content = dataEncryptionService.encrypt(dataKey, dataEncryptionService.noteContentIv(revision.noteRevisionId), revision.content); + revision.title = dataEncryptionService.encrypt(dataKey, revision.title); + revision.content = dataEncryptionService.encrypt(dataKey, revision.content); } module.exports = {