diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index df8836788..58cadb920 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -551,9 +551,10 @@ define([ $d.append(password); } - if (!data.noEditPassword && owned && parsed.type !== "sheet") { // FIXME SHEET fix password change for sheets + if (!data.noEditPassword && owned) { // FIXME SHEET fix password change for sheets var sframeChan = common.getSframeChannel(); + var isOO = parsed.type === 'sheet'; var isFile = parsed.hashData.type === 'file'; var isSharedFolder = parsed.type === 'drive'; @@ -586,7 +587,8 @@ define([ UI.confirm(changePwConfirm, function (yes) { if (!yes) { pLocked = false; return; } $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'})); - var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE'; + var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' : + (isOO ? 'Q_OO_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE'); // If this is a file password change, register to the upload events: // * if there is a pending upload, ask if we shoudl interrupt diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 25a64404d..c960967fa 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1155,6 +1155,233 @@ define([ }); }; + common.changeOOPassword = function (data, cb) { + var href = data.href; + var newPassword = data.password; + var teamId = data.teamId; + if (!href) { return void cb({ error: 'EINVAL_HREF' }); } + var parsed = Hash.parsePadUrl(href); + if (!parsed.hash) { return void cb({ error: 'EINVAL_HREF' }); } + if (parsed.type !== 'sheet') { return void cb({ error: 'EINVAL_TYPE' }); } + + var warning = false; + var newHash, newRoHref; + var oldChannel; + var oldSecret; + var oldMetadata; + var oldRtChannel; + var privateData; + var padData; + + var newSecret; + if (parsed.hashData.version >= 2) { + newSecret = Hash.getSecrets(parsed.type, parsed.hash, newPassword); + if (!(newSecret.keys && newSecret.keys.editKeyStr)) { + return void cb({error: 'EAUTH'}); + } + newHash = Hash.getEditHashFromKeys(newSecret); + } + var newHref = '/' + parsed.type + '/#' + newHash; + var newRtChannel = Hash.createChannelId(); + + var Crypt, Crypto; + var cryptgetVal; + var lastCp; + var optsPut = { + password: newPassword, + metadata: { + validateKey: newSecret.keys.validateKey + }, + }; + + Nthen(function (waitFor) { + common.getPadAttribute('', waitFor(function (err, _data) { + padData = _data; + }), href); + }).nThen(function (waitFor) { + oldSecret = Hash.getSecrets(parsed.type, parsed.hash, padData.password); + + require([ + '/common/cryptget.js', + '/bower_components/chainpad-crypto/crypto.js', + ], waitFor(function (_Crypt, _Crypto) { + Crypt = _Crypt; + Crypto = _Crypto; + })); + + common.getPadMetadata({channel: oldSecret.channel}, waitFor(function (metadata) { + oldMetadata = metadata; + })); + common.getMetadata(waitFor(function (err, data) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + privateData = data.priv; + })); + }).nThen(function (waitFor) { + // Check if we're allowed to change the password + var owners = oldMetadata.owners; + optsPut.metadata.owners = owners; + var edPublic = teamId ? (privateData.teams[teamId] || {}).edPublic : privateData.edPublic; + var isOwner = Array.isArray(owners) && edPublic && owners.indexOf(edPublic) !== -1; + if (!isOwner) { + // We're not an owner, we shouldn't be able to change the password! + waitFor.abort(); + return void cb({ error: 'EPERM' }); + } + + var mailbox = oldMetadata.mailbox; + if (mailbox) { + // Create the encryptors to be able to decrypt and re-encrypt the mailboxes + var oldCrypto = Crypto.createEncryptor(oldSecret.keys); + var newCrypto = Crypto.createEncryptor(newSecret.keys); + + var m; + if (typeof(mailbox) === "string") { + try { + m = newCrypto.encrypt(oldCrypto.decrypt(mailbox, true, true)); + } catch (e) {} + } else if (mailbox && typeof(mailbox) === "object") { + m = {}; + Object.keys(mailbox).forEach(function (ed) { + try { + m[ed] = newCrypto.encrypt(oldCrypto.decrypt(mailbox[ed], true, true)); + } catch (e) { + console.error(e); + } + }); + } + optsPut.metadata.mailbox = m; + } + + var expire = oldMetadata.expire; + if (expire) { + optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds + } + + // Get last cp (cryptget) + Crypt.get(parsed.hash, waitFor(function (err, val) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + try { + cryptgetVal = JSON.parse(val); + if (!cryptgetVal.content) { + waitFor.abort(); + return void cb({ error: 'INVALID_CONTENT' }); + } + } catch (e) { + waitFor.abort(); + return void cb({ error: 'CANT_PARSE' }); + } + }), { + password: padData.password + }); + }).nThen(function (waitFor) { + // Re-encrypt rtchannel + oldRtChannel = Util.find(cryptgetVal, ['content', 'channel']); + var newCrypto = Crypto.createEncryptor(newSecret.keys); + var oldCrypto = Crypto.createEncryptor(oldSecret.keys); + var cps = Util.find(cryptgetVal, ['content', 'hashes']); + var lastCp = cps.length ? cps[cps.length - 1] : {}; + common.getHistory({ + channel: oldRtChannel, + lastKnownHash: lastCp.hash + }, waitFor(function (obj) { + if (obj && obj.error) { + waitFor.abort(); + return void cb(obj); + } + var msgs = obj; + newHistory = msgs.map(function (str) { + try { + var d = oldCrypto.decrypt(msg, true, true); + return newCrypto.encrypt(d); + } catch (e) { + waitFor.abort(); + return void cb({error: e}); + } + }); + // Update last knwon hash in cryptgetVal + if (lastCp) { lastCp.hash = msgs[0].slice(0, 64); } + common.onlyoffice.execCommand({ + cmd: 'REENCRYPT', + data: { + channel: newRtChannel, + msgs: newHistory, + metadata: optsPut.metadata + } + }, waitFor(function (obj) { + if (obj && obj.error) { + waitFor.abort(); + return void cb(obj); + } + })); + })); + }).nThen(function (waitFor) { + // The new rt channel is ready + // The blob uses its own encryption and doesn't need to be reencrypted + cryptgetVal.content.channel = newRtChannel; + Crypt.put(newHash, cryptgetVal, waitFor(function (err) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + }), optsPut); + }).nThen(function (waitFor) { + pad.leavePad({ + channel: oldSecret.channel + }, waitFor()); + pad.onDisconnectEvent.fire(true); + }).nThen(function (waitFor) { + // Set the new password to our pad data + common.setPadAttribute('password', newPassword, waitFor(function (err) { + if (err) { warning = true; } + }), href); + common.setPadAttribute('channel', newSecret.channel, waitFor(function (err) { + if (err) { warning = true; } + }), href); + common.setPadAttribute('rtChannel', newRtChannel, waitFor(function (err) { + if (err) { warning = true; } + }), href); + var viewHash = Hash.getViewHashFromKeys(newSecret); + newRoHref = '/' + parsed.type + '/#' + viewHash; + common.setPadAttribute('roHref', newRoHref, waitFor(function (err) { + if (err) { warning = true; } + }), href); + + if (parsed.hashData.password && newPassword) { return; } // same hash + common.setPadAttribute('href', newHref, waitFor(function (err) { + if (err) { warning = true; } + }), href); + }).nThen(function (waitFor) { + // delete the old pad + common.removeOwnedChannel({ + channel: oldSecret.channel, + teamId: teamId + }, waitFor(function (obj) { + if (obj && obj.error) { + waitFor.abort(); + return void cb(obj); + } + common.removeOwnedChannel({ + channel: oldRtChannel, + teamId: teamId + }, waitFor()); + })); + }).nThen(function () { + cb({ + warning: warning, + hash: newHash, + href: newHref, + roHref: newRoHref + }); + }); + }; + + common.changeUserPassword = function (Crypt, edPublic, data, cb) { if (!edPublic) { return void cb({ @@ -1350,6 +1577,9 @@ define([ common.getFullHistory = function (data, cb) { postMessage("GET_FULL_HISTORY", data, cb, {timeout: 180000}); }; + common.getHistory = function (data, cb) { + postMessage("GET_HISTORY", data, cb, {timeout: 180000}); + }; common.getHistoryRange = function (data, cb) { postMessage("GET_HISTORY_RANGE", data, cb); }; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 5900601e8..06d5b7d3a 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1692,7 +1692,7 @@ define([ // GET_FULL_HISTORY from sframe-common-outer Store.getFullHistory = function (clientId, data, cb) { var network = store.network; - var hkn = network.historyKeeper; + var hk = network.historyKeeper; //var crypto = Crypto.createEncryptor(data.keys); // Get the history messages and send them to the iframe var parse = function (msg) { @@ -1709,6 +1709,7 @@ define([ var parsed = parse(msg); if (parsed[0] === 'FULL_HISTORY_END') { cb(msgs); + network.off('message', onMsg); completed = true; return; } @@ -1725,12 +1726,68 @@ define([ } }; network.on('message', onMsg); - network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey])); + network.sendto(hk, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey])); + }; + + Store.getHistory = function (clientId, data, cb) { + var network = store.network; + var hk = network.historyKeeper; + + var parse = function (msg) { + try { + return JSON.parse(msg); + } catch (e) { + return null; + } + }; + + var msgs = []; + var completed = false; + var onMsg = function (msg, sender) { + if (completed) { return; } + if (sender !== hk) { return; } + var parsed = parse(msg); + + // Ignore the metadata message + if (parsed.validateKey && parsed.channel) { return; } + if (parsed.error && parsed.channel) { + if (parsed.channel === data.channel) { + network.off('message', onMsg); + completed = true; + cb({error: parsed.error}); + } + return; + } + + // End of history: cb + if (parsed.state === 1 && parsed.channel) { + if (parsed.channel !== data.channel) { return; } + cb(msgs); + network.off('message', onMsg); + completed = true; + return; + } + + msg = parsed[4]; + // Keep only the history for our channel + if (parsed[3] !== data.channel) { return; } + if (msg) { + msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, ''); + msgs.push(msg); + } + }; + network.on('message', onMsg); + + var cfg = { + lastKnownHash: data.lastKnownHash + }; + var msg = ['GET_HISTORY', data.channel, cfg]; + network.sendto(hk, JSON.stringify(msg)); }; Store.getHistoryRange = function (clientId, data, cb) { var network = store.network; - var hkn = network.historyKeeper; + var hk = network.historyKeeper; var parse = function (msg) { try { return JSON.parse(msg); @@ -1778,7 +1835,7 @@ define([ }; network.on('message', onMsg); - network.sendto(hkn, JSON.stringify(['GET_HISTORY_RANGE', data.channel, { + network.sendto(hk, JSON.stringify(['GET_HISTORY_RANGE', data.channel, { from: data.lastKnownHash, cpCount: 2, txid: txid diff --git a/www/common/outer/onlyoffice.js b/www/common/outer/onlyoffice.js index 1dc010ba4..cb88596aa 100644 --- a/www/common/outer/onlyoffice.js +++ b/www/common/outer/onlyoffice.js @@ -200,6 +200,40 @@ define([ })); }; + var reencrypt = function (ctx, data, cId, cb) { + var channel = data.channel; + var network = ctx.store.network; + + var onOpen = function (wc) { + var hk = network.historyKeeper; + var cfg = { + metadata: data.metadata + }; + var msg = ['GET_HISTORY', wc.id, cfg]; + network.sendto(hk, JSON.stringify(msg)); + data.msgs.forEach(function (msg) { + wc.bcast(msg); + }); + wc.leave(); + cb(); + }; + + ctx.store.anon_rpc.send("IS_NEW_CHANNEL", channel, function (e, response) { + if (e) { return void cb({error: e}); } + if (response && response.length && typeof(response[0]) === 'boolean') { + var isNew = response[0]; + } else { + cb({error: 'INVALID_RESPONSE'}); + } + if (!isNew) { return void cb({error: 'EEXISTS'}); } + + // Channel is new: we can push our reencrypted history + network.join(channel).then(onOpen, function (err) { + return void cb({error: err}); + }); + }); + }; + var leaveChannel = function (ctx, padChan) { // Leave channel and prevent reconnect when we leave a pad Object.keys(ctx.channels).some(function (ooChan) { @@ -267,6 +301,9 @@ define([ if (cmd === 'OPEN_CHANNEL') { return void openChannel(ctx, data, clientId, cb); } + if (cmd === 'REENCRYPT') { + return void reencrypt(ctx, data, clientId, cb); + } }; return oo; diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 2ef490876..12b5ff6e5 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -73,6 +73,7 @@ define([ JOIN_PAD: Store.joinPad, LEAVE_PAD: Store.leavePad, GET_FULL_HISTORY: Store.getFullHistory, + GET_HISTORY: Store.getHistory, GET_HISTORY_RANGE: Store.getHistoryRange, IS_NEW_CHANNEL: Store.isNewChannel, REQUEST_PAD_ACCESS: Store.requestPadAccess, diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 224a9f73c..604aaa4e7 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1007,6 +1007,11 @@ define([ }, cb); }); + sframeChan.on('Q_OO_PASSWORD_CHANGE', function (data, cb) { + data.href = data.href || window.location.href; + Cryptpad.changeOOPassword(data, cb); + }); + sframeChan.on('Q_PAD_PASSWORD_CHANGE', function (data, cb) { data.href = data.href || window.location.href; Cryptpad.changePadPassword(Cryptget, Crypto, data, cb);