From a62dc7cfaebe5d0cdfacbd213c3630b0f0af8a28 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Feb 2024 16:52:30 +0100 Subject: [PATCH] New support: key rotation part 2 --- lib/commands/admin-rpc.js | 19 ++ lib/commands/channel.js | 4 +- lib/hk-util.js | 3 +- www/admin/inner.js | 12 +- www/common/outer/mailbox-handlers.js | 2 +- www/common/outer/support.js | 252 ++++++++++++++++++++------- www/common/sframe-common-mailbox.js | 2 +- www/moderation/inner.js | 10 +- www/support/inner.js | 1 + 9 files changed, 224 insertions(+), 81 deletions(-) diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index f78f720a7..ab40802c4 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -942,6 +942,24 @@ var removeModerator = (Env, Server, cb, data) => { const id = Array.isArray(data) && data[1]; Moderators.delete(Env, id, cb); }; +var archiveSupport = (Env, Server, cb, data) => { + let supportPinKey = Env.supportPinKey; + console.log(supportPinKey); + getPinList(Env, Server, (err, list) => { + console.log(list); + if (err) { return void cb(err); } + let n = nThen; + list.forEach(id => { + n = n(waitFor => { + console.log(id); + archiveDocument(Env, Server, waitFor(), [null, {id, reason:'DISABLE_SUPPORT'}]); + }).nThen; + }); + n(() => { + cb(); + }); + }, [null, supportPinKey]); +}; var commands = { ACTIVE_SESSIONS: getActiveSessions, @@ -1011,6 +1029,7 @@ var commands = { DELETE_KNOWN_USER: deleteKnownUser, UPDATE_KNOWN_USER: updateKnownUser, + ARCHIVE_SUPPORT: archiveSupport, GET_MODERATORS: getModerators, ADD_MODERATOR: addModerator, REMOVE_MODERATOR: removeModerator, diff --git a/lib/commands/channel.js b/lib/commands/channel.js index 275e6bf1f..74326174d 100644 --- a/lib/commands/channel.js +++ b/lib/commands/channel.js @@ -357,7 +357,7 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) { // historyKeeper already knows how to handle metadata and message validation, so we just pass it off here // if the message isn't valid it won't be stored. - Env.historyKeeper.channelMessage(Server, channelStruct, fullMessage, function (err) { + Env.historyKeeper.channelMessage(Server, channelStruct, fullMessage, function (err, time) { if (err) { // Message not stored... return void cb(err); @@ -368,7 +368,7 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) { Server.send(userId, fullMessage); }); - cb(); + cb(void 0, time); }); diff --git a/lib/hk-util.js b/lib/hk-util.js index b92d846bf..b39429995 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -319,6 +319,7 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash, cb) Env.queueStorage(id, function (next) { const msgBin = Buffer.from(msg + '\n', 'utf8'); + const time = Array.isArray(msg) && msg[msg.length - 1]; // Store the message first, and update the index only once it's stored. // store.messageBin can be async so updating the index first may // result in a wrong cpIndex @@ -388,7 +389,7 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash, cb) } // Message stored, call back - cb(); + cb(void 0, time); var msgLength = msgBin.length; index.size += msgLength; diff --git a/www/admin/inner.js b/www/admin/inner.js index 855e7575c..341d53e2b 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -2973,16 +2973,12 @@ Example // the support UI for the clients spinner.spin(); $delButton.attr('disabled', 'disabled'); - sFrameChan.query('Q_ADMIN_RPC', { - cmd: 'ADMIN_DECREE', - data: ['SET_SUPPORT_MAILBOX2', ['', '']] - }, function (e, response) { + APP.supportModule.execCommand('DISABLE_SUPPORT', {}, (obj) => { $delButton.removeAttr('disabled'); - if (e || response.error) { + if (obj && obj.error) { UI.warn(Messages.error); - console.error(e, response); - spinner.hide(); - return; + console.error(obj.error); + return void spinner.hide(); } spinner.done(); UI.log(Messages.saved); diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index e9a5cc720..1ee649927 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -860,7 +860,7 @@ define([ handlers['NOTIF_TICKET'] = function (ctx, box, data, cb) { var msg = data.msg; var content = msg.content; - content.time = data.time; + if (!content.time) { content.time = data.time; } var support = Util.find(ctx, ['store', 'modules', 'support']); diff --git a/www/common/outer/support.js b/www/common/outer/support.js index 5b514e258..b36957334 100644 --- a/www/common/outer/support.js +++ b/www/common/outer/support.js @@ -147,12 +147,14 @@ define([ ctx.supportData[channel] = { time: +new Date(), title: title, - curvePublic: supportKey // XXX Should we store this value or delete the ticket on key rotation? + curvePublic: supportKey // Old tickets still use previous keys }; ctx.Store.onSync(null, waitFor()); }).nThen(() => { var supportChannel = Hash.getChannelIdFromKey(supportKey); + // XXX create tickt as an admin // XXX isAdmin + // First message to deal with the new ticket (store it in the list) mailbox.sendTo('NEW_TICKET', { title: title, channel: channel, @@ -165,7 +167,9 @@ define([ if (obj && obj.error) { delete ctx.supportData[channel]; } cb(obj); }); + // XXX create tickt as an admin // XXX isAdmin + // Second message is only a notification to warn the user/admins mailbox.sendTo('NOTIF_TICKET', { title: title, channel: channel, @@ -182,6 +186,7 @@ define([ if (!mailbox) { return void cb('E_NOT_READY'); } if (!anonRpc) { return void cb("anonymous rpc session not ready"); } var theirPublic, myCurve, notifKey; + var time; nThen((waitFor) => { // Get correct keys getKeys(ctx, isAdmin, data, waitFor((err, obj) => { @@ -202,12 +207,13 @@ define([ anonRpc.send("WRITE_PRIVATE_MESSAGE", [ data.channel, ciphertext - ], waitFor((err) => { + ], waitFor((err, res) => { if (err) { waitFor.abort(); return void cb(err); } - cb(); + time = res && res[0]; + cb(void 0, time); })); }).nThen(() => { // On success, notify @@ -218,6 +224,7 @@ define([ title: data.ticket.title, isClose: data.ticket.close, channel: data.channel, + time: time, user: Util.find(data.ticket, ['sender', 'curvePublic']) ? undefined : { supportTeam: true } @@ -245,13 +252,20 @@ define([ var t = Util.clone(ctx.supportData[ticket]); getContent(ctx, { channel: ticket, + curvePublic: t.curvePublic }, false, waitFor((err, messages) => { - if (err) { t.error = err; } - else { t.messages = messages; } - - if (messages[messages.length -1].close) { - ctx.supportData[ticket].closed = true; - t.closed = true; + if (err) { + if (err.type === 'EDELETED') { + delete ctx.supportData[ticket]; + return; + } + t.error = err; + } else { + t.messages = messages; + if (messages.length && messages[messages.length -1].close) { + ctx.supportData[ticket].closed = true; + t.closed = true; + } } t.id = ticket; @@ -309,28 +323,37 @@ define([ }; var loadTicketAdmin = function (ctx, data, cId, cb) { - getContent(ctx, data, true, function (err, res) { - if (err) { return void cb({error: err}); } - ctx.adminRdyEvt.reg(() => { + let supportKey = data.supportKey; + ctx.adminRdyEvt.reg(() => { + let doc = ctx.adminDoc.proxy; + // If this ticket was created with an older support key, use the old one + if (doc.oldKeys && doc.oldKeys[supportKey]) { + data.adminCurvePrivate = doc.oldKeys[supportKey].curvePrivate; + } + + getContent(ctx, data, true, function (err, res) { + if (err) { return void cb({error: err}); } var doc = ctx.adminDoc.proxy; - if (Array.isArray(res) && res.length) { - res.sort((t1, t2) => { return t1.time - t2.time; }); - let last = res[res.length - 1]; - let premium = res.some((msg) => { - let curve = Util.find(msg, ['sender', 'curvePublic']); - if (data.curvePublic !== curve) { return; } - return Util.find(msg, ['sender', 'quota', 'plan']); - }); - var senderKey = last.sender && last.sender.edPublic; + if (!Array.isArray(res) || !res.length) { return void cb(res); } - var entry = doc.tickets.active[data.channel]; - if (entry) { - entry.time = last.time; - entry.premium = premium; + // Sort messages by server time + res.sort((t1, t2) => { return t1.time - t2.time; }); + let last = res[res.length - 1]; + let premium = res.some((msg) => { + let curve = Util.find(msg, ['sender', 'curvePublic']); + if (data.curvePublic !== curve) { return; } + return Util.find(msg, ['sender', 'quota', 'plan']); + }); + var senderKey = last.sender && last.sender.edPublic; - if (senderKey) { - entry.lastAdmin = ctx.moderatorKeys.indexOf(senderKey) !== -1 - } + // Update ChainPad with latest ticket data + var entry = doc.tickets.active[data.channel]; + if (entry) { + entry.time = last.time; + entry.premium = premium; + + if (senderKey) { + entry.lastAdmin = ctx.moderatorKeys.indexOf(senderKey) !== -1 } } cb(res); @@ -340,24 +363,36 @@ define([ var replyTicketAdmin = function (ctx, data, cId, cb) { if (!ctx.adminRdyEvt) { return void cb({ error: 'EFORBIDDEN' }); } - replyTicket(ctx, data, true, (err) => { - if (err) { return void cb({error: err}); } - ctx.adminRdyEvt.reg(() => { + let supportKey = data.supportKey; + ctx.adminRdyEvt.reg(() => { + let doc = ctx.adminDoc.proxy; + // If this ticket was created with an older support key, use the old one + if (doc.oldKeys && doc.oldKeys[supportKey]) { + data.adminCurvePrivate = doc.oldKeys[supportKey].curvePrivate; + } + replyTicket(ctx, data, true, (err, time) => { + if (err) { return void cb({error: err}); } var doc = ctx.adminDoc.proxy; var entry = doc.tickets.active[data.channel] || doc.tickets.pending[data.channel]; - entry.time = +new Date(); + entry.time = time; entry.lastAdmin = true; + cb({sent: true}); }); - cb({sent: true}); }); }; var closeTicketAdmin = function (ctx, data, cId, cb) { if (!ctx.adminRdyEvt) { return void cb({ error: 'EFORBIDDEN' }); } // Reply with `data.ticket.close = true` - replyTicket(ctx, data, true, (err) => { - if (err) { return void cb({error: err}); } - ctx.adminRdyEvt.reg(() => { + let supportKey = data.supportKey; + ctx.adminRdyEvt.reg(() => { + let doc = ctx.adminDoc.proxy; + // If this ticket was created with an older support key, use the old one + if (doc.oldKeys && doc.oldKeys[supportKey]) { + data.adminCurvePrivate = doc.oldKeys[supportKey].curvePrivate; + } + replyTicket(ctx, data, true, (err) => { + if (err) { return void cb({error: err}); } var doc = ctx.adminDoc.proxy; var entry = doc.tickets.active[data.channel] || doc.tickets.pending[data.channel]; entry.time = +new Date(); @@ -381,7 +416,7 @@ define([ var addAdminTicket = function (ctx, data, cb) { // Wait for the chainpad to be ready before adding the data - if (!ctx.adminRdyEvt) { return void cb(false); } // XXX not an admin, delete mailbox? + if (!ctx.adminRdyEvt) { return void cb(true); } // XXX not an admin, delete mailbox? ctx.adminRdyEvt.reg(() => { let supportKey; @@ -390,7 +425,7 @@ define([ getKeys(ctx, false, data, waitFor((err, obj) => { if (err) { waitFor.abort(); - return void cb({error: err}); + return void cb(true); } supportKey = obj.theirPublic; })); @@ -401,7 +436,7 @@ define([ var doc = ctx.adminDoc.proxy; if (doc.tickets.active[data.channel] || doc.tickets.closed[data.channel] || doc.tickets.pending[data.channel]) { - return void cb(false); } + return void cb(true); } doc.tickets.active[data.channel] = { title: data.title, premium: data.premium, @@ -411,7 +446,9 @@ define([ authorKey: data.user && data.user.curvePublic }; Realtime.whenRealtimeSyncs(ctx.adminDoc.realtime, function () { - cb(false); + // Call back only when synced. That way we can handle the mailbox message + // later in case of network issues. + cb(true); }); notifyClient(ctx, true, 'NEW_TICKET', data.channel); if (ctx.supportRpc) { ctx.supportRpc.pin([data.channel], () => {}); } @@ -431,12 +468,14 @@ define([ if (!doc.tickets.active[data.channel] && !doc.tickets.pending[data.channel]) { return; } let t = doc.tickets.active[data.channel] || doc.tickets.pending[data.channel]; - if (data.time > (t.time + 2000)) { t.time = data.time; } if (data.isClose) { doc.tickets.closed[data.channel] = t; delete doc.tickets.active[data.channel]; delete doc.tickets.pending[data.channel]; } + if (data.time < t.time) { return; } + + t.time = data.time; t.lastAdmin = false; notifyClient(ctx, true, 'UPDATE_TICKET', data.channel); }); @@ -614,6 +653,7 @@ define([ editKeyStr: seed } }); + console.error(hash); loadAdminDoc(ctx, hash, waitFor()); }).nThen(() => { console.log('Support admin loaded') @@ -660,6 +700,7 @@ define([ nThen((waitFor) => { // Check if support was already enabled getKeys(ctx, false, {}, waitFor((err, obj) => { + if (err === 'E_NOT_INIT') { return; } // We're going to set up a support key if (err) { cb({error: err}); return void waitFor.abort(); @@ -796,6 +837,76 @@ define([ cb({success: true}); }); }; + let disableSupport = function (ctx, data, cId, _cb) { + let cb = Util.once(Util.mkAsync(_cb)); + let proxy = ctx.store.proxy; + let edPublic = proxy.edPublic; + + let supportKey; + let moderators; + nThen((waitFor) => { + // Check if support is enabled + getKeys(ctx, false, {}, waitFor((err, obj) => { + if (err) { + cb({error: err}); + return void waitFor.abort(); + } + supportKey = obj.theirPublic; + })); + }).nThen((waitFor) => { + // Only admins can disable support + if (!ctx.adminKeys.includes(edPublic)) { + waitFor.abort(); + return void cb({error: 'EFORBIDDEN'}); + } + }).nThen((waitFor) => { + // Archive ChainPad and all the tickets (from pin log) + ctx.Store.adminRpc(null, { + cmd: 'ARCHIVE_SUPPORT', + data: {} + }, waitFor((obj) => { + if (obj && obj.error) { + waitFor.abort(); + return void cb(obj); + } + })); + }).nThen((waitFor) => { + ctx.Store.adminRpc(null, { + cmd: 'ADMIN_DECREE', + data: ['SET_SUPPORT_KEYS', ['', '']] + }, waitFor(function (obj) { + if (obj && obj.error) { + waitFor.abort(); + return void cb(obj); + } + })); + }).nThen((waitFor) => { + getModerators(ctx, null, null, waitFor((obj) => { + if (!obj || obj.error) { + waitFor.abort(); + return void cb(); + } + moderators = obj[0] || {}; + })); + }).nThen((waitFor) => { + let n = nThen; + Object.keys(moderators).forEach((ed) => { + n = n((waitFor) => { + ctx.Store.adminRpc(null, { + cmd: 'REMOVE_MODERATOR', + data: ed + }, waitFor(obj => { + if (obj && obj.error) { + console.error('Error removing moderator data', ed, obj.error); + } + })); + }).nThen; + }); + n(() => { + cb(); + }); + }); + }; var getAdminKey = function (ctx, data, cId, cb) { let proxy = ctx.store.proxy; @@ -803,10 +914,12 @@ define([ let privateKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePrivate']); getKeys(ctx, false, {}, (err, obj) => { if (err) { return void cb({error: err}); } - if (obj.theirPublic !== supportKey) { return void cb({ error: 'EFORBIDDEN' }); } + if (supportKey && obj.theirPublic !== supportKey) { + privateKey = undefined; + } cb({ curvePrivate: privateKey, - curvePublic: supportKey + curvePublic: obj.theirPublic }); }); }; @@ -860,15 +973,14 @@ define([ if (Util.find(store, ['proxy', 'mailboxes', 'supportteam'])) { initializeSupportAdmin(ctx, false, waitFor); + window.CryptPad_SupportCtx = ctx; } support.ctx = ctx; support.removeClient = function (clientId) { delete ctx.clients[clientId]; }; - support.leavePad = function (padChan) { - // XXX TODO - }; + support.leavePad = function (padChan) {}; support.addAdminTicket = function (content, cb) { addAdminTicket(ctx, content, cb); }; @@ -887,30 +999,10 @@ define([ support.execCommand = function (clientId, obj, cb) { var cmd = obj.cmd; var data = obj.data; + // User commands if (cmd === 'MAKE_TICKET') { return void makeTicket(ctx, data, clientId, cb); } - if (cmd === 'LIST_TICKETS_ADMIN') { - return void listTicketsAdmin(ctx, data, clientId, cb); - } - if (cmd === 'LOAD_TICKET_ADMIN') { - return void loadTicketAdmin(ctx, data, clientId, cb); - } - if (cmd === 'REPLY_TICKET_ADMIN') { - return void replyTicketAdmin(ctx, data, clientId, cb); - } - if (cmd === 'CLOSE_TICKET_ADMIN') { - return void closeTicketAdmin(ctx, data, clientId, cb); - } - if (cmd === 'GET_PRIVATE_KEY') { - return void getAdminKey(ctx, data, clientId, cb); - } - if (cmd === 'ROTATE_KEYS') { - return void rotateKeys(ctx, data, clientId, cb); - } - if (cmd === 'ADD_MODERATOR') { - return void addModerator(ctx, data, clientId, cb); - } if (cmd === 'GET_MY_TICKETS') { return void getMyTickets(ctx, data, clientId, cb); } @@ -923,6 +1015,32 @@ define([ if (cmd === 'DELETE_TICKET') { return void deleteMyTicket(ctx, data, clientId, cb); } + // Moderator commands + if (cmd === 'LIST_TICKETS_ADMIN') { + return void listTicketsAdmin(ctx, data, clientId, cb); + } + if (cmd === 'LOAD_TICKET_ADMIN') { + return void loadTicketAdmin(ctx, data, clientId, cb); + } + if (cmd === 'REPLY_TICKET_ADMIN') { + return void replyTicketAdmin(ctx, data, clientId, cb); + } + if (cmd === 'CLOSE_TICKET_ADMIN') { + return void closeTicketAdmin(ctx, data, clientId, cb); + } + // Admin commands + if (cmd === 'GET_PRIVATE_KEY') { + return void getAdminKey(ctx, data, clientId, cb); + } + if (cmd === 'DISABLE_SUPPORT') { + return void disableSupport(ctx, data, clientId, cb); + } + if (cmd === 'ROTATE_KEYS') { + return void rotateKeys(ctx, data, clientId, cb); + } + if (cmd === 'ADD_MODERATOR') { + return void addModerator(ctx, data, clientId, cb); + } cb({error: 'NOT_SUPPORTED'}); }; diff --git a/www/common/sframe-common-mailbox.js b/www/common/sframe-common-mailbox.js index 92da483c3..857b46bbe 100644 --- a/www/common/sframe-common-mailbox.js +++ b/www/common/sframe-common-mailbox.js @@ -102,7 +102,7 @@ define([ h('div.cp-notification-content', { tabindex: tabIndexValue }, [ - h('p', formatData(data)) + h('p', data.content.msg.type + ' - ' +formatData(data)) ]) ]); diff --git a/www/moderation/inner.js b/www/moderation/inner.js index 519c7f84c..1f7949103 100644 --- a/www/moderation/inner.js +++ b/www/moderation/inner.js @@ -110,9 +110,11 @@ define([ }; const onShow = function (ticket, channel, data, done) { + let supportKey = data.supportKey; APP.module.execCommand('LOAD_TICKET_ADMIN', { channel: channel, - curvePublic: data.authorKey + curvePublic: data.authorKey, + supportKey: data.supportKey }, function (obj) { if (!Array.isArray(obj)) { console.error(obj && obj.error); @@ -148,6 +150,7 @@ define([ channel: channel, curvePublic: data.authorKey, notifChannel: data.notifications, + supportKey: data.supportKey, ticket: formData }, function (obj) { if (obj && obj.error) { @@ -164,6 +167,7 @@ define([ channel: channel, curvePublic: data.authorKey, notifChannel: data.notifications, + supportKey: data.supportKey, ticket: APP.support.getDebuggingData({ close: true }) @@ -311,6 +315,10 @@ define([ var privateData = metadataMgr.getPrivateData(); common.setTabTitle(Messages.supportPage); + if (!ApiConfig.supportMailboxKey) { + return void UI.errorLoadingScreen(Messages.support_disabledTitle); + } + APP.privateKey = privateData.supportPrivateKey; APP.origin = privateData.origin; APP.readOnly = privateData.readOnly; diff --git a/www/support/inner.js b/www/support/inner.js index 6dea486c7..cbb9a9224 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -321,6 +321,7 @@ define([ return void UI.warn(Messages.error); } id = Util.uid(); + events.UPDATE_TICKET.fire(); $('.cp-sidebarlayout-category[data-category="tickets"]').click(); }); });