diff --git a/www/common/cryptget.js b/www/common/cryptget.js index 39c43f857..8a1684dba 100644 --- a/www/common/cryptget.js +++ b/www/common/cryptget.js @@ -29,8 +29,15 @@ define([ }; var makeConfig = function (hash, opt) { + var secret; + if (typeof(hash) === 'string') { // We can't use cryptget with a file or a user so we can use 'pad' as hash type - var secret = Hash.getSecrets('pad', hash, opt.password); + secret = Hash.getSecrets('pad', hash, opt.password); + } else if (typeof(hash) === 'object') { + // we may want to just supply options directly + // and this is the easiest place to do it + secret = hash; + } if (!secret.keys) { secret.keys = secret.key; } // support old hashses var config = { websocketURL: NetConfig.getWebsocketURL(opt.origin), @@ -109,8 +116,9 @@ define([ Realtime.whenRealtimeSyncs(realtime, function () { clearTimeout(to); + var doc = realtime.getAuthDoc(); realtime.abort(); - finish(Session, void 0); + finish(Session, void 0, doc); }); }; overwrite(config, opt); diff --git a/www/common/invitation.js b/www/common/invitation.js index cc85a7016..d54182dec 100644 --- a/www/common/invitation.js +++ b/www/common/invitation.js @@ -18,16 +18,6 @@ var factory = function (Hash, Util, Crypt, Nacl, Scrypt/*, Cred, nThen */) { }; }; - Invite.derivePreviewHash = function (seeds) { - return '#/2/invite/view/' + - Nacl.util.encodeBase64(seeds.preview.slice(0, 18)).replace('/', '-') - + '/'; - }; - - Invite.derivePreviewSecrets = function (seeds) { - return Hash.getSecrets('pad', Invite.derivePreviewHash(seeds)); - }; - Invite.deriveSalt = function (password, instance_salt) { return (password || '') + (instance_salt || ''); }; @@ -44,44 +34,6 @@ var factory = function (Hash, Util, Crypt, Nacl, Scrypt/*, Cred, nThen */) { 'base64'); // format, could be 'base64' }; - Invite.getPreviewContent = function (seeds, cryptgetOpts, _cb) { - var cb = Util.once(Util.mkAsync(_cb)); - // XXX test data - cb(void 0, { - author: { - displayName: 'Bob', - curvePublic: 'pewpewpew' - }, - team: 'CryptPad', - message: 'Hello bob' - }); - /* - var secrets = Invite.derivePreviewSecrets(seeds); - secrets = secrets; - */ - var hash = Invite.derivePreviewHash(seeds); - Crypt.get(hash, function (err, val) { - if (err) { return void cb(err); } - if (!val) { return void cb('DELETED'); } - try { - cb(void 0, JSON.parse(val)); - } catch (e) { - console.error(e); - cb(e); - } - }, cryptgetOpts); -// cb("NOT_IMPLEMENTED"); // XXX cryptget - }; - - // XXX remember to pin invites... - Invite.setPreviewContent = function (seeds, cb) { - var hash = Invite.derivePreviewHash(seeds); - Crypt.put(hash, '', function (err) { // value? - cb(err); - }); - //cb = cb; - }; - return Invite; }; if (typeof(module) !== 'undefined' && module.exports) { diff --git a/www/common/outer/invitation.js b/www/common/outer/invitation.js index 4b0844c7f..69afc5978 100644 --- a/www/common/outer/invitation.js +++ b/www/common/outer/invitation.js @@ -18,11 +18,19 @@ var factory = function (Util, Cred, nThen, Nacl) { }; }; + Invite.generateSignPair = function () { + var ed = Nacl.sign.keyPair(); + return { + validateKey: encode64(ed.publicKey), + signKey: encode64(ed.secretKey), + }; + }; + var b64ToChannelKeys = function (b64) { var dispense = Cred.dispenser(decode64(b64)); return { channel: Util.uint8ArrayToHex(dispense(16)), - cryptKey: encode64(dispense(Nacl.secretbox.keyLength)), + cryptKey: dispense(Nacl.secretbox.keyLength), }; }; @@ -36,35 +44,6 @@ var factory = function (Util, Cred, nThen, Nacl) { // derived from the link seed alone. Invite.derivePreviewKeys = b64ToChannelKeys; - // what the invite link alone will allow you to see - Invite.createPreviewContent = function (data, keys, cb) { - cb = cb; - /* should include: - { - message: "", // personal message - author: "", // author public key - from: "", // author pretty name - } - */ - }; - - // the remaining data available with the invite link + password - Invite.createInviteContent = function (data, keys, cb) { - cb = cb; - /* should include: - { - teamData: { - // everything you need to join the team - - }, - ephemeral: { - curve: "", // for the roster - ed: "" // for deleting the preview content - } - } - */ - }; - Invite.createRosterEntry = function (roster, data, cb) { var toInvite = {}; toInvite[data.curvePublic] = data.content; diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 966c30a53..b7d390539 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -11,6 +11,7 @@ define([ '/common/common-messaging.js', '/common/common-feedback.js', '/common/outer/invitation.js', + '/common/cryptget.js', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', @@ -20,7 +21,7 @@ define([ '/bower_components/saferphore/index.js', '/bower_components/tweetnacl/nacl-fast.min.js', ], function (Util, Hash, Constants, Realtime, - ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, + ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt, Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) { var Team = {}; @@ -1273,17 +1274,25 @@ define([ 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; - team = team; - /* var roster = team.roster; + + var teamName; + try { + teamName = roster.getState().metadata.name; + } catch (err) { + return void cb("TEAM_NAME_ERR"); + } + + var message = data.message; var name = data.name; + + /* var password = data.password; - var msg = data.message; var hash = data.hash; */ @@ -1298,33 +1307,81 @@ define([ var ephemeralKeys = Invite.generateKeys(); nThen(function (w) { - w = w; - // XXX Invite.createPreviewContent - // XXX cryptput the preview content - /* PUT - { - message: data.message, - // XXX authorName - // XXX authorInfo { - profile, - etc, + + var putOpts = { + initialState: '{}', + network: ctx.store.network, + }; + + (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} + + // visible with only the invite link + var previewContent = { + teamName: teamName, + message: message, + author: Messaging.createData(ctx.store.proxy, false), + displayName: name, + curvePublic: ephemeralKeys.curvePublic, + }; + + 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("SET_PREVIEW_CONTENT"); } - } - /// XXX callback if error - */ - - // Invite.createInviteContent - // XXX cryptput the secret team credentials - /* PUT - { - ephemeralKeys.edPrivate, - ephemeralKeys.curvePrivate, - teamData: { - ... + }), 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} + + // 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: previewKeys.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("SET_PREVIEW_CONTENT"); } - } - /// XXX callback if error - */ + }), putOpts); + }()); }).nThen(function (w) { team.pin([inviteKeys.channel, previewKeys.channel], function (obj) { if (obj && obj.error) { console.error(obj.error); } @@ -1332,6 +1389,7 @@ define([ Invite.createRosterEntry(team.roster, { curvePublic: ephemeralKeys.curvePublic, content: { + curvePublic: ephemeralKeys.curvePublic, displayName: data.name, pending: true, inviteChannel: inviteKeys.channel, // XXX keep this channel pinned until the invite is accepted @@ -1352,27 +1410,64 @@ define([ }).nThen(function () { // call back empty if everything worked cb(); - /* - cb({ - error: 'NOT_IMPLEMENTED' - }); - */ }); }; - // XXX ansuz - var getLinkData = function (ctx, data, cId, cb) { - /* - var password = data.password; - var hash = data.hash; + var getPreviewContent = function (ctx, data, cId, cb) { + var seeds = data.seeds; + var previewKeys; + try { + previewKeys = Invite.derivePreviewKeys(seeds.preview); + } catch (err) { + return void cb("INVALID_SEEDS"); + } + Crypt.get({ // secrets + channel: previewKeys.channel, + type: 'pad', + version: 2, + keys: { + cryptKey: previewKeys.cryptKey, + }, + }, function (err, val) { + if (err) { return void cb(err); } + if (!val) { return void cb('DELETED'); } + + var json = Util.tryParse(val); + if (!json) { return void cb("parseError"); } + console.error("JSON", json); + cb(void 0, json); + }, { // cryptget opts + network: ctx.store.network, + initialState: '{}', + }); + }; + + var getInviteContent = function (ctx, data, cId, cb) { var bytes64 = data.bytes64; - */ - return void cb(); - /* - cb({ - error: 'NOT_IMPLEMENTED' + var previewKeys; + try { + previewKeys = Invite.deriveInviteKeys(bytes64); + } catch (err) { + return void cb("INVALID_SEEDS"); + } + Crypt.get({ // secrets + channel: previewKeys.channel, + type: 'pad', + version: 2, + keys: { + cryptKey: previewKeys.cryptKey, + }, + }, function (err, val) { + if (err) { return void cb(err); } + if (!val) { return void cb('DELETED'); } + + var json = Util.tryParse(val); + if (!json) { return void cb("parseError"); } + cb(void 0, json); + }, { // cryptget opts + network: ctx.store.network, + initialState: '{}', }); - */ }; @@ -1532,10 +1627,12 @@ define([ if (cmd === 'CREATE_INVITE_LINK') { return void createInviteLink(ctx, data, clientId, cb); } - if (cmd === 'GET_LINK_DATA') { - return void getLinkData(ctx, data, clientId, cb); + if (cmd === 'GET_INVITE_CONTENT') { + return void getInviteContent(ctx, data, clientId, cb); + } + if (cmd === 'GET_PREVIEW_CONTENT') { + return void getPreviewContent(ctx, data, clientId, cb); } - // XXX ansuz }; return team; diff --git a/www/teams/inner.js b/www/teams/inner.js index de1e466c1..4fea2b25a 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -1091,7 +1091,7 @@ define([ bytes64 = bytes; })); }).nThen(function (waitFor) { - APP.module.execCommand('GET_LINK_DATA', { + APP.module.execCommand('GET_INVITE_CONTENT', { bytes64: bytes64, hash: hash, password: pw, @@ -1104,10 +1104,11 @@ define([ }; nThen(function (waitFor) { - InviteInner.getPreviewContent(seeds, { - origin: privateData.origin + APP.module.execCommand("GET_PREVIEW_CONTENT", { + seeds: seeds, }, waitFor(function (err, json) { - if (err) { + if (err) { // XXX this is failing with "team is disabled" + // XXX APP.module is not ready yet? // err === DELETED: different message? $(errorBlock).text('ERROR'+err).show(); // XXX waitFor.abort(); @@ -1115,7 +1116,6 @@ define([ return; // XXX handle errors } - json = json; // XXX {message: "", author: "", ???} $div.empty(); $div.append(h('div.cp-teams-invite-from', [ 'From', // XXX