From c82c50c27486dc21f63f5f68914221dfabce902d Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 16 Dec 2019 20:57:19 -0500 Subject: [PATCH] WIP team invitations --- www/common/outer/invitation.js | 146 ++++++++++++++++----------------- www/common/outer/roster.js | 2 +- www/common/outer/team.js | 85 +++++++++++++++++-- 3 files changed, 151 insertions(+), 82 deletions(-) diff --git a/www/common/outer/invitation.js b/www/common/outer/invitation.js index baf44e677..f33f93bb1 100644 --- a/www/common/outer/invitation.js +++ b/www/common/outer/invitation.js @@ -1,8 +1,76 @@ (function () { -var factory = function (Util, Cred, nThen) { +var factory = function (Util, Cred, nThen, Nacl) { nThen = nThen; // XXX var Invite = {}; + var encode64 = Nacl.util.encodeBase64; + var decode64 = Nacl.util.decode64; + + // ed and curve keys can be random... + Invite.generateKeys = function () { + var ed = Nacl.sign.keyPair(); + var curve = Nacl.box.keyPair(); + return { + edPublic: encode64(ed.publicKey), + edPrivate: encode64(ed.secretKey), + curvePublic: encode64(curve.publicKey), + curvePrivate: encode64(curve.secretKey), + }; + }; + + var b64ToChannelKeys = function (b64) { + var dispense = Cred.dispenser(decode64(b64)); + return { + channel: Util.uint8ArrayToHex(dispense(16)), + cryptKey: encode64(dispense(Nacl.secretbox.keyLength)), + }; + }; + + // the secret invite values (cryptkey and channel) can be derived + // from the link seed and (optional) password + Invite.deriveInviteKeys = b64ToChannelKeys; + + // the preview values (cryptkey and channel) are less sensitive than the invite values + // as they cannot be leveraged to access any further content on their own + // unless the message contains secrets. + // 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; + roster.invite(toInvite, cb); + }; + /* INPUTS * password (for scrypt) @@ -13,20 +81,6 @@ var factory = function (Util, Cred, nThen) { */ - -/* DERIVATIONS - - * components corresponding to www/common/invitation.js - * preview_hash => components - * channel - * cryptKey - * b64_bytes - * curvePrivate => curvePublic - * edSeed => edPrivate => edPublic - -*/ - - /* IO / FUNCTIONALITY * creator @@ -46,77 +100,23 @@ var factory = function (Util, Cred, nThen) { */ - - var BYTES_REQUIRED = 256; - - Invite.deriveKeys = function (seed, passwd, cb) { - cb = cb; // XXX - // TODO validate has cb - // TODO onceAsync the cb - // TODO cb with err if !(seed && passwd) - - Cred.deriveFromPassphrase(seed, passwd, BYTES_REQUIRED, function (bytes) { - var dispense = Cred.dispenser(bytes); - dispense = dispense; // XXX - - // edPriv => edPub - // curvePriv => curvePub - // channel - // cryptKey - }); - }; - - Invite.createSeed = function () { - // XXX - // return a seed - }; - - Invite.create = function (cb) { - cb = cb; // XXX - // TODO validate has cb - // TODO onceAsync the cb - // TODO cb with err if !(seed && passwd) - - - - // required - // password - // validateKey - // creatorEdPublic - // for owner - // ephemeral - // signingKey - // for owner to write invitation - // derived - // edPriv - // edPublic - // for invitee ownership - // curvePriv - // curvePub - // for acceptance OR - // authenticated decline message via mailbox - // channel - // for owned deletion - // for team pinning - // cryptKey - // for protecting channel content - }; - return Invite; }; if (typeof(module) !== 'undefined' && module.exports) { module.exports = factory( require("../common-util"), require("../common-credential.js"), - require("nthen") + require("nthen"), + require("tweetnacl/nacl-fast") ); } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { define([ '/common/common-util.js', '/common/common-credential.js', '/bower_components/nthen/index.js', + '/bower_components/tweetnacl/nacl-fast.min.js', ], function (Util, Cred, nThen) { - return factory(Util, nThen); + return factory(Util, Cred, nThen, window.nacl); }); } }()); diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index 8b038569f..214d1ff9b 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -437,7 +437,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { // so you must already be in the members list if (!isMap(members[author])) { throw new Error("INSUFFICIENT_PERMISSIONS"); } // and your membership must indicate that you are 'pending' - if (!members[author].pending) { throw new Errror("ALREADY_PRESENT"); } + if (!members[author].pending) { throw new Error("ALREADY_PRESENT"); } // args should be a string if (typeof(args) !== 'string') { throw new Error("INVALID_ARGS"); } diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 8217f030e..22daac68c 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -1261,9 +1261,14 @@ define([ }; // XXX ansuz - var createInviteLink = function (ctx, data, cId, cb) { - Invite = Invite; + var createInviteLink = function (ctx, data, cId, _cb) { + var cb = Util.mkAsync(Util.once(_cb)); + var team = ctx.teams[data.teamId]; + + var seeds = data.seeds; // {scrypt, preview} + var bytes64 = data.bytes64; + team = team; /* var roster = team.roster; @@ -1271,15 +1276,78 @@ define([ var password = data.password; var msg = data.message; var hash = data.hash; - var bytes64 = data.bytes64; */ - return void cb(); - /* - cb({ - error: 'NOT_IMPLEMENTED' + + // 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) { + w = w; + // XXX Invite.createPreviewContent + // XXX cryptput the preview content + /* PUT + { + message: data.message, + // XXX authorName + // XXX authorInfo { + profile, + etc, + } + } + /// XXX callback if error + */ + + // Invite.createInviteContent + // XXX cryptput the secret team credentials + /* PUT + { + ephemeralKeys.edPrivate, + ephemeralKeys.curvePrivate, + teamData: { + ... + } + } + /// XXX callback if error + */ + }).nThen(function (w) { + Invite.createRosterEntry(team.roster, { + curvePublic: ephemeralKeys.curvePublic, + content: { + displayName: data.name, + pending: true, + inviteChannel: inviteKeys.channel, // XXX keep this channel pinned until the invite is accepted + previewChannel: previewKeys.channel, // XXX keep this channel pinned until the invite is accepted + + // XXX encrypt the following data for your own curvePublic + // XXX and implement UI for interacting with it + // remind yourself of the password used + // bypass scrypt with bytes64 to revover other keys + // { password, bytes64, hash} + } + }, w(function (err) { + if (err) { + w.abort(); + cb(err); + } + })); + }).nThen(function () { + // call back empty if everything worked + cb(); + /* + cb({ + error: 'NOT_IMPLEMENTED' + }); + */ }); - */ }; + // XXX ansuz var getLinkData = function (ctx, data, cId, cb) { /* @@ -1455,6 +1523,7 @@ define([ if (cmd === 'GET_LINK_DATA') { return void getLinkData(ctx, data, clientId, cb); } + // XXX ansuz }; return team;