define([ '/common/common-util.js', '/common/common-hash.js', '/common/common-constants.js', '/common/common-realtime.js', '/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', '/common/cryptget.js', '/common/outer/cache-store.js', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad/chainpad.dist.js', '/bower_components/nthen/index.js', '/bower_components/saferphore/index.js', '/bower_components/tweetnacl/nacl-fast.min.js', ], function (Util, Hash, Constants, Realtime, ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt, Cache, Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) { var Team = {}; var Nacl = window.nacl; var onStoreReady = Util.mkEvent(true); var openCachedTeamChat = function () {}; // Placeholder 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; } try { team.listmap.stop(); } catch (e) {} try { team.roster.stop(); } catch (e) {} team.proxy = {}; team.stopped = true; delete ctx.teams[teamId]; delete ctx.cache[teamId]; delete ctx.store.proxy.teams[teamId]; ctx.emit('LEAVE_TEAM', teamId, team.clients); ctx.updateMetadata(); if (ctx.store.mailbox) { 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); }; 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.onRpcReadyEvt.fire(); cb(); }, Cache); }); }; var onCacheReady = function (ctx, id, lm, roster, keys, cId, _cb) { var cb = Util.once(Util.mkAsync(_cb)); if (ctx.cache[id]) { return void cb(); } 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, onRpcReadyEvt: Util.mkEvent(true), offline: true }; ctx.cache[id] = team; // 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; }); // Broadcast an event to all the tabs displaying this team 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 team.getChatData = function () { var chatKeys = keys.chat || {}; var hash = chatKeys.edit || chatKeys.view; if (!hash) { return {}; } var secret = Hash.getSecrets('chat', hash); return { teamId: id, channel: secret.channel, secret: secret, validateKey: chatKeys.validateKey }; }; team.pin = function (data, cb) { if (!keys.drive.edPrivate) { return void cb({error: 'EFORBIDDEN'}); } 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 (!keys.drive.edPrivate) { return void cb({error: 'EFORBIDDEN'}); } 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}); }); }; // Create the proxy manager var loadSharedFolder = function (id, data, cb, isNew) { SF.load({ isNew: isNew, network: ctx.store.network || ctx.store.networkPromise, store: team, isNewChannel: ctx.Store.isNewChannel, Store: ctx.Store }, id, data, cb); }; var teamData = ctx.store.proxy.teams[team.id]; var hash = teamData.hash || teamData.roHash; var secret = Hash.getSecrets('team', hash, teamData.password); 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, loadSharedFolder: loadSharedFolder, settings: { drive: Util.find(ctx.store, ['proxy', 'settings', 'drive']) }, 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, loggedIn: true, log: function (msg) { // broadcast to all drive apps team.sendEvent("DRIVE_LOG", msg); }, rt: team.realtime, editKey: secret.keys.secondaryKey, readOnly: Boolean(!secret.keys.secondaryKey) }); team.secondaryKey = secret && secret.keys.secondaryKey; team.userObject = manager.user.userObject; nThen(function (waitFor) { // Load the shared folders ctx.teams[id] = team; registerChangeEvents(ctx, team, proxy); var network = ctx.store.network || ctx.store.networkPromise; SF.loadSharedFolders(ctx.Store, network, team, team.userObject, waitFor, function (data) { ctx.progress += 70/(ctx.numberOfTeams * data.max); ctx.updateProgress({ progress: ctx.progress }); }, true); }).nThen(function () { cb(); }); }; var onReady = function (ctx, id, lm, roster, keys, cId, cb) { // Update metadata var state = roster.getState(); var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]); if (teamData) { teamData.metadata = state.metadata; } var team; if (!ctx.store.proxy.teams[id]) { return; } nThen(function (waitFor) { onCacheReady(ctx, id, lm, roster, keys, cId, waitFor()); team = ctx.teams[id] || ctx.cache[id]; // Init Team RPC if (!keys.drive.edPrivate) { return; } initRpc(ctx, team, keys.drive, waitFor(function () {})); }).nThen(function (waitFor) { // Load the shared folders team.userObject.fixFiles(); SF.checkMigration(team.secondaryKey, team.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 }); }); }).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 () { team.offline = false; 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(); }); }; var checkTeamChannels = function (ctx, id, channel, roster, waitFor, cb) { var close = function () { if (ctx.cache[id] || ctx.teams[id]) { closeTeam(ctx, id); } delete ctx.store.proxy.teams[id]; delete ctx.onReadyHandlers[id]; waitFor.abort(); cb({error: 'ENOENT'}); }; if (channel) { ctx.store.anon_rpc.send("IS_NEW_CHANNEL", channel, waitFor(function (e, res) { if (res && res.length && typeof(res[0]) === 'boolean' && res[0]) { // Channel is empty: remove this team close(); } })); } if (roster) { ctx.store.anon_rpc.send("IS_NEW_CHANNEL", roster, waitFor(function (e, res) { if (res && res.length && typeof(res[0]) === 'boolean' && res[0]) { // Channel is empty: remove this team close(); } })); } }; // Progress: // One team = (30/(#teams))% // One shared folder = (70/(#teams * #folders))% var openChannel = function (ctx, teamData, id, _cb, cache) { var cb = Util.once(Util.mkAsync(_cb)); var hash = teamData.hash || teamData.roHash; var secret = Hash.getSecrets('team', hash, teamData.password); 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; // 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) { if (cache) { // If we're in cache mode, make sure we have a cache for this team Cache.getChannelCache(secret.channel, waitFor(function (err, obj) { var c = obj && obj.c; // content if (!c) { waitFor.abort(); cb({error: 'NOCACHE_DRIVE'}); } })); Cache.getChannelCache(rosterKeys.channel, waitFor(function (err, obj) { var c = obj && obj.c; // content var k = obj && obj.k; if (k && !rosterKeys.teamEdPublic) { rosterKeys.teamEdPublic = k; } if (!c) { waitFor.abort(); cb({error: 'NOCACHE_ROSTER'}); } })); return; } checkTeamChannels(ctx, id, secret.channel, rosterKeys.channel, waitFor, cb); }).nThen(function (waitFor) { var cacheRdy = { lm: false, roster: false, check: function () { if (!this.lm || !this.roster) { return; } if (!cache) { return; } // Both are cacheready! ctx.progress += 30/ctx.numberOfTeams; ctx.updateProgress({ progress: ctx.progress }); onCacheReady(ctx, id, lm, roster, keys, null, waitFor(cb)); this.check = function () {}; } }; // Load the proxy var cfg = { data: {}, readOnly: !Boolean(secret.keys.signKey), network: ctx.store.network || ctx.store.networkPromise, channel: secret.channel, crypto: crypto, ChainPad: ChainPad, Cache: Cache, metadata: { validateKey: secret.keys.validateKey || undefined, }, userName: 'team', classic: true }; 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('cacheready', function () { cacheRdy.lm = true; cacheRdy.check(); }); lm.proxy.on('ready', waitFor()); 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 || ctx.store.networkPromise, channel: rosterKeys.channel, keys: rosterKeys, store: ctx.store, lastKnownHash: rosterData.lastKnownHash, onCacheReady: function (_roster) { if (!cache) { return; } console.error('Corrupted roster cache, cant load this team offline', teamData); if (_roster && _roster.error === "CORRUPTED") { if (lm && typeof(lm.stop) === "function") { lm.stop(); } waitFor.abort(); cb({error: 'CACHE_CORRUPTED_ROSTER'}); return; } roster = _roster; cacheRdy.roster = true; cacheRdy.check(); }, Cache: Cache }, 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; } onStoreReady.reg(function () { // If you're allowed to edit the roster, try to update your data if (!rosterData.edit) { return; } var data = {}; 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 () { if (!cache) { ctx.progress += 30/ctx.numberOfTeams; ctx.updateProgress({ progress: ctx.progress }); } onReady(ctx, id, lm, roster, keys, null, cb); }); }; var createTeam = function (ctx, data, cId, _cb) { var cb = Util.once(_cb); var password = Hash.createChannelId(); var hash = Hash.createRandomHash('team', password); var secret = Hash.getSecrets('team', hash, password); var roHash = Hash.getViewHashFromKeys(secret); 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; var chatSecret = Hash.getSecrets('chat'); var chatHashes = Hash.getHashes(chatSecret); 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, owners: [ctx.store.proxy.edPublic] }; nThen(function (waitFor) { // Initialize the roster Roster.create({ network: ctx.store.network || ctx.store.networkPromise, channel: rosterKeys.channel, //sharedConfig.rosterChannel, owners: [ctx.store.proxy.edPublic], keys: rosterKeys, store: ctx.store, lastKnownHash: void 0, Cache: Cache }, 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'}); } })); }).nThen(function () { var id = Util.createRandomInteger(); config.onMetadataUpdate = function () { var team = ctx.teams[id]; if (!team) { return; } ctx.emit('ROSTER_CHANGE', id, team.clients); }; var lm = Listmap.create(config); var proxy = lm.proxy; proxy.version = 2; // No migration needed proxy.on('ready', function () { // Store keys in our drive 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, } }; var t = ctx.store.proxy.teams[id] = { owner: true, channel: secret.channel, hash: hash, roHash: roHash, password: password, keys: keys, //members: membersHashes.editHash, metadata: { name: data.name } }; // 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(); cb(); }); }).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); } } }); }); }; 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']); 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 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(); w(); }); }); }); }).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); } })); // 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'); closeTeam(ctx, teamId); cb(); }); }; var joinTeam = function (ctx, data, cId, cb) { var team = data.team; if (!(team.hash || team.roHash) || !team.channel || !team.password || !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(); cb(obj); }); }; 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); 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) if (ctx.store.messenger) { 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.offline) { return void cb({error: 'OFFLINE'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } if (data.metadata) { delete data.metadata.offline; } team.roster.metadata(data.metadata, function (err) { if (err) { return void cb({error: err}); } var localTeam = ctx.store.proxy.teams[teamId]; if (localTeam) { 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']), 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, 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, 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(); } 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(); }); }; 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'; 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); // And leave the chat channel try { ctx.store.messenger.removeClient(cId); } catch (e) {} openCachedTeamChat = function () {}; if (!id) { return void cb(); } // If the team is loading, add ourselves to the list if (ctx.onReadyHandlers[id] && !ctx.teams[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]) { 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); }; if (ctx.store.messenger) { ctx.store.messenger.openTeamChat(team.getChatData(), onUpdate, cId, cb); } else { openCachedTeamChat = function () { ctx.store.messenger.openTeamChat(team.getChatData(), onUpdate, cId, cb); }; } }; var createInviteLink = function (ctx, data, cId, _cb) { var cb = Util.mkAsync(Util.once(_cb)); 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; var teamName; try { teamName = roster.getState().metadata.name; } catch (err) { return void cb({ error: "TEAM_NAME_ERR" }); } var message = data.message; var name = data.name; /* 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) { (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; // 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(); return void cb({ error: "SET_PREVIEW_CONTENT" }); } }), 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; // 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, 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(); return void cb({ error: "SET_PREVIEW_CONTENT" }); } }), 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: { curvePublic: ephemeralKeys.curvePublic, displayName: data.name, pending: true, inviteChannel: inviteKeys.channel, previewChannel: previewKeys.channel, } }, w(function (err) { if (err) { w.abort(); cb(err); } })); }).nThen(function () { // call back empty if everything worked cb(); }); }; var getPreviewContent = function (ctx, data, cId, cb) { var seeds = data.seeds; var previewKeys; try { previewKeys = Invite.derivePreviewKeys(seeds.preview); } catch (err) { return void cb({ error: "INVALID_SEEDS" }); } Crypt.get({ // secrets channel: previewKeys.channel, type: 'pad', version: 2, keys: { cryptKey: previewKeys.cryptKey, }, }, function (err, val) { if (err) { return void cb({ error: err }); } if (!val) { return void cb({ error: 'DELETED' }); } var json = Util.tryParse(val); if (!json) { return void cb({ error: "parseError" }); } cb(json); }, { // cryptget opts network: ctx.store.network, initialState: '{}', }); }; var getInviteContent = function (ctx, data, cId, cb) { var bytes64 = data.bytes64; var previewKeys; try { previewKeys = Invite.deriveInviteKeys(bytes64); } catch (err) { return void cb({ error: "INVALID_SEEDS" }); } Crypt.get({ // secrets channel: previewKeys.channel, type: 'pad', version: 2, keys: { cryptKey: previewKeys.cryptKey, }, }, function (err, val) { if (err) { return void cb({error: err}); } if (!val) { return void cb({error: 'DELETED'}); } var json = Util.tryParse(val); if (!json) { return void cb({error: "parseError"}); } cb(json); }, { // 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 || ctx.store.networkPromise, channel: rosterData.channel, keys: rosterKeys, store: ctx.store, Cache: Cache }, 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); var mailboxChannel = Util.uint8ArrayToHex(hash.slice(32,48)); var curvePair = Nacl.box.keyPair.fromSecretKey(seed); return { channel: mailboxChannel, viewed: [], keys: { curvePrivate: Nacl.util.encodeBase64(curvePair.secretKey), curvePublic: Nacl.util.encodeBase64(curvePair.publicKey) } }; }; Team.init = function (cfg, waitFor, emit) { var team = {}; var store = cfg.store; if (!store.loggedIn || !store.proxy.edPublic) { return; } var ctx = { store: store, Store: cfg.Store, pinPads: cfg.pinPads, emit: emit, onReadyHandlers: {}, teams: {}, cache: {}, updateMetadata: cfg.updateMetadata, updateProgress: cfg.updateLoadingProgress, progress: 0 }; var teams = store.proxy.teams = store.proxy.teams || {}; ctx.numberOfTeams = Object.keys(teams).length; // 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 removeDuplicates = function () { 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)) { 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 Object.keys(teams).forEach(function (id) { 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) { delete ctx.onReadyHandlers[id]; delete ctx.cache[id]; var txt = typeof(err) === "string" ? err : (err.type || err.message); Feedback.send("TEAM_LOADING_ERROR="+txt); return void console.error(err); } console.debug('Team '+id+' cache ready'); }), true); }); // Proxy is ready, check if our team list has changed team.onReady = function (waitFor) { removeDuplicates(); // Close all the teams from our cache that have been removed and add waitFor to the // one that still exist var checkTeam = function (id) { if (!teams[id]) { closeTeam(ctx, id); delete ctx.onReadyHandlers[id]; return true; } return false; }; Object.keys(ctx.teams).forEach(checkTeam); Object.keys(ctx.onReadyHandlers).forEach(function (id) { var closed = checkTeam(id); if (closed) { return; } var team = ctx.store.proxy.teams[id]; var rosterChan = Util.find(team, ['keys', 'roster', 'channel']); var _cb = Util.once(Util.mkAsync(waitFor())); nThen(function (w) { checkTeamChannels(ctx, id, team.channel, rosterChan, w, _cb); }); ctx.onReadyHandlers[id].push({ cb: _cb }); }); // Load all the teams that weren't in our cache Object.keys(teams).forEach(function (id) { // Team already loaded? abort if (ctx.onReadyHandlers[id] || ctx.teams[id]) { return; } // Load team 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); } console.debug('Team '+id+' ready'); })); }); openCachedTeamChat(); onStoreReady.fire(); }; team.getTeam = function (id) { return ctx.teams[id]; }; team.getTeamsData = function (app) { var t = {}; 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, 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]) { 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) { if (!teams[teamId]) { return; } // When receiving a negative answer to a team invitation, remove // the pending user from the roster. if (pendingOnly && !isPending(teamId, curve)) { return; } 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) { 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); } }); }); }; 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); }; team.execCommand = function (clientId, obj, cb) { 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); } if (cmd === 'LIST_TEAMS') { return void listTeams(cb); } if (cmd === 'OPEN_TEAM_CHAT') { return void openTeamChat(ctx, data, clientId, cb); } 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); } if (cmd === 'INVITE_TO_TEAM') { if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void inviteToTeam(ctx, data, clientId, cb); } if (cmd === 'LEAVE_TEAM') { return void leaveTeam(ctx, data, clientId, cb); } if (cmd === 'JOIN_TEAM') { if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void joinTeam(ctx, data, clientId, cb); } if (cmd === 'REMOVE_USER') { return void removeUser(ctx, data, clientId, cb); } if (cmd === 'DELETE_TEAM') { if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void deleteTeam(ctx, data, clientId, cb); } if (cmd === 'CREATE_TEAM') { if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void createTeam(ctx, data, clientId, cb); } if (cmd === 'GET_EDITABLE_FOLDERS') { return void getEditableFolders(ctx, data, clientId, cb); } if (cmd === 'CREATE_INVITE_LINK') { return void createInviteLink(ctx, data, clientId, cb); } 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); } }; return team; }; Team.anonGetPreviewContent = function (cfg, data, cb) { getPreviewContent(cfg, data, null, cb); }; return Team; });