diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index edb5153af..8be15dc75 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -763,7 +763,7 @@ define([ }; common.getPadAttribute = function (attr, cb, href, channel) { href = Hash.getRelativeHref(href || currentPad.href); - if (!href) { + if (!href && !channel) { return void cb('E404'); } postMessage("GET_PAD_ATTRIBUTE", { diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 3c882d11c..9c38e4c2f 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -1188,10 +1188,7 @@ console.error(el, data, app, isRo); var obj = { t: APP.team }; - if (!data.roHref && parsed.revocable) { // Revocable - if (isRo) { obj.mode = 'view'; } // force view VS use best rights - return void common.openURL(Hash.getNewPadURL(href, obj)); - } + if (isRo && parsed.revocable) { obj.mode = 'view'; } var priv = metadataMgr.getPrivateData(); @@ -1201,7 +1198,8 @@ console.error(el, data, app, isRo); } // Get hidden hash - var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password); + var secret = parsed.revocable ? { key: true, channel: data.channel } + : Hash.getSecrets(parsed.type, parsed.hash, data.password); var opts = {}; if (isRo) { opts.view = true; } var hash = Hash.getHiddenHashFromKeys(parsed.type, secret, opts); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 4ac0773ab..f381ac1f2 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1244,7 +1244,7 @@ define([ } } - if (h.mode === 'view') { return; } + if (h.mode === 'view' || accessType) { return; } // If we only have rohref, it means we have a stronger href if (!pad.href) { @@ -1388,6 +1388,13 @@ define([ return chans.some(function (pad) { if (!pad || !pad.data) { return; } var data = pad.data; + + // Revocable pads: the revocation module will try to get the best access later + if (data.r) { + res = data; + return true; + } + // We've found a match: return the value and stop the loops if ((edit && data.href) || (!edit && data.roHref) || isFile) { res = data; diff --git a/www/common/outer/revocation.js b/www/common/outer/revocation.js index 4575718c2..b6c08f9e9 100644 --- a/www/common/outer/revocation.js +++ b/www/common/outer/revocation.js @@ -85,7 +85,21 @@ id: { }); }; - var isValidRotateMessage = function (ctx, log, newSeeds, channel, _cb) { + + var askPassword = function (ctx, clients, cb) { + var uid = Util.uid(); + ctx.passwords.expect(uid, cb) + ctx.emit('ASK_PASSWORD', { + uid: uid + }, clients); + }; + var onPassword = function (ctx, data) { + var uid = data.uid; + var pw = data.pw; + ctx.passwords.handle(uid, pw); + }; + + var isValidRotateMessage = function (ctx, clients, log, newSeeds, channel, _cb) { var cb = Util.once(Util.mkAsync(_cb)); var rotateUid = newSeeds.uid; @@ -132,8 +146,20 @@ id: { } else { valid = isValidPw(pw); } - if (valid) { return void cb(true, pw); } - return void cb(false); + if (valid) { + if (!knownPasswords.includes(pw)) { + return ctx.Store.setPadAttribute(null, { + channel: channel, + attr: 'password', + value: pw + }, function () { + console.error('stored'); + cb(true, pw); + }); + } + return void cb(true, pw); + } + //return void cb(false); /* // XXX check last password from drive // XXX then start asking passwords to the user @@ -143,23 +169,24 @@ id: { // TODO since we need to support old pads, we may have to move all the password logic to the worker, even for old pads, to avoid duplicate code - askPassword(ctx, function (pw) { + */ + askPassword(ctx, clients, function (pw) { + console.error(pw); check(pw); }); - */ }; check(false); }; var getChannelFromBox = function (messages) { - if (!Array.isArray(messages) || !messages.length) { return void cb('EEMPTY'); } + if (!Array.isArray(messages) || !messages.length) { return; } var init = messages[0]; if (init.type !== 'INIT') { return; } var content = init.content; return content && content.doc && content.doc.channel; }; - var getMailboxParser = function (ctx, onNewKeys) { + var getMailboxParser = function (ctx, clients, onNewKeys) { var data = { doc: {} }; @@ -169,7 +196,7 @@ id: { // Only the INIT message can add a private key AND must be line 0 INIT: function (msg, log, channel, i, waitFor) { if (i || !msg.content || !msg.content.doc || !msg.content.edPrivate) { return; } - isValidRotateMessage(ctx, log, msg.content.doc, channel, + isValidRotateMessage(ctx, clients, log, msg.content.doc, channel, waitFor(function (valid, password) { if (!valid) { return; } data = JSON.parse(JSON.stringify(msg.content)); @@ -180,7 +207,7 @@ id: { if (!i || !data.doc) { return; } // Trust that all the keys you need are there // Make sure it was sent by the same moderator than in the moderators log - isValidRotateMessage(ctx, log, msg.content, channel, waitFor(function (valid, password) { + isValidRotateMessage(ctx, clients, log, msg.content, channel, waitFor(function (valid, password) { if (!valid) { return; } data.doc = msg.content data.password = password; @@ -264,7 +291,7 @@ id: { var cb = Util.once(Util.mkAsync(_cb)); var seed = boxData.seed; - +console.error(seed); var secret = Hash.getRevocable('pad', seed); @@ -308,15 +335,18 @@ id: { cb({ error: info.type }); }; - box.parse = getMailboxParser(ctx, function (keys) { + box.parse = getMailboxParser(ctx, box.clients, function (keys) { if (keys === false) { // XXX revoked // XXX close mailbox, etc. box.revoked = true; cb({error: 'EFORBIDDEN'}); } - if (first) { cb(keys); } - first = false; + if (first) { + cb(keys); + first = false; + return; + } onNewKeys(keys); }); @@ -329,6 +359,7 @@ id: { if (box.ready) { return; } box.ready = true; + console.error(box.messages); var padChan = getChannelFromBox(box.messages); if (!padChan) { return void cb({error: 'ENOCHAN'}); } @@ -346,7 +377,11 @@ id: { box.messages.push(parsed); if (box.ready) { box.parse.addMessages([parsed]); } }; - CPNetflux.start(config); + config.onConnectionChange = function (info) { + console.warn(secret.channel, info.state, info.myID, ctx.store.network.myID); + }; + + box.cpNf = CPNetflux.start(config); }; @@ -427,8 +462,10 @@ id: { accesses.map(function (obj) { n = n(function (waitFor) { loadMailbox(ctx, clientId, obj, function (newKeys) { - // XXX called when we received new keys in real time - }, waitFor()); + data.onNewKeysEvt.fire(newKeys); + }, waitFor(function () { + + })); }).nThen; }); @@ -454,18 +491,50 @@ id: { loadMailbox(ctx, clientId, { seed: seed }, function (newKeys) { - // XXX called when we received new keys in real time + data.onNewKeysEvt.fire(newKeys); }, function (obj) { if (obj && obj.error) { return void cb(obj); } - // XXX we now know the channel ID and we can load the other mailboxes on this channel console.error(obj); var chan = obj.channel; - loadMailboxesFromChannel(ctx, {channel: chan}, clientId, function (bestKeys) { + loadMailboxesFromChannel(ctx, { + channel: chan, + onNewKeysEvt: data.onNewKeysEvt + }, clientId, function (bestKeys) { cb(bestKeys); }); }); }; + var loadPad = function (ctx, data, clientId, cb) { + var seed = data.seed; + var chan = data.chan; + var onNewKeysEvt = Util.mkEvent(); + onNewKeysEvt.reg(function (newKeys) { + // XXX REVOCATION all mailboxes can fire at once + console.error('NEW KEYS', newKeys); + }); + + + if (chan) { + loadMailboxesFromChannel(ctx, { + channel: chan, + onNewKeysEvt: onNewKeysEvt + }, clientId, function (bestKeys) { + cb(bestKeys); + }); + return; + } + + loadPadFromBox(ctx, { + seed: seed, + onNewKeysEvt: onNewKeysEvt + }, clientId, function (bestKeys) { + cb(bestKeys); + }); + + + }; + var listPadAccess = function (ctx, data, clientId, cb) { @@ -770,17 +839,17 @@ console.error(keyHashStr); }); }; + + var leaveBox = function (ctx, box, boxChannel) { + if (box.cpNf) { box.cpNf.stop(); } + delete ctx.mailboxes[boxChannel]; + }; var leaveChannel = function (ctx, padChan) { // Leave channel and prevent reconnect when we leave a pad Object.keys(ctx.mailboxes).some(function (boxChannel) { var box = ctx.mailboxes[boxChannel]; if (box.padChan !== padChan) { return; } - if (box.wc) { box.wc.leave(); } - if (box.onReconnect) { - var network = ctx.store.network; - network.off('reconnect', box.onReconnect); - } - delete ctx.mailboxes[boxChannel]; + leaveBox(ctx, box, boxChannel); return true; }); }; @@ -796,14 +865,7 @@ console.error(keyHashStr); for (var k in ctx.mailboxes) { box = ctx.mailboxes[k]; box.clients = box.clients.filter(filter); - if (box.clients.length === 0) { - if (box.wc) { box.wc.leave(); } - if (box.onReconnect) { - var network = ctx.store.network; - network.off('reconnect', box.onReconnect); - } - delete ctx.mailboxes[k]; - } + if (box.clients.length === 0) { leaveBox(ctx, box, k); } } }; @@ -819,6 +881,7 @@ console.error(keyHashStr); store: cfg.store, Store: cfg.Store, emit: emit, + passwords: Util.response(function (l, i) { console.error('REV_PW_' + l, i); }), mailboxes: {}, }; @@ -841,11 +904,14 @@ console.error(keyHashStr); return void addAccess(ctx, data, clientId, cb); } if (cmd === 'LOAD_PAD') { - return void loadPadFromBox(ctx, data, clientId, cb); + return void loadPad(ctx, data, clientId, cb); } if (cmd === 'CREATE_PAD') { return void createPad(ctx, data, clientId, cb); } + if (cmd === 'PASSWORD') { + return void onPassword(ctx, data, clientId, cb); + } }; return revocation; diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 6f3c84514..cc4023361 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -1089,7 +1089,7 @@ define([ } Env.user.proxy[UserObject.SHARED_FOLDERS][sfId][data.attr] = data.value; } - var datas = findHref(Env, data.href); + var datas = data.channel ? findChannel(Env, data.channel) : findHref(Env, data.href); var nt = nThen; datas.forEach(function (d) { nt = nt(function (waitFor) { diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 15773eb0b..0d6a48b43 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -479,28 +479,46 @@ define([ value: '' }; - // Hidden hash: can't find the channel in our drives: abort - var noPadData = function (err) { - sframeChan.event("EV_PAD_NODATA", err); - }; + var getRevocable = function (w) { + var correctPassword = waitFor(); - var newHref; - var expire; - nThen(function (w) { - // XXX REVOCATION - if (parsed.hashData.version !== 5) { return; } - var seed = parsed.hashData.key; - Cryptpad.universal.execCommand({ - type: 'revocation', - data: { - cmd: 'LOAD_PAD', + var revCommand = function (cmd, data, cb) { + Cryptpad.universal.execCommand({ + type: 'revocation', data: { - seed: seed + cmd: cmd, + data: data } - } + }, cb) + }; + + var lastUid; + sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) { + password = data; + revCommand('PASSWORD', { + uid: lastUid, + pw: password + }); + }); + var onEvent = function (obj) { + if (obj.type !== 'revocation') { return; } + var q = obj.data; + if (q.ev !== 'ASK_PASSWORD') { return; } + lastUid = q.data && q.data.uid; + var oldPw = q.data && q.data.pw; + if (oldPw) { passwordCfg.value = oldPw; } + + sframeChan.event("EV_PAD_PASSWORD", passwordCfg); + }; + Cryptpad.universal.onEvent.reg(onEvent); + + var seed = parsed.revocable && parsed.hashData.key; + var chan = parsed.hashData && parsed.hashData.channel; + revCommand('LOAD_PAD', { + seed: seed, + chan: chan }, w(function (obj) { // XXX fix with password workflow - w.abort(); if (obj && obj.error) { if (obj.error === 'EFORBIDDEN') { sframeChan.event("EV_RESTRICTED_ERROR"); @@ -509,6 +527,9 @@ define([ return; } + Cryptpad.universal.onEvent.unreg(onEvent); + correctPassword(); + // XXX get access type (sf/team/user/link) currentPad.type = 'link'; @@ -530,8 +551,22 @@ define([ keys: secret.revocation } }; - done(); })); + + }; + + // Hidden hash: can't find the channel in our drives: abort + var noPadData = function (err) { + sframeChan.event("EV_PAD_NODATA", err); + }; + + var newHref; + var expire; + var revocable; + nThen(function (w) { + return; + // XXX REVOCATION + if (parsed.hashData.version !== 5) { return; } }).nThen(function (w) { // If we're using an unsafe link, get pad attribute if (parsed.hashData.key || !parsed.hashData.channel) { @@ -548,6 +583,7 @@ define([ edit: edit, file: parsed.hashData.type === 'file' }, w(function (err, res) { + console.error(res); // Error while getting data? abort if (err || !res || res.error) { w.abort(); @@ -560,6 +596,9 @@ define([ } // Data found but weaker? warn expire = res.expire; + + if (res.r) { revocable = true; return; } // Revocable + if (edit && !res.href) { newHref = res.roHref; return; @@ -576,13 +615,25 @@ define([ currentPad.href = parsed.getUrl(opts); currentPad.hash = parsed.hashData && parsed.hashData.getHash(opts); } + var chan = parsed.hashData.version === 3 && parsed.hashData.channel; + var url = revocable ? null : parsed.getUrl(); Cryptpad.getPadAttribute('channel', w(function (err, data) { + console.error(err, data); stored = (!err && typeof (data) === "string"); - })); + }), null, chan); Cryptpad.getPadAttribute('password', w(function (err, val) { + console.error(err, val); password = val; - }), parsed.getUrl()); + }), url, chan); }).nThen(function (w) { + console.error(expire, password); + // Revocable pad, safe (revocable) or unsafe (parsed.revocable) link + if (parsed.revocable || revocable) { + console.error(parsed.revocable, revocable); + return void getRevocable(w); + } + + // If we've already tested this password and this is a redirect, force if (typeof(newPadPassword) !== "undefined" && newPadPasswordForce) { password = newPadPassword; @@ -1244,7 +1295,8 @@ define([ var useUnsafe = Utils.Util.find(settings, ['security', 'unsafeLinks']); if (useUnsafe !== true && window.history && window.history.replaceState) { if (!/^#/.test(hash)) { hash = '#' + hash; } - window.history.replaceState({}, window.document.title, hash); + //window.history.replaceState({}, window.document.title, hash); + // XXX REVOCATION USE SAFE HERE } } cb({error: err});