You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/common/outer/team.js

1982 lines
78 KiB
JavaScript

5 years ago
define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
5 years ago
'/common/proxy-manager.js',
'/common/userObject.js',
'/common/outer/sharedfolder.js',
'/common/outer/roster.js',
'/common/common-messaging.js',
'/common/common-feedback.js',
'/common/outer/invitation.js',
5 years ago
'/common/cryptget.js',
'/common/outer/cache-store.js',
5 years ago
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
5 years ago
'/bower_components/chainpad/chainpad.dist.js',
5 years ago
'/bower_components/nthen/index.js',
5 years ago
'/bower_components/saferphore/index.js',
5 years ago
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, Constants, Realtime,
ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt, Cache,
5 years ago
Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
5 years ago
var Team = {};
5 years ago
var Nacl = window.nacl;
var registerChangeEvents = function (ctx, team, proxy, fId) {
if (!team) { return; }
if (!fId) {
// Listen for shared folder password change
proxy.on('change', ['drive', UserObject.SHARED_FOLDERS], function (o, n, p) {
if (p.length > 3 && p[3] === 'password') {
var id = p[2];
var data = proxy.drive[UserObject.SHARED_FOLDERS][id];
var href = team.manager.user.userObject.getHref ?
team.manager.user.userObject.getHref(data) : data.href;
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets(parsed.type, parsed.hash, o);
// We've received a new password, we should update it locally
// NOTE: this is an async call because new password means new roHref!
// We need to wait for the new roHref in the proxy before calling the handlers
// because a read-only team will use it when connecting to the new channel
setTimeout(function () {
SF.updatePassword(ctx.Store, {
oldChannel: secret.channel,
password: n,
href: href
}, ctx.store.network, function () {
console.log('Shared folder password changed');
});
});
return false;
}
});
proxy.on('disconnect', function () {
team.offline = true;
team.sendEvent('NETWORK_DISCONNECT', team.id);
});
proxy.on('reconnect', function () {
team.offline = false;
team.sendEvent('NETWORK_RECONNECT', team.id);
});
}
proxy.on('change', [], function (o, n, p) {
if (fId) {
// Pin the new pads
if (p[0] === UserObject.FILES_DATA && typeof(n) === "object" && n.channel && !n.owners) {
var toPin = [n.channel];
// Also pin the onlyoffice channels if they exist
if (n.rtChannel) { toPin.push(n.rtChannel); }
if (n.lastVersion) { toPin.push(n.lastVersion); }
team.pin(toPin, function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
}
// Unpin the deleted pads (deleted <=> changed to undefined)
if (p[0] === UserObject.FILES_DATA && typeof(o) === "object" && o.channel && !n) {
var toUnpin = [o.channel];
var c = team.manager.findChannel(o.channel);
var exists = c.some(function (data) {
return data.fId !== fId;
});
if (!exists) { // Unpin
// Also unpin the onlyoffice channels if they exist
if (o.rtChannel) { toUnpin.push(o.rtChannel); }
if (o.lastVersion) { toUnpin.push(o.lastVersion); }
team.unpin(toUnpin, function (obj) {
if (obj && obj.error) { console.error(obj); }
});
}
}
}
if (o && !n && Array.isArray(p) && (p[0] === UserObject.FILES_DATA ||
(p[0] === 'drive' && p[1] === UserObject.FILES_DATA))) {
setTimeout(function () {
ctx.Store.checkDeletedPad(o && o.channel);
});
}
team.sendEvent('DRIVE_CHANGE', {
id: fId,
old: o,
new: n,
path: p
});
});
proxy.on('remove', [], function (o, p) {
team.sendEvent('DRIVE_REMOVE', {
id: fId,
old: o,
path: p
});
});
};
var closeTeam = function (ctx, teamId) {
var team = ctx.teams[teamId];
if (!team) { return; }
5 years ago
try { team.listmap.stop(); } catch (e) {}
try { team.roster.stop(); } catch (e) {}
team.proxy = {};
team.stopped = true;
delete ctx.teams[teamId];
delete ctx.store.proxy.teams[teamId];
ctx.emit('LEAVE_TEAM', teamId, team.clients);
ctx.updateMetadata();
ctx.store.mailbox.close('team-'+teamId, function () {
// Close team mailbox
});
};
var getTeamChannelList = function (ctx, id) {
// Get the list of pads' channel ID in your drive
// This list is filtered so that it doesn't include pad owned by other users
// It now includes channels from shared folders
var store = ctx.teams[id];
if (!store) { return null; }
var list = store.manager.getChannelsList('pin');
var team = ctx.store.proxy.teams[id];
list.push(team.channel);
var chatChannel = Util.find(team, ['keys', 'chat', 'channel']);
var membersChannel = Util.find(team, ['keys', 'roster', 'channel']);
var mailboxChannel = Util.find(team, ['keys', 'mailbox', 'channel']);
if (chatChannel) { list.push(chatChannel); }
if (membersChannel) { list.push(membersChannel); }
if (mailboxChannel) { list.push(mailboxChannel); }
var state = store.roster.getState();
if (state.members) {
Object.keys(state.members).forEach(function (curve) {
var m = state.members[curve];
if (m.inviteChannel && m.pending) { list.push(m.inviteChannel); }
if (m.previewChannel && m.pending) { list.push(m.previewChannel); }
});
}
list.sort();
return list;
};
var handleSharedFolder = function (ctx, id, sfId, rt) {
var t = ctx.teams[id];
if (!t) { return; }
if (!rt) {
delete t.sharedFolders[sfId];
return;
}
t.sharedFolders[sfId] = rt;
registerChangeEvents(ctx, t, rt.proxy, sfId);
};
5 years ago
var initRpc = function (ctx, team, data, cb) {
if (team.rpc) { return void cb(); }
if (!data.edPrivate || !data.edPublic) { return void cb('EFORBIDDEN'); }
require(['/common/pinpad.js'], function (Pinpad) {
Pinpad.create(ctx.store.network, data, function (e, call) {
if (e) { return void cb(e); }
team.rpc = call;
team.pin = function (data, cb) {
if (!team.rpc) { return void cb({error: 'TEAM_RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') { console.error('expected a callback'); }
team.rpc.pin(data, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
team.unpin = function (data, cb) {
if (!team.rpc) { return void cb({error: 'TEAM_RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') { console.error('expected a callback'); }
team.rpc.unpin(data, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
cb();
5 years ago
});
});
};
var onReady = function (ctx, id, lm, roster, keys, cId, cb) {
5 years ago
var proxy = lm.proxy;
var team = {
id: id,
proxy: proxy,
listmap: lm,
clients: [],
realtime: lm.realtime,
handleSharedFolder: function (sfId, rt) { handleSharedFolder(ctx, id, sfId, rt); },
sharedFolders: {}, // equivalent of store.sharedFolders in async-store
roster: roster
5 years ago
};
// Subscribe to events
if (cId) { team.clients.push(cId); }
// Listen for roster changes
roster.on('change', function () {
var state = roster.getState();
var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']);
if (!state.members || !Object.keys(state.members).length) {
// invalid roster, don't leave the team
console.error(JSON.stringify(state));
return;
}
if (!state.members[me]) {
return void closeTeam(ctx, id);
}
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]);
if (teamData) { teamData.metadata = state.metadata; }
ctx.updateMetadata();
ctx.emit('ROSTER_CHANGE', id, team.clients);
});
roster.on('checkpoint', function (hash) {
var rosterData = Util.find(ctx, ['store', 'proxy', 'teams', id, 'keys', 'roster']);
rosterData.lastKnownHash = hash;
});
// Update metadata
var state = roster.getState();
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]);
if (teamData) { teamData.metadata = state.metadata; }
// Broadcast an event to all the tabs displaying this team
5 years ago
team.sendEvent = function (q, data, sender) {
ctx.emit(q, data, team.clients.filter(function (cId) {
return cId !== sender;
}));
};
// Provide team chat keys to the messenger app
5 years ago
team.getChatData = function () {
var chatKeys = keys.chat || {};
var hash = chatKeys.edit || chatKeys.view;
if (!hash) { return {}; }
5 years ago
var secret = Hash.getSecrets('chat', hash);
return {
teamId: id,
5 years ago
channel: secret.channel,
secret: secret,
validateKey: chatKeys.validateKey
5 years ago
};
};
5 years ago
var secret;
team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
5 years ago
nThen(function (waitFor) {
// Init Team RPC
if (!keys.drive.edPrivate) { return; }
initRpc(ctx, team, keys.drive, waitFor(function () {}));
5 years ago
}).nThen(function () {
// Create the proxy manager
var loadSharedFolder = function (id, data, cb, isNew) {
5 years ago
SF.load({
isNew: isNew,
5 years ago
network: ctx.store.network,
store: team,
isNewChannel: ctx.Store.isNewChannel
}, id, data, cb);
5 years ago
};
var teamData = ctx.store.proxy.teams[team.id];
var hash = teamData.hash || teamData.roHash;
secret = Hash.getSecrets('team', hash, teamData.password);
5 years ago
var manager = team.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { ctx.Store.onSync(id, cb); },
edPublic: keys.drive.edPublic,
pin: team.pin,
unpin: team.unpin,
5 years ago
loadSharedFolder: loadSharedFolder,
settings: {
drive: Util.find(ctx.store, ['proxy', 'settings', 'drive'])
},
5 years ago
removeOwnedChannel: function (channel, cb) {
var data;
if (typeof(channel) === "object") {
channel.teamId = id;
data = channel;
} else {
data = {
channel: channel,
teamId: id
};
}
ctx.Store.removeOwnedChannel('', data, cb);
},
Store: ctx.Store
}, {
outer: true,
edPublic: keys.drive.edPublic,
5 years ago
loggedIn: true,
log: function (msg) {
// broadcast to all drive apps
team.sendEvent("DRIVE_LOG", msg);
5 years ago
},
rt: team.realtime,
editKey: secret.keys.secondaryKey,
readOnly: Boolean(!secret.keys.secondaryKey)
5 years ago
});
5 years ago
team.secondaryKey = secret && secret.keys.secondaryKey;
5 years ago
team.userObject = manager.user.userObject;
team.userObject.fixFiles();
}).nThen(function (waitFor) {
// Load the shared folders
ctx.teams[id] = team;
registerChangeEvents(ctx, team, proxy);
5 years ago
SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor());
SF.loadSharedFolders(ctx.Store, ctx.store.network, team,
team.userObject, waitFor, function (data) {
ctx.progress += 70/(ctx.numberOfTeams * data.max);
ctx.updateProgress({
progress: ctx.progress
});
});
5 years ago
}).nThen(function () {
if (!team.rpc) { return; }
var list = getTeamChannelList(ctx, id);
var local = Hash.hashChannelList(list);
// Check pin list
team.rpc.getServerHash(function (e, hash) {
if (e) { return void console.warn(e); }
if (hash !== local) {
// Reset pin list
team.rpc.reset(list, function (e/*, hash*/) {
if (e) { console.warn(e); }
});
}
});
}).nThen(function () {
5 years ago
if (ctx.onReadyHandlers[id]) {
ctx.onReadyHandlers[id].forEach(function (obj) {
// Callback and subscribe the client to new notifications
if (typeof (obj.cb) === "function") { obj.cb(); }
if (!obj.cId) { return; }
var idx = team.clients.indexOf(obj.cId);
if (idx === -1) {
team.clients.push(obj.cId);
}
});
}
delete ctx.onReadyHandlers[id];
cb();
});
};
// Progress:
// One team = (30/(#teams))%
// One shared folder = (70/(#teams * #folders))%
5 years ago
var openChannel = function (ctx, teamData, id, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
5 years ago
var hash = teamData.hash || teamData.roHash;
var secret = Hash.getSecrets('team', hash, teamData.password);
5 years ago
var crypto = Crypto.createEncryptor(secret.keys);
if (!teamData.roHash) {
teamData.roHash = Hash.getViewHashFromKeys(secret);
}
var keys = teamData.keys;
if (!keys.chat.validateKey && keys.chat.edit) {
var chatSecret = Hash.getSecrets('chat', keys.chat.edit);
keys.chat.validateKey = chatSecret.keys.validateKey;
}
var roster;
var lm;
5 years ago
// Roster keys
var myKeys = {
curvePublic: ctx.store.proxy.curvePublic,
curvePrivate: ctx.store.proxy.curvePrivate
};
var rosterData = keys.roster || {};
var rosterKeys = rosterData.edit ? Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys)
: Crypto.Team.deriveGuestKeys(rosterData.view || '');
nThen(function (waitFor) {
5 years ago
ctx.store.anon_rpc.send("IS_NEW_CHANNEL", secret.channel, waitFor(function (e, response) {
if (response && response.length && typeof(response[0]) === 'boolean' && response[0]) {
// Channel is empty: remove this team
delete ctx.store.proxy.teams[id];
waitFor.abort();
cb({error: 'ENOENT'});
}
}));
ctx.store.anon_rpc.send("IS_NEW_CHANNEL", rosterKeys.channel, waitFor(function (e, response) {
if (response && response.length && typeof(response[0]) === 'boolean' && response[0]) {
// Channel is empty: remove this team
delete ctx.store.proxy.teams[id];
waitFor.abort();
cb({error: 'ENOENT'});
}
}));
}).nThen(function (waitFor) {
// Load the proxy
var cfg = {
data: {},
readOnly: !Boolean(secret.keys.signKey),
network: ctx.store.network,
channel: secret.channel,
crypto: crypto,
ChainPad: ChainPad,
Cache: Cache, // XXX re-enable cache usage
metadata: {
validateKey: secret.keys.validateKey || undefined,
},
userName: 'team',
classic: true
};
5 years ago
cfg.onMetadataUpdate = function () {
var team = ctx.teams[id];
if (!team) { return; }
ctx.emit('ROSTER_CHANGE', id, team.clients);
};
lm = Listmap.create(cfg);
lm.proxy.on('ready', waitFor());
5 years ago
lm.proxy.on('error', function (info) {
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'});
}
if (info && info.error) {
if (info.error === "EDELETED" ) {
closeTeam(ctx, id);
}
}
});
// Load the roster
Roster.create({
network: ctx.store.network,
channel: rosterKeys.channel,
keys: rosterKeys,
anon_rpc: ctx.store.anon_rpc,
lastKnownHash: rosterData.lastKnownHash,
}, waitFor(function (err, _roster) {
if (err) {
waitFor.abort();
console.error(err);
return void cb({error: 'ROSTER_ERROR'});
}
roster = _roster;
rosterData.lastKnownHash = roster.getLastCheckpointHash();
// If we've been kicked, don't try to update our data, we'll close everything
// in the next nThen part
var state = roster.getState();
var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']);
if (!state.members[me]) { return; }
// If you're allowed to edit the roster, try to update your data
if (!rosterData.edit) { return; }
var data = {};
5 years ago
var myData = Messaging.createData(ctx.store.proxy, false);
myData.pending = false;
data[ctx.store.proxy.curvePublic] = myData;
roster.describe(data, function (err) {
if (!err) { return; }
if (err === 'NO_CHANGE') { return; }
console.error(err);
});
}));
}).nThen(function (waitFor) {
// Make sure we have not been kicked from the roster
var state = roster.getState();
var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']);
// FIXME roster history temporarily corrupted, don't leave the team
if (!state.members || !Object.keys(state.members).length) {
lm.stop();
roster.stop();
lm.proxy = {};
cb({error: 'EINVAL'});
waitFor.abort();
console.error(JSON.stringify(state));
Feedback.send("ROSTER_CORRUPTED");
return;
}
// Kicked from the team
if (!state.members[me]) {
lm.stop();
roster.stop();
lm.proxy = {};
delete ctx.store.proxy.teams[id];
ctx.updateMetadata();
cb({error: 'EFORBIDDEN'});
waitFor.abort();
return;
}
// Check access rights
// If we're not a viewer, make sure we have edit rights
var s = state.members[me];
var teamEdPrivate = Util.find(teamData, ['keys', 'drive', 'edPrivate']);
if ((!teamData.hash || !teamEdPrivate) && ['ADMIN', 'MEMBER'].indexOf(s.role) !== -1) {
console.warn("Missing edit rights: demote to viewer");
var data = {};
data[ctx.store.proxy.curvePublic] = {
role: "VIEWER"
};
roster.describe(data, function (err) {
Feedback.send("TEAM_RIGHTS_FIXED");
// Make sure we've removed all the keys
delete teamData.hash;
delete teamData.keys.drive.edPrivate;
delete teamData.keys.chat.edit;
if (!err) { return; }
if (err === 'NO_CHANGE') { return; }
console.error(err);
});
} else if ((!teamData.hash || !teamEdPrivate) && s.role === "OWNER") {
Feedback.send("TEAM_RIGHTS_OWNER");
}
}).nThen(function () {
ctx.progress += 30/ctx.numberOfTeams;
ctx.updateProgress({
progress: ctx.progress
});
onReady(ctx, id, lm, roster, keys, null, cb);
5 years ago
});
};
5 years ago
var createTeam = function (ctx, data, cId, _cb) {
var cb = Util.once(_cb);
5 years ago
var password = Hash.createChannelId();
var hash = Hash.createRandomHash('team', password);
var secret = Hash.getSecrets('team', hash, password);
var roHash = Hash.getViewHashFromKeys(secret);
5 years ago
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var curvePair = Nacl.box.keyPair();
var rosterSeed = Crypto.Team.createSeed();
var rosterKeys = Crypto.Team.deriveMemberKeys(rosterSeed, {
curvePublic: ctx.store.proxy.curvePublic,
curvePrivate: ctx.store.proxy.curvePrivate
});
var roster;
5 years ago
5 years ago
var chatSecret = Hash.getSecrets('chat');
var chatHashes = Hash.getHashes(chatSecret);
5 years ago
var config = {
network: ctx.store.network,
channel: secret.channel,
data: {},
validateKey: secret.keys.validateKey, // derived validation key
crypto: Crypto.createEncryptor(secret.keys),
logLevel: 1,
classic: true,
ChainPad: ChainPad,
Cache: Cache,
5 years ago
owners: [ctx.store.proxy.edPublic]
};
nThen(function (waitFor) {
// Initialize the roster
Roster.create({
network: ctx.store.network,
channel: rosterKeys.channel, //sharedConfig.rosterChannel,
owners: [ctx.store.proxy.edPublic],
keys: rosterKeys,
anon_rpc: ctx.store.anon_rpc,
lastKnownHash: void 0,
}, waitFor(function (err, _roster) {
if (err) {
waitFor.abort();
return void cb({error: 'ROSTER_ERROR'});
}
roster = _roster;
var myData = Messaging.createData(ctx.store.proxy);
delete myData.channel;
roster.init(myData, waitFor(function (err) {
if (err) {
waitFor.abort();
return void cb({error: 'ROSTER_INIT_ERROR'});
}
}));
}));
// Add yourself as owner of the chat channel
var crypto = Crypto.createEncryptor(chatSecret.keys);
var chatCfg = {
network: ctx.store.network,
channel: chatSecret.channel,
noChainPad: true,
crypto: crypto,
metadata: {
validateKey: chatSecret.keys.validateKey,
owners: [ctx.store.proxy.edPublic],
}
};
var chatReady = waitFor();
var cpNf2;
chatCfg.onReady = function () {
if (cpNf2) { cpNf2.stop(); }
chatReady();
};
chatCfg.onError = function () {
waitFor.abort();
return void cb({error: 'CHAT_INIT_ERROR'});
};
cpNf2 = CpNetflux.start(chatCfg);
}).nThen(function (waitFor) {
roster.metadata({
name: data.name
}, waitFor(function (err) {
if (err) {
waitFor.abort();
return void cb({error: 'ROSTER_INIT_ERROR'});
}
}));
5 years ago
}).nThen(function () {
var id = Util.createRandomInteger();
5 years ago
config.onMetadataUpdate = function () {
var team = ctx.teams[id];
if (!team) { return; }
ctx.emit('ROSTER_CHANGE', id, team.clients);
};
5 years ago
var lm = Listmap.create(config);
var proxy = lm.proxy;
proxy.version = 2; // No migration needed
5 years ago
proxy.on('ready', function () {
// Store keys in our drive
5 years ago
var keys = {
mailbox: {
channel: Hash.createChannelId(),
viewed: [],
keys: {
curvePrivate: Nacl.util.encodeBase64(curvePair.secretKey),
curvePublic: Nacl.util.encodeBase64(curvePair.publicKey)
}
},
drive: {
edPrivate: Nacl.util.encodeBase64(keyPair.secretKey),
edPublic: Nacl.util.encodeBase64(keyPair.publicKey)
},
chat: {
edit: chatHashes.editHash,
view: chatHashes.viewHash,
validateKey: chatSecret.keys.validateKey,
channel: chatSecret.channel
},
roster: {
channel: rosterKeys.channel,
edit: rosterSeed,
view: rosterKeys.viewKeyStr,
}
5 years ago
};
var t = ctx.store.proxy.teams[id] = {
owner: true,
channel: secret.channel,
5 years ago
hash: hash,
roHash: roHash,
5 years ago
password: password,
keys: keys,
//members: membersHashes.editHash,
metadata: {
name: data.name
}
5 years ago
};
// Initialize the team drive
proxy.drive = {};
onReady(ctx, id, lm, roster, keys, cId, function () {
Feedback.send('TEAM_CREATION');
ctx.store.mailbox.open('team-'+id, t.keys.mailbox, function () {
// Team mailbox loaded
}, true, {
owners: t.keys.drive.edPublic
});
ctx.updateMetadata();
5 years ago
cb();
5 years ago
});
5 years ago
}).on('error', function (info) {
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'});
}
5 years ago
if (info && info.error) {
if (info.error === "EDELETED") {
closeTeam(ctx, id);
}
}
5 years ago
});
5 years ago
});
};
5 years ago
var deleteTeam = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!team || !teamData) { return void cb ({error: 'ENOENT'}); }
var state = team.roster.getState();
var curvePublic = Util.find(ctx, ['store', 'proxy', 'curvePublic']);
var me = state.members[curvePublic];
if (!me || me.role !== "OWNER") { return cb({ error: "EFORBIDDEN"}); }
var edPublic = Util.find(ctx, ['store', 'proxy', 'edPublic']);
var teamEdPublic = Util.find(teamData, ['keys', 'drive', 'edPublic']);
5 years ago
nThen(function (waitFor) {
ctx.Store.anonRpcMsg(null, {
msg: 'GET_METADATA',
data: teamData.channel
}, waitFor(function (obj) {
// If we can't get owners, abort
if (obj && obj.error) {
waitFor.abort();
return cb({ error: obj.error});
}
// Check if we're an owner of the team drive
var metadata = obj[0];
if (metadata && Array.isArray(metadata.owners) &&
metadata.owners.indexOf(edPublic) !== -1) { return; }
// If w'e're not an owner, abort
waitFor.abort();
cb({error: 'EFORBIDDEN'});
}));
}).nThen(function (waitFor) {
team.proxy.delete = true;
// For each pad, check on the server if there are other owners.
// If yes, then remove yourself as an owner
// If no, delete the pad
5 years ago
var ownedPads = team.manager.getChannelsList('owned');
var sem = Saferphore.create(10);
ownedPads.forEach(function (c) {
var w = waitFor();
sem.take(function (give) {
var otherOwners = false;
nThen(function (_w) {
// Don't check server metadata for blobs
if (c.length !== 32) { return; }
ctx.Store.anonRpcMsg(null, {
msg: 'GET_METADATA',
data: c
}, _w(function (obj) {
if (obj && obj.error) {
give();
return void _w.abort();
}
var md = obj[0];
var isOwner = md && Array.isArray(md.owners) && md.owners.indexOf(teamEdPublic) !== -1;
if (!isOwner) {
give();
return void _w.abort();
}
otherOwners = md.owners.some(function (ed) { return ed !== teamEdPublic; });
}));
}).nThen(function (_w) {
if (otherOwners) {
ctx.Store.setPadMetadata(null, {
channel: c,
command: 'RM_OWNERS',
value: [teamEdPublic],
}, _w());
return;
}
// We're the only owner: delete the pad
team.rpc.removeOwnedChannel(c, _w(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function () {
give();
5 years ago
w();
});
5 years ago
});
});
}).nThen(function (waitFor) {
// Delete the pins log
team.rpc.removePins(waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the mailbox
var mailboxChan = Util.find(teamData, ['keys', 'mailbox', 'channel']);
team.rpc.removeOwnedChannel(mailboxChan, waitFor(function (err) {
if (err) { console.error(err); }
}));
5 years ago
// Delete the roster
var rosterChan = Util.find(teamData, ['keys', 'roster', 'channel']);
ctx.store.rpc.removeOwnedChannel(rosterChan, waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the chat
var chatChan = Util.find(teamData, ['keys', 'chat', 'channel']);
ctx.store.rpc.removeOwnedChannel(chatChan, waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the team drive
ctx.store.rpc.removeOwnedChannel(teamData.channel, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function () {
Feedback.send('TEAM_DELETION');
5 years ago
closeTeam(ctx, teamId);
cb();
5 years ago
});
};
5 years ago
var joinTeam = function (ctx, data, cId, cb) {
var team = data.team;
if (!(team.hash || team.roHash) || !team.channel || !team.password
5 years ago
|| !team.keys || !team.metadata) { return void cb({error: 'EINVAL'}); }
var id = Util.createRandomInteger();
ctx.store.proxy.teams[id] = team;
ctx.onReadyHandlers[id] = [];
openChannel(ctx, team, id, function (obj) {
if (!(obj && obj.error)) { console.debug('Team joined:' + id); }
var t = ctx.store.proxy.teams[id];
ctx.store.mailbox.open('team-'+id, t.keys.mailbox, function () {
// Team mailbox loaded
}, true, {
owners: t.keys.drive.edPublic
});
ctx.updateMetadata();
5 years ago
cb(obj);
});
};
5 years ago
var leaveTeam = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var curvePublic = ctx.store.proxy.curvePublic;
team.roster.remove([curvePublic], function (err) {
if (err) { return void cb({error: err}); }
closeTeam(ctx, teamId);
5 years ago
cb();
});
};
var getTeamRoster = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var state = team.roster.getState() || {};
var members = state.members || {};
// Get pending owners
var md = team.listmap.metadata || {};
if (Array.isArray(md.pending_owners)) {
// Get the members associated to the pending_owners' edPublic and mark them as such
md.pending_owners.forEach(function (ed) {
var member;
Object.keys(members).some(function (curve) {
if (members[curve].edPublic === ed) {
member = members[curve];
return true;
}
});
if (!member && teamData.owner) {
var removeOwnership = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: 'RM_PENDING_OWNERS',
value: [ed],
}, function () {});
};
removeOwnership(teamData.channel);
removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel']));
removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel']));
return;
}
member.pendingOwner = true;
});
}
// Add online status (using messenger data)
var chatData = team.getChatData();
var online = ctx.store.messenger.getOnlineList(chatData.channel) || [];
online.forEach(function (curve) {
if (members[curve]) {
members[curve].online = true;
}
});
cb(members);
};
// Return folders with edit rights available to everybody (decrypted pad href)
var getEditableFolders = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
var folders = team.manager.folders || {};
var ids = Object.keys(folders).filter(function (id) {
return !folders[id].proxy.version;
});
cb(ids.map(function (id) {
var uo = Util.find(team, ['user', 'userObject']);
return {
name: Util.find(folders, [id, 'proxy', 'metadata', 'title']),
path: uo ? uo.findFile(id)[0] : []
};
}));
};
var getTeamMetadata = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var state = team.roster.getState() || {};
var md = state.metadata || {};
md.offline = team.offline;
cb(md);
};
var setTeamMetadata = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
team.roster.metadata(data.metadata, function (err) {
if (err) { return void cb({error: err}); }
var localTeam = ctx.store.proxy.teams[teamId];
if (localTeam) {
5 years ago
localTeam.metadata = data.metadata;
}
cb();
});
};
var offerOwnership = function (ctx, data, cId, _cb) {
var cb = Util.once(_cb);
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
if (!data.curvePublic) { return void cb({error: 'MISSING_DATA'}); }
var state = team.roster.getState();
var user = state.members[data.curvePublic];
nThen(function (waitFor) {
// Offer ownership to a friend
var onError = function (res) {
var err = res && res.error;
if (err) {
console.error(err);
waitFor.abort();
return void cb({error:err});
}
};
var addPendingOwner = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: 'ADD_PENDING_OWNERS',
value: [user.edPublic],
}, waitFor(onError));
};
// Team proxy
addPendingOwner(teamData.channel);
// Team roster
addPendingOwner(Util.find(teamData, ['keys', 'roster', 'channel']));
// Team chat
addPendingOwner(Util.find(teamData, ['keys', 'chat', 'channel']));
}).nThen(function (waitFor) {
var obj = {};
obj[user.curvePublic] = {
role: 'OWNER'
};
team.roster.describe(obj, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function (waitFor) {
// Send mailbox to offer ownership
ctx.store.mailbox.sendTo("ADD_OWNER", {
teamChannel: teamData.channel,
chatChannel: Util.find(teamData, ['keys', 'chat', 'channel']),
rosterChannel: Util.find(teamData, ['keys', 'roster', 'channel']),
5 years ago
title: teamData.metadata.name
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, waitFor());
}).nThen(function () {
cb();
});
};
var revokeOwnership = function (ctx, teamId, user, _cb) {
var cb = Util.once(_cb);
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
var md = team.listmap.metadata || {};
var isPendingOwner = (md.pending_owners || []).indexOf(user.edPublic) !== -1;
nThen(function (waitFor) {
var cmd = isPendingOwner ? 'RM_PENDING_OWNERS' : 'RM_OWNERS';
var onError = function (res) {
var err = res && res.error;
if (err) {
console.error(err);
waitFor.abort();
return void cb(err);
}
};
var removeOwnership = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: cmd,
value: [user.edPublic],
}, waitFor(onError));
};
// Team proxy
removeOwnership(teamData.channel);
// Team roster
removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel']));
// Team chat
removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel']));
}).nThen(function (waitFor) {
var obj = {};
obj[user.curvePublic] = {
role: 'ADMIN',
pendingOwner: false
};
team.roster.describe(obj, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function (waitFor) {
// Send mailbox to offer ownership
ctx.store.mailbox.sendTo("RM_OWNER", {
teamChannel: teamData.channel,
title: teamData.metadata.name,
5 years ago
pending: isPendingOwner
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, waitFor());
}).nThen(function () {
cb();
});
};
// We've received an offer to be an owner of the team.
// If we accept, we need to set the "owner" flag in our team data
// If we decline, we need to change our role back to "ADMIN"
var answerOwnership = function (ctx, data, cId, cb) {
var myTeams = ctx.store.proxy.teams;
var teamId;
Object.keys(myTeams).forEach(function (id) {
if (myTeams[id].channel === data.teamChannel) {
teamId = id;
return true;
}
});
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var obj = {};
// Accept
if (data.answer) {
teamData.owner = true;
return;
}
// Decline
obj[ctx.store.proxy.curvePublic] = {
role: 'ADMIN',
};
team.roster.describe(obj, function (err) {
if (err) { return void cb({error: err}); }
cb();
});
};
var getInviteData = function (ctx, teamId, edit) {
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return {}; }
var data = Util.clone(teamData);
if (!edit) {
// Delete edit keys
delete data.hash;
delete data.keys.drive.edPrivate;
delete data.keys.chat.edit;
}
// Delete owner key
delete data.owner;
return data;
};
// Update my edit rights in listmap (only upgrade) and userObject (upgrade and downgrade)
// We also need to propagate the changes to the shared folders
var updateMyRights = function (ctx, teamId, hash) {
if (!teamId) { return true; }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return true; }
var team = ctx.teams[teamId];
if (!team) { return true; }
var secret = Hash.getSecrets('team', hash || teamData.roHash, teamData.password);
// Upgrade the listmap if we can
SF.upgrade(teamData.channel, secret);
// Set the new readOnly value in userObject
if (team.userObject) {
team.userObject.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey);
}
if (!secret.keys.secondaryKey && team.rpc) {
team.rpc.destroy();
}
// Upgrade the shared folders
var folders = Util.find(team, ['proxy', 'drive', 'sharedFolders']);
Object.keys(folders || {}).forEach(function (sfId) {
var data = team.manager.getSharedFolderData(sfId);
var parsed = Hash.parsePadUrl(data.href || data.roHref);
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
SF.upgrade(secret.channel, secret);
var uo = Util.find(team, ['manager', 'folders', sfId, 'userObject']);
if (uo) {
uo.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey);
}
});
ctx.updateMetadata();
ctx.emit('ROSTER_CHANGE_RIGHTS', teamId, team.clients);
};
var changeMyRights = function (ctx, teamId, state, data, cb) {
if (!teamId) { return void cb(false); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb(false); }
var onReady = ctx.onReadyHandlers[teamId];
var team = ctx.teams[teamId];
if (teamData.channel !== data.channel || teamData.password !== data.password) { return void cb(false); }
// Update our proxy
if (state) {
teamData.hash = data.hash;
teamData.keys.drive.edPrivate = data.keys.drive.edPrivate;
teamData.keys.chat.edit = data.keys.chat.edit;
} else {
delete teamData.hash;
delete teamData.keys.drive.edPrivate;
delete teamData.keys.chat.edit;
}
// Team not ready yet: try again onReady
if (!team && Array.isArray(onReady)) {
onReady.push({
cb: function () {
changeMyRights(ctx, teamId, state, data, cb);
}
});
return;
}
// No team and not initialized at all...
if (!team) { return void cb(false); }
// Team is initialized and ready: update the loaded elements
if (state) {
initRpc(ctx, team, teamData.keys.drive, function () {
team.manager.addPin(team.pin, team.unpin);
});
var secret = Hash.getSecrets('team', data.hash, teamData.password);
team.secondaryKey = secret && secret.keys.secondaryKey;
var crypto = Crypto.createEncryptor(secret.keys);
team.listmap.setReadOnly(false, crypto);
} else {
delete team.secondaryKey;
if (team.rpc && team.rpc.destroy) {
team.rpc.destroy();
}
team.manager.removePin();
team.listmap.setReadOnly(true);
}
updateMyRights(ctx, teamId, data.hash);
cb(true);
};
var changeEditRights = function (ctx, teamId, user, state, cb) {
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
// Send mailbox to offer ownership
ctx.store.mailbox.sendTo("TEAM_EDIT_RIGHTS", {
state: state,
5 years ago
teamData: getInviteData(ctx, teamId, state)
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, cb);
};
var describeUser = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
if (!data.curvePublic || !data.data) { return void cb({error: 'MISSING_DATA'}); }
var state = team.roster.getState();
var user = state.members[data.curvePublic];
// It it is an ownership revocation, we have to set it in pad metadata first
if (user.role === "OWNER" && data.data.role !== "OWNER") {
revokeOwnership(ctx, teamId, user, function (err) {
if (!err) { return void cb(); }
5 years ago
console.error(err);
return void cb({error: err});
});
return;
}
// Viewer to editor
if (user.role === "VIEWER" && data.data.role !== "VIEWER") {
changeEditRights(ctx, teamId, user, true, function (err) {
return void cb({error: err});
});
}
// Editor to viewer
if (user.role !== "VIEWER" && data.data.role === "VIEWER") {
changeEditRights(ctx, teamId, user, false, function (err) {
return void cb({error: err});
});
}
var obj = {};
obj[data.curvePublic] = data.data;
team.roster.describe(obj, function (err) {
if (err) { return void cb({error: err}); }
cb();
});
};
5 years ago
var inviteToTeam = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var user = data.user;
if (!user || !user.curvePublic || !user.notifications) { return void cb({error: 'MISSING_DATA'}); }
delete user.channel;
delete user.lastKnownHash;
user.pending = true;
var obj = {};
obj[user.curvePublic] = user;
obj[user.curvePublic].role = 'VIEWER';
5 years ago
team.roster.add(obj, function (err) {
if (err && err !== 'NO_CHANGE') { return void cb({error: err}); }
ctx.store.mailbox.sendTo('INVITE_TO_TEAM', {
team: getInviteData(ctx, teamId)
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, function (obj) {
cb(obj);
});
});
};
var removeUser = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
if (!data.curvePublic) { return void cb({error: 'MISSING_DATA'}); }
var state = team.roster.getState();
var userData = state.members[data.curvePublic];
team.roster.remove([data.curvePublic], function (err) {
if (err) { return void cb({error: err}); }
// The user has been removed, send them a notification
if (!userData || !userData.notifications) { return cb(); }
ctx.store.mailbox.sendTo('KICKED_FROM_TEAM', {
pending: data.pending,
teamChannel: getInviteData(ctx, teamId).channel,
teamName: getInviteData(ctx, teamId).metadata.name
}, {
channel: userData.notifications,
curvePublic: userData.curvePublic
}, function (obj) {
cb(obj);
});
});
};
// Remove a client from all the team they're subscribed to
var removeClient = function (ctx, cId) {
Object.keys(ctx.onReadyHandlers).forEach(function (teamId) {
var idx = -1;
ctx.onReadyHandlers[teamId].some(function (obj, _idx) {
if (obj.cId === cId) {
idx = _idx;
return true;
}
});
if (idx !== -1) {
ctx.onReadyHandlers[teamId].splice(idx, 1);
}
});
Object.keys(ctx.teams).forEach(function (id) {
var clients = ctx.teams[id].clients;
var idx = clients.indexOf(cId);
if (idx !== -1) { clients.splice(idx, 1); }
});
};
var subscribe = function (ctx, id, cId, cb) {
// Unsubscribe from other teams: one tab can only receive events about one team
removeClient(ctx, cId);
5 years ago
// And leave the chat channel
try {
ctx.store.messenger.removeClient(cId);
} catch (e) {}
if (!id) { return void cb(); }
5 years ago
// If the team is loading, as ourselves in the list
if (ctx.onReadyHandlers[id]) {
var _idx = ctx.onReadyHandlers[id].indexOf(cId);
if (_idx === -1) {
ctx.onReadyHandlers[id].push({
cId: cId,
cb: cb
});
}
return;
}
// Otherwise, subscribe to new notifications
if (!ctx.teams[id]) {
5 years ago
return void cb({error: 'EINVAL'});
}
var clients = ctx.teams[id].clients;
var idx = clients.indexOf(cId);
if (idx === -1) {
clients.push(cId);
}
cb();
};
var openTeamChat = function (ctx, data, cId, cb) {
var team = ctx.teams[data.teamId];
if (!team) { return void cb({error: 'ENOENT'}); }
var onUpdate = function () {
ctx.emit('ROSTER_CHANGE', data.teamId, team.clients);
};
ctx.store.messenger.openTeamChat(team.getChatData(), onUpdate, cId, cb);
};
var createInviteLink = function (ctx, data, cId, _cb) {
var cb = Util.mkAsync(Util.once(_cb));
5 years ago
var teamId = data.teamId;
var team = ctx.teams[data.teamId];
var seeds = data.seeds; // {scrypt, preview}
var bytes64 = data.bytes64;
if (!teamId || !team) { return void cb({error: 'EINVAL'}); }
var roster = team.roster;
5 years ago
var teamName;
try {
teamName = roster.getState().metadata.name;
} catch (err) {
5 years ago
return void cb({ error: "TEAM_NAME_ERR" });
5 years ago
}
var message = data.message;
var name = data.name;
5 years ago
/*
var password = data.password;
var hash = data.hash;
*/
// derive { channel, cryptKey} for the preview content channel
var previewKeys = Invite.derivePreviewKeys(seeds.preview);
// derive {channel, cryptkey} for the invite content channel
var inviteKeys = Invite.deriveInviteKeys(bytes64);
// randomly generate ephemeral keys for ownership of the above content
// and a placeholder in the roster
var ephemeralKeys = Invite.generateKeys();
nThen(function (w) {
5 years ago
(function () {
// a random signing keypair to prevent further writes to the channel
// we don't need to remember it cause we're only writing once
var sign = Invite.generateSignPair(); // { validateKey, signKey}
var putOpts = {
initialState: '{}',
network: ctx.store.network,
metadata: {
owners: [ctx.store.proxy.edPublic, ephemeralKeys.edPublic]
}
};
putOpts.metadata.validateKey = sign.validateKey;
5 years ago
// visible with only the invite link
var previewContent = {
teamName: teamName,
message: message,
author: Messaging.createData(ctx.store.proxy, false),
displayName: name,
};
var cryptput_config = {
channel: previewKeys.channel,
type: 'pad',
version: 2,
keys: { // what would normally be provided by getSecrets
cryptKey: previewKeys.cryptKey,
validateKey: sign.validateKey, // sent to historyKeeper
signKey: sign.signKey, // b64EdPrivate
},
};
Crypt.put(cryptput_config, JSON.stringify(previewContent), w(function (err /*, doc */) {
if (err) {
console.error("CRYPTPUT_ERR", err);
w.abort();
5 years ago
return void cb({ error: "SET_PREVIEW_CONTENT" });
}
5 years ago
}), putOpts);
}());
(function () {
// a different random signing key so that the server can't correlate these documents
// as components of an invite
var sign = Invite.generateSignPair(); // { validateKey, signKey}
var putOpts = {
initialState: '{}',
network: ctx.store.network,
metadata: {
owners: [ctx.store.proxy.edPublic, ephemeralKeys.edPublic]
}
};
putOpts.metadata.validateKey = sign.validateKey;
5 years ago
// available only with the link and the content
var inviteContent = {
teamData: getInviteData(ctx, teamId, false),
ephemeral: {
edPublic: ephemeralKeys.edPublic,
edPrivate: ephemeralKeys.edPrivate,
curvePublic: ephemeralKeys.curvePublic,
curvePrivate: ephemeralKeys.curvePrivate,
},
};
var cryptput_config = {
channel: inviteKeys.channel,
5 years ago
type: 'pad',
version: 2,
keys: {
cryptKey: inviteKeys.cryptKey,
validateKey: sign.validateKey,
signKey: sign.signKey,
},
};
Crypt.put(cryptput_config, JSON.stringify(inviteContent), w(function (err /*, doc */) {
if (err) {
console.error("CRYPTPUT_ERR", err);
w.abort();
5 years ago
return void cb({ error: "SET_PREVIEW_CONTENT" });
}
5 years ago
}), putOpts);
}());
}).nThen(function (w) {
team.pin([inviteKeys.channel, previewKeys.channel], function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
Invite.createRosterEntry(team.roster, {
curvePublic: ephemeralKeys.curvePublic,
content: {
5 years ago
curvePublic: ephemeralKeys.curvePublic,
displayName: data.name,
pending: true,
5 years ago
inviteChannel: inviteKeys.channel,
previewChannel: previewKeys.channel,
}
}, w(function (err) {
if (err) {
w.abort();
cb(err);
}
}));
}).nThen(function () {
// call back empty if everything worked
cb();
});
};
5 years ago
var getPreviewContent = function (ctx, data, cId, cb) {
var seeds = data.seeds;
var previewKeys;
try {
previewKeys = Invite.derivePreviewKeys(seeds.preview);
} catch (err) {
5 years ago
return void cb({ error: "INVALID_SEEDS" });
5 years ago
}
Crypt.get({ // secrets
channel: previewKeys.channel,
type: 'pad',
version: 2,
keys: {
cryptKey: previewKeys.cryptKey,
},
}, function (err, val) {
5 years ago
if (err) { return void cb({ error: err }); }
if (!val) { return void cb({ error: 'DELETED' }); }
5 years ago
var json = Util.tryParse(val);
5 years ago
if (!json) { return void cb({ error: "parseError" }); }
cb(json);
5 years ago
}, { // cryptget opts
network: ctx.store.network,
initialState: '{}',
});
};
var getInviteContent = function (ctx, data, cId, cb) {
var bytes64 = data.bytes64;
5 years ago
var previewKeys;
try {
previewKeys = Invite.deriveInviteKeys(bytes64);
} catch (err) {
5 years ago
return void cb({ error: "INVALID_SEEDS" });
5 years ago
}
Crypt.get({ // secrets
channel: previewKeys.channel,
type: 'pad',
version: 2,
keys: {
cryptKey: previewKeys.cryptKey,
},
}, function (err, val) {
5 years ago
if (err) { return void cb({error: err}); }
if (!val) { return void cb({error: 'DELETED'}); }
5 years ago
var json = Util.tryParse(val);
5 years ago
if (!json) { return void cb({error: "parseError"}); }
cb(json);
5 years ago
}, { // cryptget opts
network: ctx.store.network,
initialState: '{}',
});
};
var acceptLinkInvitation = function (ctx, data, cId, cb) {
var inviteContent;
var rosterState;
nThen(function (waitFor) {
// Get team keys and ephemeral keys
getInviteContent(ctx, data, cId, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
inviteContent = obj;
}));
}).nThen(function (waitFor) {
// Check if you're already a member of this team
var chan = Util.find(inviteContent, ['teamData', 'channel']);
var myTeams = ctx.store.proxy.teams || {};
var isMember = Object.keys(myTeams).some(function (k) {
var t = myTeams[k];
return t.channel === chan;
});
if (isMember) {
waitFor.abort();
return void cb({error: 'ALREADY_MEMBER'});
}
// Accept the roster invitation: relplace our ephemeral keys with our user keys
var rosterData = Util.find(inviteContent, ['teamData', 'keys', 'roster']);
var myKeys = inviteContent.ephemeral;
if (!rosterData || !myKeys) {
waitFor.abort();
return void cb({error: 'INVALID_INVITE_CONTENT'});
}
var rosterKeys = Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys);
Roster.create({
network: ctx.store.network,
channel: rosterData.channel,
keys: rosterKeys,
anon_rpc: ctx.store.anon_rpc,
}, waitFor(function (err, roster) {
if (err) {
waitFor.abort();
console.error(err);
return void cb({error: 'ROSTER_ERROR'});
}
var myData = Messaging.createData(ctx.store.proxy, false);
var state = roster.getState();
rosterState = state.members[myKeys.curvePublic];
roster.accept(myData.curvePublic, waitFor(function (err) {
roster.stop();
if (err) {
waitFor.abort();
console.error(err);
return void cb({error: 'ACCEPT_ERROR'});
}
}));
}));
}).nThen(function () {
var tempRpc = {};
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
if (err) { return; }
var rpc = tempRpc.rpc;
if (rosterState.inviteChannel) {
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
if (err) { console.error(err); }
});
}
if (rosterState.previewChannel) {
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
if (err) { console.error(err); }
});
}
});
// Add the team to our list and join...
joinTeam(ctx, {
team: inviteContent.teamData
}, cId, cb);
});
};
var deriveMailbox = function (team) {
if (!team) { return; }
if (team.keys && team.keys.mailbox) { return team.keys.mailbox; }
var strSeed = Util.find(team, ['keys', 'roster', 'edit']);
if (!strSeed) { return; }
var hash = Nacl.hash(Nacl.util.decodeUTF8(strSeed));
var seed = hash.slice(0,32);
5 years ago
var mailboxChannel = Util.uint8ArrayToHex(hash.slice(32,48));
var curvePair = Nacl.box.keyPair.fromSecretKey(seed);
return {
5 years ago
channel: mailboxChannel,
viewed: [],
keys: {
curvePrivate: Nacl.util.encodeBase64(curvePair.secretKey),
curvePublic: Nacl.util.encodeBase64(curvePair.publicKey)
}
};
};
5 years ago
Team.init = function (cfg, waitFor, emit) {
var team = {};
var store = cfg.store;
if (!store.loggedIn || !store.proxy.edPublic) { return; }
var ctx = {
store: store,
5 years ago
Store: cfg.Store,
5 years ago
pinPads: cfg.pinPads,
emit: emit,
5 years ago
onReadyHandlers: {},
5 years ago
teams: {},
updateMetadata: cfg.updateMetadata,
updateProgress: cfg.updateLoadingProgress,
progress: 0
5 years ago
};
var teams = store.proxy.teams = store.proxy.teams || {};
ctx.numberOfTeams = Object.keys(teams).length;
5 years ago
// Listen for changes in our access rights (if another worker receives edit access)
ctx.store.proxy.on('change', ['teams'], function (o, n, p) {
if (p[2] !== 'hash') { return; }
updateMyRights(ctx, p[1], n);
});
ctx.store.proxy.on('remove', ['teams'], function (o, p) {
if (p[2] !== 'hash') { return; }
updateMyRights(ctx, p[1]);
});
var checkKeyPair = function (edPrivate, edPublic) {
if (!edPrivate || !edPublic) { return true; }
try {
var secretKey = Nacl.util.decodeBase64(edPrivate);
var pair = Nacl.sign.keyPair.fromSecretKey(secretKey);
return Nacl.util.encodeBase64(pair.publicKey) === edPublic;
} catch (e) {
return false;
}
};
// Remove duplicate teams
var _teams = {};
Object.keys(teams).forEach(function (id) {
try {
var t = teams[id];
var _t = _teams[t.channel];
var edPrivate = Util.find(t, ['keys', 'drive', 'edPrivate']);
var edPublic = Util.find(t, ['keys', 'drive', 'edPublic']);
// If the edPrivate is corrupted, remove it
if (!edPublic) {
Feedback.send("TEAM_CORRUPTED_EDPUBLIC");
} else if (edPrivate && edPublic && !checkKeyPair(edPrivate, edPublic)) {
4 years ago
Feedback.send("TEAM_CORRUPTED_EDPRIVATE");
delete teams[id].keys.drive.edPrivate;
edPrivate = undefined;
}
// If the hash is corrupted, feedback
if (t.hash) {
var parsed = Hash.parseTypeHash('drive', t.hash);
if (parsed.version === 2 && t.hash.length !== 40) {
Feedback.send("TEAM_CORRUPTED_HASH");
// FIXME ?
}
}
// Not found yet? add to the list
if (!_t) {
_teams[t.channel] = id;
return;
}
// Duplicate found: update our team to add missing data
var best = teams[_t]; // This is a proxy!
var bestPrivate = Util.find(best, ['keys', 'drive', 'edPrivate']);
var bestChat = Util.find(best, ['keys', 'chat', 'edit']);
var chat = Util.find(t, ['keys', 'chat', 'edit']);
if (!best.hash && t.hash) {
best.hash = t.hash;
}
if (!bestPrivate && edPrivate) {
best.keys.drive.edPrivate = edPrivate;
}
if (!bestChat && chat) {
best.keys.chat.edit = chat;
}
// Deprecate the duplicate
ctx.store.proxy.duplicateTeams = ctx.store.proxy.duplicateTeams || {};
ctx.store.proxy.duplicateTeams[id] = teams[id];
delete teams[id];
} catch (e) { console.error(e); }
});
// Load teams
5 years ago
Object.keys(teams).forEach(function (id) {
5 years ago
ctx.onReadyHandlers[id] = [];
if (!Util.find(teams, [id, 'keys', 'mailbox'])) {
teams[id].keys.mailbox = deriveMailbox(teams[id]);
}
openChannel(ctx, teams[id], id, waitFor(function (err) {
if (err) {
var txt = typeof(err) === "string" ? err : (err.type || err.message);
Feedback.send("TEAM_LOADING_ERROR="+txt);
return void console.error(err);
}
5 years ago
console.debug('Team '+id+' ready');
5 years ago
}));
5 years ago
});
team.getTeam = function (id) {
return ctx.teams[id];
};
5 years ago
team.getTeamsData = function (app) {
var t = {};
5 years ago
var safe = false;
if (['drive', 'teams', 'settings'].indexOf(app) !== -1) { safe = true; }
Object.keys(teams).forEach(function (id) {
if (!ctx.teams[id]) { return; }
t[id] = {
owner: teams[id].owner,
name: teams[id].metadata.name,
5 years ago
edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']),
avatar: Util.find(teams[id], ['metadata', 'avatar']),
viewer: !Util.find(teams[id], ['keys', 'drive', 'edPrivate']),
notifications: Util.find(teams[id], ['keys', 'mailbox', 'channel']),
curvePublic: Util.find(teams[id], ['keys', 'mailbox', 'keys', 'curvePublic']),
validKeys: checkKeyPair(Util.find(teams[id], ['keys', 'drive', 'edPrivate']), Util.find(teams[id], ['keys', 'drive', 'edPublic']))
};
if (safe && ctx.teams[id]) {
5 years ago
t[id].secondaryKey = ctx.teams[id].secondaryKey;
}
if (ctx.teams[id]) {
t[id].hasSecondaryKey = Boolean(ctx.teams[id].secondaryKey);
}
});
return t;
};
team.getTeams = function () {
return Object.keys(ctx.teams);
};
var isPending = function (teamId, curve) {
var team = ctx.teams[teamId];
if (!team) { return; }
var state = team.roster && team.roster.getState();
if (!state.members) { return; }
var m = state.members[curve] || {};
return m.pending;
};
team.removeFromTeam = function (teamId, curve, pendingOnly) {
5 years ago
if (!teams[teamId]) { return; }
// When receiving a negative answer to a team invitation, remove
// the pending user from the roster.
4 years ago
if (pendingOnly && !isPending(teamId, curve)) { return; }
5 years ago
if (ctx.onReadyHandlers[teamId]) {
ctx.onReadyHandlers[teamId].push({cb : function () {
ctx.teams[teamId].roster.remove([curve], function (err) {
if (err && err !== 'NO_CHANGE') { console.error(err); }
});
}});
return;
}
var team = ctx.teams[teamId];
if (!team) { return void console.error("TEAM MODULE ERROR"); }
team.roster.remove([curve], function (err) {
5 years ago
if (err && err !== 'NO_CHANGE') { console.error(err); }
});
};
team.changeMyRights = function (id, edit, teamData, cb) {
changeMyRights(ctx, id, edit, teamData, cb);
};
team.updateMyData = function (data) {
Object.keys(ctx.teams).forEach(function (id) {
var team = ctx.teams[id];
if (!team.roster) { return; }
var obj = {};
obj[data.curvePublic] = data;
team.roster.describe(obj, function (err) {
if (err) { console.error(err); }
});
});
};
5 years ago
team.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
var listTeams = function (cb) {
var t = Util.clone(teams);
Object.keys(t).forEach(function (id) {
// If failure to load the team, don't send it
if (ctx.teams[id]) {
t[id].offline = ctx.teams[id].offline;
return;
}
t[id].error = true;
});
cb(t);
};
5 years ago
team.execCommand = function (clientId, obj, cb) {
5 years ago
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {
// Only the team app will subscribe to events?
return void subscribe(ctx, data, clientId, cb);
}
5 years ago
if (cmd === 'LIST_TEAMS') {
return void listTeams(cb);
5 years ago
}
5 years ago
if (cmd === 'OPEN_TEAM_CHAT') {
return void openTeamChat(ctx, data, clientId, cb);
5 years ago
}
if (cmd === 'GET_TEAM_ROSTER') {
return void getTeamRoster(ctx, data, clientId, cb);
}
if (cmd === 'GET_TEAM_METADATA') {
return void getTeamMetadata(ctx, data, clientId, cb);
}
if (cmd === 'SET_TEAM_METADATA') {
return void setTeamMetadata(ctx, data, clientId, cb);
}
if (cmd === 'OFFER_OWNERSHIP') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void offerOwnership(ctx, data, clientId, cb);
}
if (cmd === 'ANSWER_OWNERSHIP') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void answerOwnership(ctx, data, clientId, cb);
}
if (cmd === 'DESCRIBE_USER') {
return void describeUser(ctx, data, clientId, cb);
}
5 years ago
if (cmd === 'INVITE_TO_TEAM') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
5 years ago
return void inviteToTeam(ctx, data, clientId, cb);
}
5 years ago
if (cmd === 'LEAVE_TEAM') {
return void leaveTeam(ctx, data, clientId, cb);
}
5 years ago
if (cmd === 'JOIN_TEAM') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
5 years ago
return void joinTeam(ctx, data, clientId, cb);
}
if (cmd === 'REMOVE_USER') {
return void removeUser(ctx, data, clientId, cb);
}
5 years ago
if (cmd === 'DELETE_TEAM') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
5 years ago
return void deleteTeam(ctx, data, clientId, cb);
}
5 years ago
if (cmd === 'CREATE_TEAM') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
5 years ago
return void createTeam(ctx, data, clientId, cb);
5 years ago
}
if (cmd === 'GET_EDITABLE_FOLDERS') {
return void getEditableFolders(ctx, data, clientId, cb);
}
if (cmd === 'CREATE_INVITE_LINK') {
return void createInviteLink(ctx, data, clientId, cb);
}
5 years ago
if (cmd === 'GET_PREVIEW_CONTENT') {
return void getPreviewContent(ctx, data, clientId, cb);
}
if (cmd === 'ACCEPT_LINK_INVITATION') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void acceptLinkInvitation(ctx, data, clientId, cb);
}
5 years ago
};
return team;
};
Team.anonGetPreviewContent = function (cfg, data, cb) {
getPreviewContent(cfg, data, null, cb);
};
5 years ago
return Team;
});