mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-12 19:49:59 +05:00
New support: rotate keys
This commit is contained in:
parent
757f30d4a1
commit
20dd15b99d
@ -25,7 +25,9 @@ Moderators.add = (Env, edPublic, data, adminKey, _cb) => {
|
||||
const safeKey = Util.escapeKeyCharacters(edPublic);
|
||||
Moderator.write(Env, safeKey, data, (err) => {
|
||||
if (err) { return void cb(err); }
|
||||
Env.moderators.push(edPublic);
|
||||
if (!Env.moderators.includes(edPublic)) {
|
||||
Env.moderators.push(edPublic);
|
||||
}
|
||||
Env.envUpdated.fire();
|
||||
cb();
|
||||
});
|
||||
@ -33,9 +35,14 @@ Moderators.add = (Env, edPublic, data, adminKey, _cb) => {
|
||||
|
||||
Moderators.delete = (Env, id, _cb) => {
|
||||
const cb = Util.once(Util.mkAsync(_cb));
|
||||
Moderator.delete(Env, id, (err) => {
|
||||
const safeKey = Util.escapeKeyCharacters(id);
|
||||
Moderator.delete(Env, safeKey, (err) => {
|
||||
if (err && err !== 'ENOENT') { return void cb(err); }
|
||||
// XXX update Env.moderators
|
||||
let idx = Env.moderators.indexOf(id);
|
||||
if (idx !== -1) {
|
||||
Env.moderators.splice(idx, 1);
|
||||
Env.envUpdated.fire();
|
||||
}
|
||||
cb(void 0, true);
|
||||
});
|
||||
};
|
||||
|
||||
@ -40,7 +40,7 @@ SET_MAINTENANCE
|
||||
// EASIER CONFIG
|
||||
SET_ADMIN_EMAIL
|
||||
SET_SUPPORT_MAILBOX
|
||||
SET_SUPPORT_MAILBOX2
|
||||
SET_SUPPORT_KEYS
|
||||
|
||||
// COMMUNITY PARTICIPATION AND GOVERNANCE
|
||||
CONSENT_TO_CONTACT
|
||||
|
||||
@ -2926,12 +2926,13 @@ Example
|
||||
Messages.admin_supportNewConfirm = "Are you sure? This will remove access to all current moderators and delete all existing tickets.";
|
||||
Messages.admin_supportMembers = "Current support team";
|
||||
Messages.admin_supportAdd = "Add a contact to the support team";
|
||||
Messages.admin_supportRotateNotify = "Warning: new keys have been generated but an unenexpected error prevented the system to send them to the moderators. You may have to remove and re-add all the other moderators";
|
||||
create['support-new'] = function () {
|
||||
const $div = makeBlock('support-new'); // Msg.admin_supportNewHint, .admin_supportNewTitle
|
||||
let supportKey = ApiConfig.supportMailboxKey;
|
||||
let edPublic = common.getMetadataMgr().getPrivateData().edPublic; // My edPublic
|
||||
let refresh = function () {};
|
||||
const redraw = function (membersData, oldPrivKey) {
|
||||
const redraw = function (moderatorsData, oldPrivKey) {
|
||||
$div.empty();
|
||||
|
||||
const state = h('div');
|
||||
@ -2986,7 +2987,7 @@ Example
|
||||
spinner.done();
|
||||
UI.log(Messages.saved);
|
||||
supportKey = undefined;
|
||||
setState();
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -3006,7 +3007,7 @@ Example
|
||||
};
|
||||
console.log(oldPrivKey, getEncryptor(oldPrivKey));
|
||||
*/
|
||||
const getMemberData = (curve) => {
|
||||
const getContactData = (curve) => {
|
||||
let friends = common.getFriends(true);
|
||||
let f = friends[curve || 'me'];
|
||||
return {
|
||||
@ -3019,17 +3020,12 @@ Example
|
||||
};
|
||||
};
|
||||
const generateKey = function () {
|
||||
if (supportKey && !membersData[edPublic]) {
|
||||
if (supportKey && !moderatorsData[edPublic]) {
|
||||
UI.alert("A support key already exists. You must be a moderator to generate a new one or delete the existing support data.");
|
||||
return;
|
||||
}
|
||||
spinner.spin();
|
||||
$button.attr('disabled', 'disabled');
|
||||
const keyPair = Nacl.box.keyPair();
|
||||
const pub = Nacl.util.encodeBase64(keyPair.publicKey);
|
||||
const priv = Nacl.util.encodeBase64(keyPair.secretKey);
|
||||
const ed = Nacl.sign.keyPair.fromSeed(keypair.secretKey);
|
||||
const edPub = Nacl.util.encodeBase64(ed.publicKey);
|
||||
const onError = (waitFor, err, res) => {
|
||||
if (waitFor) { waitFor.abort(); }
|
||||
console.error(err, res);
|
||||
@ -3041,46 +3037,36 @@ Example
|
||||
//const newCrypto = getEncryptor(priv);
|
||||
|
||||
nThen((waitFor) => {
|
||||
// Send new key to server
|
||||
sFrameChan.query('Q_ADMIN_RPC', {
|
||||
cmd: 'ADMIN_DECREE',
|
||||
data: ['SET_SUPPORT_MAILBOX2', [pub, edPub]]
|
||||
}, waitFor((e, response) => {
|
||||
$button.removeAttr('disabled');
|
||||
if (e || response.error) { return void onError(waitFor, e, response); }
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
// Add to own mailbox
|
||||
sFrameChan.query("Q_ADMIN_MAILBOX", {
|
||||
version: 2,
|
||||
priv: priv
|
||||
}, waitFor((err, obj) => {
|
||||
if (err || (obj && obj.error)) { return void onError(waitFor, err, obj); }
|
||||
|
||||
spinner.done();
|
||||
UI.log(Messages.saved);
|
||||
supportKey = pub;
|
||||
setState();
|
||||
}));
|
||||
}).nThen(() => {
|
||||
// Add myself to moderator role if not already there
|
||||
let me = getContactData();
|
||||
if (moderatorsData[me.edPublic]) { return; }
|
||||
sFrameChan.query('Q_ADMIN_RPC', {
|
||||
cmd: 'ADD_MODERATOR',
|
||||
data: getMemberData()
|
||||
}, (e, response) => {
|
||||
data: me
|
||||
}, waitFor((e, response) => {
|
||||
console.error(e, response);
|
||||
});
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
// Copy chainpad doc and pin
|
||||
APP.supportModule.execCommand('ROTATE_KEYS', {}, waitFor((obj) => {
|
||||
$button.removeAttr('disabled');
|
||||
if (obj && obj.error) { return void onError(waitFor, obj.error); }
|
||||
if (obj && obj.success && obj.noNotify) {
|
||||
UI.alert(Messages.admin_supportRotateNotify);
|
||||
onError(waitFor, 'NOTIFY_ERROR', obj);
|
||||
return;
|
||||
}
|
||||
spinner.done();
|
||||
UI.log(Messages.saved);
|
||||
}));
|
||||
}).nThen(() => {
|
||||
// XXX migrate chainpad doc
|
||||
// XXX delete old pin log
|
||||
// XXX send new keys to members
|
||||
console.error('XXX TODO send to members');
|
||||
refresh();
|
||||
});
|
||||
};
|
||||
|
||||
const addSupportMember = (curve, _cb) => {
|
||||
const addModerator = (curve, _cb) => {
|
||||
let cb = Util.mkAsync(_cb);
|
||||
let userData = getMemberData(curve);
|
||||
let userData = getContactData(curve);
|
||||
if (!userData) { return void cb('INVALID_USER'); }
|
||||
sFrameChan.query('Q_ADMIN_RPC', {
|
||||
cmd: 'ADD_MODERATOR',
|
||||
@ -3097,6 +3083,18 @@ Example
|
||||
});
|
||||
});
|
||||
};
|
||||
const removeModerator = (ed) => {
|
||||
sFrameChan.query('Q_ADMIN_RPC', {
|
||||
cmd: 'REMOVE_MODERATOR',
|
||||
data: ed
|
||||
}, (e, response) => {
|
||||
if (e || (response && response.error)) {
|
||||
console.error(e || response.error);
|
||||
return void UI.warn(Messages.error);
|
||||
}
|
||||
generateKey();
|
||||
});
|
||||
};
|
||||
|
||||
Util.onClickEnter($button, function () {
|
||||
/*
|
||||
@ -3109,12 +3107,12 @@ Example
|
||||
generateKey();
|
||||
});
|
||||
|
||||
const drawMembers = () => {
|
||||
const drawModerators = () => {
|
||||
if (!supportKey) { return; }
|
||||
const members = {};
|
||||
const friends = Util.clone(common.getFriends(false))
|
||||
Object.keys(membersData).forEach((ed) => {
|
||||
let m = membersData[ed];
|
||||
Object.keys(moderatorsData).forEach((ed) => {
|
||||
let m = moderatorsData[ed];
|
||||
members[m.curvePublic] = {
|
||||
displayName: m.name,
|
||||
edPublic: m.edPublic,
|
||||
@ -3133,7 +3131,8 @@ Example
|
||||
noSelect: true,
|
||||
data: members,
|
||||
remove: (el) => {
|
||||
console.error('REMOVE', el);
|
||||
let ed = $(el).attr('data-ed');
|
||||
removeModerator(ed);
|
||||
}
|
||||
});
|
||||
let contactsGrid = UIElements.getUserGrid(Messages.admin_supportAdd, {
|
||||
@ -3153,14 +3152,14 @@ Example
|
||||
console.error('Missing data on selected user', el);
|
||||
return void UI.warn(Messages.error);
|
||||
}
|
||||
addSupportMember(curve, waitFor());
|
||||
addModerator(curve, waitFor());
|
||||
});
|
||||
}).nThen(() => {
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
// Only moderators can add new moderators
|
||||
if (!membersData[edPublic]) {
|
||||
if (!moderatorsData[edPublic]) {
|
||||
contactsGrid.div = undefined;
|
||||
addBtn = undefined;
|
||||
}
|
||||
@ -3172,13 +3171,14 @@ Example
|
||||
]);
|
||||
$div.append(list);
|
||||
};
|
||||
drawMembers();
|
||||
drawModerators();
|
||||
};
|
||||
refresh = () => {
|
||||
let oldKey, members;
|
||||
let oldKey, moderators;
|
||||
nThen((waitFor) => {
|
||||
APP.supportModule.execCommand('GET_PRIVATE_KEY', {}, waitFor((obj) => {
|
||||
oldKey = obj && obj.curvePrivate;
|
||||
supportKey = obj && obj.curvePublic;
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
sFrameChan.query('Q_ADMIN_RPC', {
|
||||
@ -3190,10 +3190,10 @@ Example
|
||||
UI.warn(Messages.error);
|
||||
return;
|
||||
}
|
||||
members = response[0];
|
||||
moderators = response[0];
|
||||
}));
|
||||
}).nThen(() => {
|
||||
redraw(members, oldKey);
|
||||
redraw(moderators, oldKey);
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
|
||||
@ -913,17 +913,18 @@ define([
|
||||
var msg = data.msg;
|
||||
var content = msg.content;
|
||||
var newKey = content.supportKey;
|
||||
// check if it matches the server key
|
||||
var pub = Hash.getBoxPublicFromSecret(newKey);
|
||||
if (pub !== ApiConfig.supportMailboxKey) { return void cb(true); }
|
||||
// We have a correct key: add support mailbox
|
||||
ctx.Store.addAdminMailbox(null, {
|
||||
version: 2,
|
||||
priv: newKey,
|
||||
lastKnownHash: content.lastKnownHash
|
||||
}, function (err) {
|
||||
if (err) { return void cb(true); }
|
||||
cb(false);
|
||||
|
||||
var support = Util.find(ctx, ['store', 'modules', 'support']);
|
||||
support.updateAdminKey(content, cb);
|
||||
};
|
||||
handlers['MODERATOR_NEW_KEY'] = function (ctx, box, data, cb) {
|
||||
var msg = data.msg;
|
||||
var content = msg.content;
|
||||
var newKey = content.supportKey;
|
||||
|
||||
var support = Util.find(ctx, ['store', 'modules', 'support']);
|
||||
support.updateAdminKey(content, function () {
|
||||
cb(true); // Always dismiss, this should be invisible
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -276,6 +276,7 @@ proxy.mailboxes = {
|
||||
|
||||
|
||||
var leaveChannel = function (ctx, type, cb) {
|
||||
cb = cb || function () {};
|
||||
var box = ctx.boxes[type];
|
||||
if (!box) { return void cb(); }
|
||||
if (!box.cpNf || typeof(box.cpNf.stop) !== "function") { return void cb('EINVAL'); }
|
||||
|
||||
@ -8,21 +8,26 @@ define([
|
||||
'/common/common-hash.js',
|
||||
'/common/common-realtime.js',
|
||||
'/common/pinpad.js',
|
||||
'/common/cryptget.js',
|
||||
'/components/nthen/index.js',
|
||||
'/components/chainpad-crypto/crypto.js',
|
||||
'chainpad-listmap',
|
||||
'/components/chainpad/chainpad.dist.js',
|
||||
'chainpad-netflux'
|
||||
], function (ApiConfig, Util, Hash, Realtime, Pinpad, nThen, Crypto, Listmap, ChainPad, CpNetflux) {
|
||||
], function (ApiConfig, Util, Hash, Realtime, Pinpad, Crypt,
|
||||
nThen, Crypto, Listmap, ChainPad, CpNetflux) {
|
||||
var Support = {};
|
||||
|
||||
var Nacl = Crypto.Nacl;
|
||||
|
||||
// UTILS
|
||||
|
||||
var getKeys = function (ctx, isAdmin, data, _cb) {
|
||||
var cb = Util.mkAsync(_cb);
|
||||
if (isAdmin && !ctx.adminRdyEvt) { return void cb('EFORBIDDEN'); }
|
||||
require(['/api/config?' + (+new Date())], function (NewConfig) {
|
||||
ctx.moderatorKeys = NewConfig.moderatorKeys; // Update admin keys // XXX MODERATOR
|
||||
ctx.moderatorKeys = NewConfig.moderatorKeys; // Update moderator keys
|
||||
ctx.adminKeys = NewConfig.adminKeys; // Update moderator keys
|
||||
|
||||
var supportKey = NewConfig.supportMailboxKey;
|
||||
if (!supportKey) { return void cb('E_NOT_INIT'); }
|
||||
@ -379,31 +384,44 @@ define([
|
||||
if (!ctx.adminRdyEvt) { return void cb(false); } // XXX not an admin, delete mailbox?
|
||||
|
||||
ctx.adminRdyEvt.reg(() => {
|
||||
// random timeout to avoid duplication wiht multiple admins
|
||||
var rdmTo = Math.floor(Math.random() * 2000); // Between 0 and 2000ms
|
||||
setTimeout(() => {
|
||||
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); }
|
||||
doc.tickets.active[data.channel] = {
|
||||
title: data.title,
|
||||
premium: data.premium,
|
||||
time: data.time,
|
||||
author: data.user && data.user.displayName,
|
||||
authorKey: data.user && data.user.curvePublic
|
||||
};
|
||||
Realtime.whenRealtimeSyncs(ctx.adminDoc.realtime, function () {
|
||||
cb(false);
|
||||
let supportKey;
|
||||
nThen((waitFor) => {
|
||||
// Send ticket to the admins and call back
|
||||
getKeys(ctx, false, data, waitFor((err, obj) => {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb({error: err});
|
||||
}
|
||||
supportKey = obj.theirPublic;
|
||||
}));
|
||||
}).nThen(() => {
|
||||
// random timeout to avoid duplication wiht multiple admins
|
||||
var rdmTo = Math.floor(Math.random() * 2000); // Between 0 and 2000ms
|
||||
setTimeout(() => {
|
||||
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); }
|
||||
doc.tickets.active[data.channel] = {
|
||||
title: data.title,
|
||||
premium: data.premium,
|
||||
time: data.time,
|
||||
author: data.user && data.user.displayName,
|
||||
supportKey: supportKey, // Store current support key
|
||||
authorKey: data.user && data.user.curvePublic
|
||||
};
|
||||
Realtime.whenRealtimeSyncs(ctx.adminDoc.realtime, function () {
|
||||
cb(false);
|
||||
});
|
||||
notifyClient(ctx, true, 'NEW_TICKET', data.channel);
|
||||
if (ctx.supportRpc) { ctx.supportRpc.pin([data.channel], () => {}); }
|
||||
});
|
||||
notifyClient(ctx, true, 'NEW_TICKET', data.channel);
|
||||
if (ctx.adminRpc) { ctx.adminRpc.pin([data.channel], () => {}); }
|
||||
});
|
||||
});
|
||||
};
|
||||
var updateAdminTicket = function (ctx, data) {
|
||||
// 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; } // XXX not an admin, delete mailbox?
|
||||
|
||||
ctx.adminRdyEvt.reg(() => {
|
||||
// random timeout to avoid duplication wiht multiple admins
|
||||
@ -442,10 +460,52 @@ define([
|
||||
}
|
||||
};
|
||||
|
||||
var updateAdminKey = (ctx, data, cb) => {
|
||||
let newKey = data.supportKey;
|
||||
let newKeyPub = Hash.getBoxPublicFromSecret(newKey);
|
||||
|
||||
let proxy = ctx.store.proxy;
|
||||
const oldKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePrivate']);
|
||||
const oldKeyPub = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePublic']);
|
||||
|
||||
getKeys(ctx, false, {}, (err, obj) => {
|
||||
if (err) { return void cb(true); }
|
||||
// already deprecated? abort
|
||||
if (newKeyPub !== obj.theirPublic) { return void cb(true); }
|
||||
// already known? abort
|
||||
if (oldKey === newKey || oldKeyPub === newKeyPub) { return void cb(true); }
|
||||
|
||||
// Close old data
|
||||
let mailbox = Util.find(ctx, [ 'store', 'mailbox' ]);
|
||||
try {
|
||||
if (ctx.adminDoc) { ctx.adminDoc.stop(); }
|
||||
if (mailbox) { mailbox.close('supportteam'); }
|
||||
if (ctx.supportRpc) { ctx.supportRpc.destroy(); }
|
||||
ctx.adminRdyEvt = Util.mkEvent(true);
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
// Store new key
|
||||
ctx.Store.addAdminMailbox(null, {
|
||||
version: 2,
|
||||
priv: newKey
|
||||
}, (obj) => {
|
||||
if (obj && obj.error) { return void cb(true); }
|
||||
// Reload moderator data
|
||||
nThen((waitFor) => {
|
||||
initializeSupportAdmin(ctx, true, waitFor);
|
||||
ctx.adminRdyEvt.reg(() => {
|
||||
notifyClient(ctx, true, 'UPDATE_RIGHTS');
|
||||
cb(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// INITIALIZE ADMIN
|
||||
|
||||
var getPinList = function (ctx) {
|
||||
if (!ctx.adminDoc || !ctx.adminRpc) { return; }
|
||||
if (!ctx.adminDoc || !ctx.supportRpc) { return; }
|
||||
let adminChan = ctx.adminDoc.metadata && ctx.adminDoc.metadata.channel;
|
||||
let doc = ctx.adminDoc.proxy;
|
||||
let t = doc.tickets;
|
||||
@ -459,7 +519,6 @@ define([
|
||||
};
|
||||
var initAdminRpc = function (ctx, _cb) {
|
||||
let cb = Util.mkAsync(_cb);
|
||||
let Nacl = Crypto.Nacl;
|
||||
let proxy = ctx.store.proxy;
|
||||
let curvePrivate = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePrivate']);
|
||||
if (!curvePrivate) { return void cb('EFORBIDDEN'); }
|
||||
@ -477,7 +536,7 @@ define([
|
||||
}, (e, call) => {
|
||||
if (e) { return void cb(e); }
|
||||
console.log("Support RPC ready, public key is ", edPublic);
|
||||
ctx.adminRpc = call;
|
||||
ctx.supportRpc = call;
|
||||
cb();
|
||||
});
|
||||
};
|
||||
@ -507,14 +566,14 @@ define([
|
||||
ctx.adminRdyEvt.fire();
|
||||
cb();
|
||||
|
||||
if (!ctx.adminRpc) { return; }
|
||||
if (!ctx.supportRpc) { return; }
|
||||
// Check pin list
|
||||
let list = getPinList(ctx);
|
||||
let local = Hash.hashChannelList(list);
|
||||
ctx.adminRpc.getServerHash(function (e, hash) {
|
||||
ctx.supportRpc.getServerHash(function (e, hash) {
|
||||
if (e) { return void console.warn(e); }
|
||||
if (hash !== local) {
|
||||
ctx.adminRpc.reset(list, function (e, hash) {
|
||||
ctx.supportRpc.reset(list, function (e, hash) {
|
||||
if (e) { console.warn(e); }
|
||||
});
|
||||
}
|
||||
@ -523,19 +582,22 @@ define([
|
||||
};
|
||||
|
||||
|
||||
var initializeSupportAdmin = function (ctx, waitFor) {
|
||||
var initializeSupportAdmin = function (ctx, isReset, waitFor) {
|
||||
let unlock = waitFor();
|
||||
let proxy = ctx.store.proxy;
|
||||
let supportKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePublic']);
|
||||
let privateKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePrivate']);
|
||||
ctx.adminRdyEvt = Util.mkEvent(true);
|
||||
if (!isReset) { ctx.adminRdyEvt = Util.mkEvent(true); }
|
||||
nThen((waitFor) => {
|
||||
getKeys(ctx, false, {}, waitFor((err, obj) => {
|
||||
setTimeout(unlock); // Unlock loading process
|
||||
if (err) { return void waitFor.abort(); }
|
||||
if (obj.theirPublic !== supportKey) {
|
||||
// Deprecated support key: no longer an admin!
|
||||
// XXX delete the mailbox?
|
||||
try {
|
||||
delete proxy.mailboxes.supportteam;
|
||||
ctx.store.mailbox.close('supportteam');
|
||||
} catch (e) {}
|
||||
delete ctx.adminRdyEvt;
|
||||
return void waitFor.abort();
|
||||
}
|
||||
}));
|
||||
@ -559,6 +621,182 @@ define([
|
||||
|
||||
};
|
||||
|
||||
let updateServerKey = (ctx, curvePublic, curvePrivate, cb) => {
|
||||
let edPrivate, edPublic
|
||||
try {
|
||||
let pair = Nacl.sign.keyPair.fromSeed(Nacl.util.decodeBase64(curvePrivate));
|
||||
edPublic = Nacl.util.encodeBase64(pair.publicKey);
|
||||
} catch (e) {
|
||||
return void cb(e);
|
||||
}
|
||||
ctx.Store.adminRpc(null, {
|
||||
cmd: 'ADMIN_DECREE',
|
||||
data: ['SET_SUPPORT_KEYS', [curvePublic, edPublic]]
|
||||
}, cb);
|
||||
};
|
||||
let getModerators = (ctx, data, cId, cb) => {
|
||||
ctx.Store.adminRpc(null, {
|
||||
cmd: 'GET_MODERATORS',
|
||||
data: {}
|
||||
}, cb);
|
||||
};
|
||||
let rotateKeys = function (ctx, data, cId, _cb) {
|
||||
let cb = Util.once(Util.mkAsync(_cb));
|
||||
let oldSupportKey;
|
||||
let proxy = ctx.store.proxy;
|
||||
let edPublic = proxy.edPublic;
|
||||
|
||||
const keyPair = Nacl.box.keyPair();
|
||||
const newKeyPub = Nacl.util.encodeBase64(keyPair.publicKey);
|
||||
const newKey = Nacl.util.encodeBase64(keyPair.secretKey);
|
||||
const newEd = Nacl.sign.keyPair.fromSeed(keyPair.secretKey);
|
||||
const newEdPub = Nacl.util.encodeBase64(newEd.publicKey);
|
||||
|
||||
const oldKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePrivate']);
|
||||
const oldKeyPub = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePublic']);
|
||||
|
||||
if (!newKey || !newKeyPub) { return void cb({ error: 'INVALID_KEY' }); }
|
||||
let oldAdminChan;
|
||||
nThen((waitFor) => {
|
||||
// Check if support was already enabled
|
||||
getKeys(ctx, false, {}, waitFor((err, obj) => {
|
||||
if (err) {
|
||||
cb({error: err});
|
||||
return void waitFor.abort();
|
||||
}
|
||||
oldSupportKey = obj.theirPublic;
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
// Only admins can rotate the keys
|
||||
if (!ctx.adminKeys.includes(edPublic)) {
|
||||
waitFor.abort();
|
||||
return void cb({error: 'EFORBIDDEN'});
|
||||
}
|
||||
}).nThen((waitFor) => {
|
||||
// If support is enabled, only current moderators can rotate keys.
|
||||
// Other admins can only delete the support
|
||||
if (!oldSupportKey) { return; } // support disabled
|
||||
if (!ctx.moderatorKeys.includes(edPublic)) {
|
||||
waitFor.abort();
|
||||
return void cb({error: 'EINVAL'});
|
||||
}
|
||||
if (oldKeyPub !== oldSupportKey) {
|
||||
waitFor.abort();
|
||||
return void cb({error: 'EFORBIDDEN'});
|
||||
}
|
||||
}).nThen((waitFor) => {
|
||||
// If support was enabled, migrate old chainpad
|
||||
if (!oldSupportKey) { return; } // No old doc to copy
|
||||
ctx.adminRdyEvt.reg(() => {
|
||||
let oldDoc = ctx.adminDoc.proxy;
|
||||
oldAdminChan = ctx.adminDoc.metadata && ctx.adminDoc.metadata.channel;
|
||||
|
||||
let seed = newKey.slice(0,24);
|
||||
let hash = Hash.getEditHashFromKeys({
|
||||
version: 2,
|
||||
type: 'support',
|
||||
keys: {
|
||||
editKeyStr: seed
|
||||
}
|
||||
});
|
||||
let cfg = {
|
||||
network: ctx.store.network,
|
||||
initialState: '{}'
|
||||
};
|
||||
var oldKeys = oldDoc.oldKeys = oldDoc.oldKeys || {};
|
||||
oldKeys[oldKeyPub] = {
|
||||
curvePrivate: oldKey,
|
||||
rotatedOn: +new Date(),
|
||||
rotatedBy: edPublic
|
||||
};
|
||||
Crypt.put(hash, JSON.stringify(oldDoc), waitFor((err) => {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb({error: err});
|
||||
}
|
||||
}), cfg);
|
||||
});
|
||||
}).nThen((waitFor) => {
|
||||
// Send new key to server
|
||||
updateServerKey(ctx, newKeyPub, newKey, waitFor((obj) => {
|
||||
if (obj && obj.error) {
|
||||
waitFor.abort();
|
||||
return void cb(obj);
|
||||
}
|
||||
}));
|
||||
}).nThen(() => {
|
||||
// From now on, each error may cause issue because
|
||||
// the server has already stored the new key
|
||||
|
||||
// Disconnect old doc and mailbox
|
||||
if (!oldSupportKey) { return; } // support disabled
|
||||
|
||||
if (ctx.adminDoc) { ctx.adminDoc.stop(); }
|
||||
|
||||
let mailbox = Util.find(ctx, [ 'store', 'mailbox' ]);
|
||||
if (mailbox) { mailbox.close('supportteam'); }
|
||||
|
||||
if (ctx.supportRpc) { ctx.supportRpc.destroy(); }
|
||||
|
||||
ctx.adminRdyEvt = Util.mkEvent(true);
|
||||
}).nThen((waitFor) => {
|
||||
// Add key to my proxy
|
||||
ctx.Store.addAdminMailbox(null, {
|
||||
version: 2,
|
||||
priv: newKey
|
||||
}, waitFor((obj) => {
|
||||
if (obj && obj.error) { // Should never happen with the previous checks
|
||||
waitFor.abort();
|
||||
if (oldSupportKey) {
|
||||
// If we weren't able to store the new key, abort and restore old keys
|
||||
return updateServerKey(ctx, oldKeyPub, oldKey, () => {
|
||||
return void cb(obj);
|
||||
});
|
||||
}
|
||||
return void cb(obj);
|
||||
}
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
// Notify other moderators
|
||||
if (!oldSupportKey) { return; }
|
||||
let mailbox = Util.find(ctx, [ 'store', 'mailbox' ]);
|
||||
getModerators(ctx, null, null, waitFor((obj) => {
|
||||
if (obj && obj.error) {
|
||||
return void cb({
|
||||
success: true,
|
||||
noNotify: true
|
||||
});
|
||||
}
|
||||
let all = obj && obj[0];
|
||||
Object.keys(all || {}).forEach((modEdPub) => {
|
||||
let modData = all[modEdPub];
|
||||
mailbox.sendTo('MODERATOR_NEW_KEY', {
|
||||
supportKey: newKey
|
||||
}, {
|
||||
channel: modData.mailbox,
|
||||
curvePublic: modData.curvePublic
|
||||
}, () => {});
|
||||
});
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
// Initialize new chainpad
|
||||
initializeSupportAdmin(ctx, true, waitFor);
|
||||
}).nThen((waitFor) => {
|
||||
// Clean old data
|
||||
if (!oldAdminChan) { return; }
|
||||
ctx.Store.adminRpc(null, {
|
||||
cmd: 'ARCHIVE_DOCUMENT',
|
||||
data: {
|
||||
id: oldAdminChan,
|
||||
reason: 'Deprecated support pad'
|
||||
}
|
||||
}, waitFor());
|
||||
}).nThen((waitFor) => {
|
||||
// Call back
|
||||
cb({success: true});
|
||||
});
|
||||
};
|
||||
|
||||
var getAdminKey = function (ctx, data, cId, cb) {
|
||||
let proxy = ctx.store.proxy;
|
||||
let supportKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePublic']);
|
||||
@ -567,7 +805,8 @@ define([
|
||||
if (err) { return void cb({error: err}); }
|
||||
if (obj.theirPublic !== supportKey) { return void cb({ error: 'EFORBIDDEN' }); }
|
||||
cb({
|
||||
curvePrivate: privateKey
|
||||
curvePrivate: privateKey,
|
||||
curvePublic: supportKey
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -611,6 +850,7 @@ define([
|
||||
|
||||
var ctx = {
|
||||
moderatorKeys: ApiConfig.moderatorKeys,
|
||||
adminKeys: ApiConfig.adminKeys,
|
||||
supportData: proxy,
|
||||
store: cfg.store,
|
||||
Store: cfg.Store,
|
||||
@ -619,7 +859,7 @@ define([
|
||||
};
|
||||
|
||||
if (Util.find(store, ['proxy', 'mailboxes', 'supportteam'])) {
|
||||
initializeSupportAdmin(ctx, waitFor);
|
||||
initializeSupportAdmin(ctx, false, waitFor);
|
||||
}
|
||||
|
||||
support.ctx = ctx;
|
||||
@ -635,6 +875,9 @@ define([
|
||||
support.updateAdminTicket = function (content) {
|
||||
updateAdminTicket(ctx, content);
|
||||
};
|
||||
support.updateAdminKey = function (content, cb) {
|
||||
updateAdminKey(ctx, content, cb);
|
||||
};
|
||||
support.checkAdminTicket = function (content, cb) {
|
||||
checkAdminTicket(ctx, content, cb);
|
||||
};
|
||||
@ -662,6 +905,9 @@ define([
|
||||
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);
|
||||
}
|
||||
|
||||
@ -43,7 +43,8 @@ define([
|
||||
var sframeChan;
|
||||
var events = {
|
||||
'NEW_TICKET': Util.mkEvent(),
|
||||
'UPDATE_TICKET': Util.mkEvent()
|
||||
'UPDATE_TICKET': Util.mkEvent(),
|
||||
'UPDATE_RIGHTS': Util.mkEvent()
|
||||
};
|
||||
|
||||
// XXX
|
||||
@ -68,6 +69,14 @@ define([
|
||||
APP.module.execCommand('LIST_TICKETS_ADMIN', {
|
||||
type: type
|
||||
}, (tickets) => {
|
||||
if (tickets.error) {
|
||||
if (tickets.error === 'EFORBIDDEN') {
|
||||
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
|
||||
}
|
||||
return void UI.errorLoadingScreen(tickets.error);
|
||||
}
|
||||
UI.removeLoadingScreen();
|
||||
|
||||
let activeForms = {};
|
||||
$container.find('.cp-support-form-container').each((i, el) => {
|
||||
let id = $(el).attr('data-id');
|
||||
@ -203,6 +212,7 @@ define([
|
||||
let _refresh = Util.throttle(refreshAll, 500);
|
||||
events.NEW_TICKET.reg(_refresh);
|
||||
events.UPDATE_TICKET.reg(_refresh);
|
||||
events.UPDATE_RIGHTS.reg(_refresh);
|
||||
|
||||
// Make sidebar layout
|
||||
const categories = {
|
||||
@ -301,11 +311,6 @@ define([
|
||||
var privateData = metadataMgr.getPrivateData();
|
||||
common.setTabTitle(Messages.supportPage);
|
||||
|
||||
if (!Array.isArray(ApiConfig.moderatorKeys) ||
|
||||
!ApiConfig.moderatorKeys.includes(privateData.edPublic)) {
|
||||
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
|
||||
}
|
||||
|
||||
APP.privateKey = privateData.supportPrivateKey;
|
||||
APP.origin = privateData.origin;
|
||||
APP.readOnly = privateData.readOnly;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user