diff --git a/www/common/make-backup.js b/www/common/make-backup.js index 382aee0b3..7db9b998f 100644 --- a/www/common/make-backup.js +++ b/www/common/make-backup.js @@ -52,7 +52,8 @@ define([ var cancel = function () { cancelled = true; }; - var parsed = Hash.parsePadUrl(fData.href || fData.roHref); + var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref; + var parsed = Hash.parsePadUrl(href); var hash = parsed.hash; var name = fData.filename || fData.title; var secret = Hash.getSecrets('file', hash, fData.password); @@ -88,7 +89,8 @@ define([ cancelled = true; }; - var parsed = Hash.parsePadUrl(pData.href || pData.roHref); + var href = (pData.href && pData.href.indexOf('#') !== -1) ? pData.href : pData.roHref; + var parsed = Hash.parsePadUrl(href); var name = pData.filename || pData.title; var opts = { password: pData.password @@ -137,7 +139,8 @@ define([ }); } - var parsed = Hash.parsePadUrl(fData.href || fData.roHref); + var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref; + var parsed = Hash.parsePadUrl(href); if (['pad', 'file'].indexOf(parsed.hashData.type) === -1) { return; } // waitFor is used to make sure all the pads and files are process before downloading the zip. diff --git a/www/common/mergeDrive.js b/www/common/mergeDrive.js index 4c63a7e0c..4aeb03dc5 100644 --- a/www/common/mergeDrive.js +++ b/www/common/mergeDrive.js @@ -38,8 +38,7 @@ define([ var data = oldFo.getFileData(id); var channel = data.channel; - // XXX encrypted href: we need to be able to change the value here - var datas = manager.findChannel(channel, true); + var datas = manager.findChannel(channel); // Do not migrate a pad if we already have it, it would create a duplicate // in the drive if (datas.length !== 0) { @@ -50,7 +49,9 @@ define([ // We want to merge an edit pad: check if we have the same channel // but read-only and upgrade it in that case datas.forEach(function (pad) { - if (pad.data && !pad.data.href) { pad.data.href = data.href; } + if (pad.data && !pad.data.href) { + pad.userObject.setHref(channel, null, data.href); + } }); return; } diff --git a/www/common/migrate-user-object.js b/www/common/migrate-user-object.js index 4684b200d..ff56065a8 100644 --- a/www/common/migrate-user-object.js +++ b/www/common/migrate-user-object.js @@ -145,10 +145,14 @@ define([ n = n.nThen(function (w) { setTimeout(w(function () { el = data[k]; - if (!el.href || (el.roHref && false)) { + if (!el.href) { // Already migrated return void progress(7, Math.round(100*i/padsLength)); } + if (el.href.indexOf('#') === -1) { + // Encrypted href: already migrated + return void progress(7, Math.round(100*i/padsLength)); + } parsed = Hash.parsePadUrl(el.href); if (parsed.hashData.type !== "pad") { // No read-only mode for files diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 9ff329335..2a19184c4 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1045,7 +1045,7 @@ define([ if (data.teamId && s.id !== data.teamId) { return; } if (storeLocally && s.id) { return; } - var res = s.manager.findChannel(channel); + var res = s.manager.findChannel(channel, true); if (res.length) { sendTo.push(s.id); } @@ -1081,7 +1081,7 @@ define([ // If all of the weaker ones were in the trash, add the stronger to ROOT obj.userObject.restoreHref(href); } - pad.href = href; + obj.userObject.setHref(channel, null, href); }); // Pads owned by us ("us" can be a user or a team) that are not in our "main" drive @@ -1474,7 +1474,7 @@ define([ onMetadataUpdate: function (metadata) { channel.data = metadata || {}; getAllStores().forEach(function (s) { - var allData = s.manager.findChannel(data.channel); + var allData = s.manager.findChannel(data.channel, true); allData.forEach(function (obj) { obj.data.owners = metadata.owners; obj.data.atime = +new Date(); @@ -1640,7 +1640,7 @@ define([ // Update owners and expire time in the drive getAllStores().forEach(function (s) { - var allData = s.manager.findChannel(data.channel); + var allData = s.manager.findChannel(data.channel, true); var changed = false; allData.forEach(function (obj) { if (Sortify(obj.data.owners) !== Sortify(metadata.owners)) { diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 945bcc284..87597f0b1 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -249,14 +249,13 @@ define([ if (msg.author !== content.user.curvePublic) { return void cb(true); } var channel = content.channel; - // XXX encrypted href - var res = ctx.store.manager.findChannel(channel); + var res = ctx.store.manager.findChannel(channel, true); var title; res.forEach(function (obj) { if (obj.data && !obj.data.href) { if (!title) { title = obj.data.filename || obj.data.title; } - obj.data.href = content.href; + obj.userObject.setHref(channel, null, content.href); } }); diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index f9963e82f..d9a7058a3 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -25,7 +25,9 @@ define([ var teamId = store.id || -1; var handler = store.handleSharedFolder; - var parsed = Hash.parsePadUrl(data.href); + var href = store.manager.user.userObject.getHref(data); + + var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets('drive', parsed.hash, data.password); var secondaryKey = secret.keys.secondaryKey; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index d28b9d84c..bf9c16fa2 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -31,13 +31,26 @@ define([ var debug = exp.debug; + exp.setHref = function (channel, id, href) { + if (!id && !channel) { return; } + var ids = id ? [id] : exp.findChannels([channel]); + ids.forEach(function (i) { + var data = exp.getFileData(i, true); + data.href = exp.cryptor.encrypt(href); + }); + }; + exp.setPadAttribute = function (href, attr, value, cb) { cb = cb || function () {}; var id = exp.getIdFromHref(href); if (!id) { return void cb("E_INVAL_HREF"); } if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } var data = exp.getFileData(id, true); - data[attr] = clone(value); + if (attr === "href") { + exp.setHref(null, id, value); + } else { + data[attr] = clone(value); + } cb(null); }; exp.getPadAttribute = function (href, attr, cb) { @@ -51,6 +64,8 @@ define([ exp.pushData = function (data, cb) { if (typeof cb !== "function") { cb = function () {}; } var id = Util.createRandomInteger(); + // If we were given an edit link, encrypt its value if needed + if (data.href) { data.href = exp.cryptor.encrypt(data.href); } files[FILES_DATA][id] = data; cb(null, id); }; @@ -70,6 +85,7 @@ define([ return void cb("EAUTH"); } var id = Util.createRandomInteger(); + if (data.href) { data.href = exp.cryptor.encrypt(data.href); } files[SHARED_FOLDERS][id] = data; cb(null, id); }; @@ -209,11 +225,15 @@ define([ id = Number(id); // Find and maybe update existing pads with the same channel id var d = data[id]; + // If we were given an edit link, encrypt its value if needed + if (d.href) { d.href = exp.cryptor.encrypt(d.href); } var found = false; for (var i in files[FILES_DATA]) { if (files[FILES_DATA][i].channel === d.channel) { // Update href? - if (!files[FILES_DATA][i].href) { files[FILES_DATA][i].href = d.href; } + if (!files[FILES_DATA][i].href) { + files[FILES_DATA][i].href = d.href; + } found = true; break; } @@ -222,7 +242,7 @@ define([ toRemove.push(id); return; } - files[FILES_DATA][id] = data[id]; + files[FILES_DATA][id] = d; }); // Remove existing pads from the "element" variable @@ -500,6 +520,7 @@ define([ if (silent) { debug = function () {}; } + var t0 = +new Date(); debug("Cleaning file system..."); var before = JSON.stringify(files); @@ -536,7 +557,10 @@ define([ // We have an old file (href) which is not in filesData: add it var id = Util.createRandomInteger(); var key = Hash.createChannelId(); - files[FILES_DATA][id] = {href: element[el], filename: el}; + files[FILES_DATA][id] = { + href: exp.cryptor.encrypt(element[el]), + filename: el + }; element[key] = id; delete element[el]; } @@ -562,7 +586,10 @@ define([ if (typeof obj.element === "string") { // We have an old file (href) which is not in filesData: add it var id = Util.createRandomInteger(); - files[FILES_DATA][id] = {href: obj.element, filename: el}; + files[FILES_DATA][id] = { + href: exp.cryptor.encrypt(obj.element), + filename: el + }; obj.element = id; } if (exp.isFolder(obj.element)) { fixRoot(obj.element); } @@ -607,7 +634,9 @@ define([ if (typeof el === "string") { // We have an old file (href) which is not in filesData: add it var id = Util.createRandomInteger(); - files[FILES_DATA][id] = {href: el}; + files[FILES_DATA][id] = { + href: exp.cryptor.encrypt(el) + }; us[idx] = id; } if (typeof el === "number") { @@ -653,7 +682,12 @@ define([ continue; } - var parsed = Hash.parsePadUrl(el.href || el.roHref); + var href; + try { + href = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href)); + } catch (e) {} + + var parsed = Hash.parsePadUrl(href || el.roHref); var secret; // Clean invalid hash @@ -670,9 +704,9 @@ define([ } // If we have an edit link, check the view link - if (el.href && parsed.hashData.type === "pad" && parsed.hashData.version) { + if (href && parsed.hashData.type === "pad" && parsed.hashData.version) { if (parsed.hashData.mode === "view") { - el.roHref = el.href; + el.roHref = href; delete el.href; } else if (!el.roHref) { secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); @@ -691,7 +725,7 @@ define([ } // Fix href - if (el.href && /^https*:\/\//.test(el.href)) { el.href = Hash.getRelativeHref(el.href); } + if (href && href.slice(0,1) !== '/') { el.href = exp.cryptor.encrypt(Hash.getRelativeHref(el.href)); } // Fix creation time if (!el.ctime) { el.ctime = el.atime; } // Fix title @@ -732,8 +766,13 @@ define([ el = sf[id]; id = Number(id); + var href; + try { + href = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href)); + } catch (e) {} + // Fix undefined hash - parsed = Hash.parsePadUrl(el.href || el.roHref); + parsed = Hash.parsePadUrl(href || el.roHref); secret = Hash.getSecrets('drive', parsed.hash, el.password); if (!secret.keys) { delete sf[id]; @@ -762,11 +801,12 @@ define([ fixDrive(); fixSharedFolders(); + var ms = (+new Date() - t0) + 'ms'; if (JSON.stringify(files) !== before) { - debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely"); + debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely.", ms); return; } - debug("File system was clean"); + debug("File system was clean.", ms); }; return exp; diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 2c0741d01..77c466db7 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -81,11 +81,13 @@ define([ // Return files data objects associated to a channel for setPadTitle // All occurences are returned, in drive or shared folders - var findChannel = function (Env, channel) { + // If "editable" is true, the data returned is a proxy, otherwise + // it's a cloned object (NOTE: href should never be edited directly) + var findChannel = function (Env, channel, editable) { var ret = []; Env.user.userObject.findChannels([channel]).forEach(function (id) { ret.push({ - data: Env.user.userObject.getFileData(id), + data: Env.user.userObject.getFileData(id, editable), userObject: Env.user.userObject }); }); @@ -93,7 +95,7 @@ define([ Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) { ret.push({ fId: fId, - data: Env.folders[fId].userObject.getFileData(id), + data: Env.folders[fId].userObject.getFileData(id, editable), userObject: Env.folders[fId].userObject }); }); @@ -101,6 +103,8 @@ define([ return ret; }; // Return files data objects associated to a given href for setPadAttribute... + // If "editable" is true, the data returned is a proxy, otherwise + // it's a cloned object (NOTE: href should never be edited directly) var findHref = function (Env, href) { var ret = []; var id = Env.user.userObject.getIdFromHref(href); @@ -155,11 +159,11 @@ define([ return ret; }; - var _getFileData = function (Env, id) { + var _getFileData = function (Env, id, editable) { var userObjects = _getUserObjects(Env); var data = {}; userObjects.some(function (uo) { - data = uo.getFileData(id); + data = uo.getFileData(id, editable); if (Object.keys(data).length) { return true; } }); return data; @@ -278,11 +282,6 @@ define([ filesData[f] = userObject.getFileData(f); }); - // TODO RO - // Encrypt or decrypt edit link here - // filesData.forEach(function (d) { d.href = encrypt(d.href); }); - - data.push({ el: el, data: filesData, @@ -435,6 +434,7 @@ define([ Env.pinPads([folderData.channel], waitFor()); }).nThen(function (waitFor) { // 1. add the shared folder to our list of shared folders + // NOTE: pushSharedFolder will encrypt the href directly in the object if needed Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) { if (err) { waitFor.abort(); @@ -1128,7 +1128,13 @@ define([ if (!Env.folders[id]) { return {}; } var obj = Env.folders[id].proxy.metadata || {}; for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) { - obj[k] = Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]; + var data = JSON.parse(JSON.stringify(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k])); + if (data.href && data.href.indexOf('#') === -1) { + try { + data.href = Env.user.userObject.cryptor.decrypt(data.href); + } catch (e) {} + } + obj[k] = data; } return obj; }; diff --git a/www/common/userObject.js b/www/common/userObject.js index b847ce50d..909e0e654 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -39,7 +39,13 @@ define([ }; if (config.editKey) { try { - exp.cryptor = Crypto.createEncryptor(config.editKey); + var c = Crypto.createEncryptor(config.editKey); + exp.cryptor.encrypt = function (href) { + // Never encrypt blob href, they are always read-only + if (href.slice(0,7) === '/file/#') { return href; } + return c.encrypt(href); + }; + exp.cryptor.decrypt = c.decrypt; } catch (e) { console.error(e); } @@ -106,6 +112,16 @@ define([ return a; }; + var getHref = exp.getHref = function (pad) { + if (pad.href && pad.href.indexOf('#') !== -1) { + return pad.href; + } + if (pad.href) { + return exp.cryptor.decrypt(pad.href); + } + return pad.roHref; + }; + var type = function (dat) { return dat === null? 'null': Array.isArray(dat)?'array': typeof(dat); }; @@ -219,12 +235,23 @@ define([ }; // Get data from AllFiles (Cryptpad_RECENTPADS) - var getFileData = exp.getFileData = function (file, noCopy) { + var getFileData = exp.getFileData = function (file, editable) { if (!file) { return; } var data = files[FILES_DATA][file] || {}; - if (!noCopy) { - // XXX encrypted href: decrypt or remove "href" + if (!editable) { data = JSON.parse(JSON.stringify(data)); + if (data.href && data.href.indexOf('#') === -1) { + // Encrypted href: decrypt it if we can, otherwise remove it + if (config.editKey) { + try { + data.href = exp.cryptor.decrypt(data.href); + } catch (e) { + delete data.href; + } + } else { + delete data.href; + } + } } return data; }; @@ -401,7 +428,7 @@ define([ var getIdFromHref = exp.getIdFromHref = function (href) { var result; getFiles([FILES_DATA]).some(function (id) { - if (files[FILES_DATA][id].href === href || + if (getHref(files[FILES_DATA][id]) === href || files[FILES_DATA][id].roHref === href) { result = id; return true; @@ -413,7 +440,7 @@ define([ exp.getSFIdFromHref = function (href) { var result; getFiles([SHARED_FOLDERS]).some(function (id) { - if (files[SHARED_FOLDERS][id].href === href || + if (getHref(files[SHARED_FOLDERS][id]) === href || files[SHARED_FOLDERS][id].roHref === href) { result = id; return true; diff --git a/www/debug/inner.js b/www/debug/inner.js index 1a5c9e023..3373b4803 100644 --- a/www/debug/inner.js +++ b/www/debug/inner.js @@ -97,7 +97,7 @@ define([ for (var i = 0; i