diff --git a/historyKeeper.js b/historyKeeper.js index 03c767506..342910ad5 100644 --- a/historyKeeper.js +++ b/historyKeeper.js @@ -396,32 +396,35 @@ module.exports.create = function (cfg) { // parsed[3] is the last known hash (optionnal) sendMsg(ctx, user, [seq, 'ACK']); channelName = parsed[1]; - var validateKey = parsed[2]; - var lastKnownHash = parsed[3]; - var owners; - var expire; - if (parsed[2] && typeof parsed[2] === "object") { - validateKey = parsed[2].validateKey; - lastKnownHash = parsed[2].lastKnownHash; - owners = parsed[2].owners; - if (parsed[2].expire) { - expire = +parsed[2].expire * 1000 + (+new Date()); + var config = parsed[2]; + var metadata = {}; + var lastKnownHash; + if (config && typeof config === "object") { + lastKnownHash = config.lastKnownHash; + metadata = config.metadata || {}; + if (metadata.expire) { + metadata.expire = +metadata.expire * 1000 + (+new Date()); } + } else if (config) { + // This is the old way: parsed[2] is the validateKey and parsed[3] is the last known hash + lastKnownHash = parsed[3]; + metadata.validateKey = parsed[2]; } + metadata.channel = channelName; nThen(function (waitFor) { if (!tasks) { return; } // tasks are not supported - if (typeof(expire) !== 'number' || !expire) { return; } + if (typeof(metadata.expire) !== 'number' || !metadata.expire) { return; } // the fun part... // the user has said they want this pad to expire at some point - tasks.write(expire, "EXPIRE", [ channelName ], waitFor(function (err) { + tasks.write(metadata.expire, "EXPIRE", [ channelName ], waitFor(function (err) { if (err) { // if there is an error, we don't want to crash the whole server... // just log it, and if there's a problem you'll be able to fix it // at a later date with the provided information Log.error('HK_CREATE_EXPIRE_TASK', err); - Log.info('HK_INVALID_EXPIRE_TASK', JSON.stringify([expire, 'EXPIRE', channelName])); + Log.info('HK_INVALID_EXPIRE_TASK', JSON.stringify([metadata.expire, 'EXPIRE', channelName])); } })); }).nThen(function (waitFor) { @@ -482,20 +485,9 @@ module.exports.create = function (cfg) { // the first message in the file const chan = ctx.channels[channelName]; if (msgCount === 0 && !historyKeeperKeys[channelName] && chan && chan.indexOf(user) > -1) { - var key = {}; - key.channel = channelName; - if (validateKey) { - key.validateKey = validateKey; - } - if (owners) { - key.owners = owners; - } - if (expire) { - key.expire = expire; - } - historyKeeperKeys[channelName] = key; - storeMessage(ctx, chan, JSON.stringify(key), false, undefined); - sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(key)]); + historyKeeperKeys[channelName] = metadata; + storeMessage(ctx, chan, JSON.stringify(metadata), false, undefined); + sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(metadata)]); } // End of history message: diff --git a/www/common/common-messenger.js b/www/common/common-messenger.js index 1102f94a2..365c8be40 100644 --- a/www/common/common-messenger.js +++ b/www/common/common-messenger.js @@ -422,8 +422,10 @@ define([ var friend = getFriendFromChannel(chan.id) || {}; var cfg = { - validateKey: keys ? keys.validateKey : undefined, - owners: [proxy.edPublic, friend.edPublic], + metadata: { + validateKey: keys ? keys.validateKey : undefined, + owners: [proxy.edPublic, friend.edPublic], + }, lastKnownHash: data.lastKnownHash }; var msg = ['GET_HISTORY', chan.id, cfg]; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 5ee07946b..3f0cf8af9 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -764,6 +764,10 @@ define([ postMessage("GIVE_PAD_ACCESS", data, cb); }; + common.getPadMetadata = function (data, cb) { + postMessage('GET_PAD_METADATA', data, cb); + }; + common.changePadPassword = function (Crypt, href, newPassword, edPublic, cb) { if (!href) { return void cb({ error: 'EINVAL_HREF' }); } var parsed = Hash.parsePadUrl(href); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 6b7942d13..b7ac2da26 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1209,10 +1209,7 @@ define([ }, noChainPad: true, channel: data.channel, - validateKey: data.validateKey, - owners: data.owners, - password: data.password, - expire: data.expire, + metadata: data.metadata, network: store.network, //readOnly: data.readOnly, onConnect: function (wc, sendMessage) { @@ -1262,11 +1259,14 @@ define([ channel.sendMessage(msg, clientId, cb); }; + // requestPadAccess is used to check if we have a way to contact the owner + // of the pad AND to send the request if we want + // data.send === false ==> check if we can contact them + // data.send === true ==> send the request Store.requestPadAccess = function (clientId, data, cb) { - // Get owners from pad metadata - // Try to find an owner in our friend list - // Mailbox... + var owner = data.owner; var channel = channels[data.channel]; + if (!channel) { return void cb({error: 'ENOTFOUND'}); } if (!data.send && channel && (!channel.data || !channel.data.channel)) { var i = 0; var it = setInterval(function () { @@ -1277,16 +1277,19 @@ define([ } if (i >= 300) { // One minute timeout clearInterval(it); + return void cb({error: 'ETIMEOUT'}); } i++; }, 200); return; } + + // If the owner was not is the pad metadata, check if it is a friend. + // We'll contact the first owner for whom we know the mailbox var fData = channel.data || {}; - if (fData.owners) { + if (!owner && fData.owners) { var friends = store.proxy.friends || {}; if (Object.keys(friends).length > 1) { - var owner; fData.owners.some(function (edPublic) { return Object.keys(friends).some(function (curve) { if (curve === "me") { return; } @@ -1297,26 +1300,28 @@ define([ } }); }); - if (owner) { - if (data.send) { - var myData = Messaging.createData(store.proxy); - delete myData.channel; - store.mailbox.sendTo('REQUEST_PAD_ACCESS', { - channel: data.channel, - user: myData - }, { - channel: owner.notifications, - curvePublic: owner.curvePublic - }, function () { - cb({state: true}); - }); - return; - } - return void cb({state: true}); - } } } - cb({sent: false}); + + // If send is true, send the request to the owner. + if (owner) { + if (data.send) { + var myData = Messaging.createData(store.proxy); + delete myData.channel; + store.mailbox.sendTo('REQUEST_PAD_ACCESS', { + channel: data.channel, + user: myData + }, { + channel: owner.notifications, + curvePublic: owner.curvePublic + }, function () { + cb({state: true}); + }); + return; + } + return void cb({state: true}); + } + cb({state: false}); }; Store.givePadAccess = function (clientId, data, cb) { var edPublic = store.proxy.edPublic; @@ -1353,6 +1358,29 @@ define([ cb(); }; + Store.getPadMetadata = function (clientId, data, cb) { + if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); } + var channel = channels[data.channel]; + if (!channel) { return void cb({ error: 'ENOTFOUND' }); } + if (!channel.data || !channel.data.channel) { + var i = 0; + var it = setInterval(function () { + if (channel.data && channel.data.channel) { + clearInterval(it); + Store.getPadMetadata(clientId, data, cb); + return; + } + if (i >= 300) { // One minute timeout + clearInterval(it); + return void cb({error: 'ETIMEOUT'}); + } + i++; + }, 200); + return; + } + cb(channel.data || {}); + }; + // GET_FULL_HISTORY from sframe-common-outer Store.getFullHistory = function (clientId, data, cb) { var network = store.network; @@ -1459,14 +1487,16 @@ define([ websocketURL: NetConfig.getWebsocketURL(), channel: secret.channel, readOnly: false, - validateKey: secret.keys.validateKey || undefined, crypto: Crypto.createEncryptor(secret.keys), userName: 'sharedFolder', logLevel: 1, ChainPad: ChainPad, classic: true, network: store.network, - owners: owners + metadata: { + validateKey: secret.keys.validateKey || undefined, + owners: owners + } }; var rt = Listmap.create(listmapConfig); store.sharedFolders[id] = rt; diff --git a/www/common/outer/onlyoffice.js b/www/common/outer/onlyoffice.js index 70bc413ec..37359d757 100644 --- a/www/common/outer/onlyoffice.js +++ b/www/common/outer/onlyoffice.js @@ -92,9 +92,11 @@ define([ var hk = network.historyKeeper; var cfg = { validateKey: obj.validateKey, - lastKnownHash: chan.lastKnownHash || chan.lastCpHash, - owners: obj.owners, - expire: obj.expire + metadata: { + lastKnownHash: chan.lastKnownHash || chan.lastCpHash, + owners: obj.owners, + expire: obj.expire + } }; var msg = ['GET_HISTORY', wc.id, cfg]; // Add the validateKey if we are the channel creator and we have a validateKey diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index bb8e0a674..18096d96d 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -80,6 +80,7 @@ define([ IS_NEW_CHANNEL: Store.isNewChannel, REQUEST_PAD_ACCESS: Store.requestPadAccess, GIVE_PAD_ACCESS: Store.givePadAccess, + GET_PAD_METADATA: Store.getPadMetadata, // Drive DRIVE_USEROBJECT: Store.userObjectCommand, // Settings, diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index a91f9f6d1..9dbfb1816 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -23,14 +23,12 @@ define([], function () { var start = function (conf) { var channel = conf.channel; var Crypto = conf.crypto; - var validateKey = conf.validateKey; var isNewHash = conf.isNewHash; var readOnly = conf.readOnly || false; var padRpc = conf.padRpc; var sframeChan = conf.sframeChan; - var password = conf.password; - var owners = conf.owners; - var expire = conf.expire; + var metadata= conf.metadata || {}; + var validateKey = metadata.validateKey; var onConnect = conf.onConnect || function () { }; conf = undefined; @@ -127,11 +125,8 @@ define([], function () { // join the netflux network, promise to handle opening of the channel padRpc.joinPad({ channel: channel || null, - validateKey: validateKey, readOnly: readOnly, - owners: owners, - password: password, - expire: expire + metadata: metadata }); }; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index ba778a331..5f82d2fe5 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -272,7 +272,7 @@ define([ var parsed = Utils.Hash.parsePadUrl(window.location.href); if (!parsed.type) { throw new Error(); } var defaultTitle = Utils.Hash.getDefaultName(parsed); - var edPublic, isTemplate; + var edPublic, curvePublic, notifications, isTemplate; var forceCreationScreen = cfg.useCreationScreen && sessionStorage[Utils.Constants.displayPadCreationScreen]; delete sessionStorage[Utils.Constants.displayPadCreationScreen]; @@ -284,6 +284,8 @@ define([ if (err) { console.log(err); } metaObj = m; edPublic = metaObj.priv.edPublic; // needed to create an owned pad + curvePublic = metaObj.user.curvePublic; + notifications = metaObj.user.notifications; })); if (typeof(isTemplate) === "undefined") { Cryptpad.isTemplate(window.location.href, waitFor(function (err, t) { @@ -973,10 +975,33 @@ define([ if (readOnly && hashes.editHash) { return void cb({error: 'ALREADYKNOWN'}); } - Cryptpad.padRpc.requestAccess({ - send: data, - channel: secret.channel - }, cb); + var owner; + var crypto = Crypto.createEncryptor(secret.keys); + nThen(function (waitFor) { + // Try to get the owner's mailbox from the pad metadata first. + // If it's is an older owned pad, check if the owner is a friend + // or an acquaintance (from async-store directly in requestAccess) + Cryptpad.getPadMetadata({ + channel: secret.channel + }, waitFor(function (obj) { + obj = obj || {}; + if (obj.error) { return; } + if (obj.mailbox) { + try { + var dataStr = crypto.decrypt(obj.mailbox, true, true); + var data = JSON.parse(dataStr); + if (!data.notifications || !data.curvePublic) { return; } + owner = data; + } catch (e) { console.error(e); } + } + })); + }).nThen(function () { + Cryptpad.padRpc.requestAccess({ + send: data, + channel: secret.channel, + owner: owner + }, cb); + }); }); if (cfg.messaging) { @@ -1099,13 +1124,21 @@ define([ readOnly = false; updateMeta(); - var rtConfig = {}; + var rtConfig = { + metadata: {} + }; if (data.owned) { - rtConfig.owners = [edPublic]; + rtConfig.metadata.owners = [edPublic]; + rtConfig.metadata.mailbox = Utils.crypto.encrypt(JSON.stringify({ + notifications: notifications, + curvePublic: curvePublic + })); } if (data.expire) { - rtConfig.expire = data.expire; + rtConfig.metadata.expire = data.expire; } + rtConfig.metadata.validateKey = (secret.keys && secret.keys.validateKey) || undefined; + Utils.rtConfig = rtConfig; nThen(function(waitFor) { if (data.templateId) {