From 6425d492f1f349a76ea6a87b5db72a3779de977c Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 12:04:31 +0200 Subject: [PATCH 01/15] move credential.js from customize.dist/ to www/common/ --- customize.dist/login.js | 2 +- .../common/common-credential.js | 24 ++++++++++++++----- www/common/cryptpad-common.js | 2 +- www/register/main.js | 2 +- www/settings/inner.js | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) rename customize.dist/credential.js => www/common/common-credential.js (80%) diff --git a/customize.dist/login.js b/customize.dist/login.js index 21668140f..0bf36b7ce 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -4,7 +4,7 @@ define([ '/bower_components/chainpad-crypto/crypto.js', '/common/common-util.js', '/common/outer/network-config.js', - '/customize/credential.js', + '/common/common-credential.js', '/bower_components/chainpad/chainpad.dist.js', '/common/common-realtime.js', '/common/common-constants.js', diff --git a/customize.dist/credential.js b/www/common/common-credential.js similarity index 80% rename from customize.dist/credential.js rename to www/common/common-credential.js index cdbd835c5..ffbc482d6 100644 --- a/customize.dist/credential.js +++ b/www/common/common-credential.js @@ -1,9 +1,6 @@ -define([ - '/customize/application_config.js', - '/bower_components/scrypt-async/scrypt-async.min.js', -], function (AppConfig) { +(function () { +var factory = function (AppConfig, Scrypt) { var Cred = {}; - var Scrypt = window.scrypt; Cred.MINIMUM_PASSWORD_LENGTH = typeof(AppConfig.minimumPasswordLength) === 'number'? AppConfig.minimumPasswordLength: 8; @@ -86,4 +83,19 @@ define([ }; return Cred; -}); +}; + + if (typeof(module) !== 'undefined' && module.exports) { + module.exports = factory( + {}, //require("../../customize.dist/application_config.js"), + require("../bower_components/scrypt-async/scrypt-async.min.js") + ); + } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { + define([ + '/customize/application_config.js', + '/bower_components/scrypt-async/scrypt-async.min.js', + ], function (AppConfig) { + return factory(AppConfig, window.scrypt); + }); + } +}()); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 0516bf057..3be8ed032 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1017,7 +1017,7 @@ define([ var Cred, Block, Login; Nthen(function (waitFor) { require([ - '/customize/credential.js', + '/common/common-credential.js', '/common/outer/login-block.js', '/customize/login.js' ], waitFor(function (_Cred, _Block, _Login) { diff --git a/www/register/main.js b/www/register/main.js index 63324f810..d052f88b0 100644 --- a/www/register/main.js +++ b/www/register/main.js @@ -3,7 +3,7 @@ define([ '/customize/login.js', '/common/cryptpad-common.js', '/common/test.js', - '/customize/credential.js', // preloaded for login.js + '/common/common-credential.js', '/common/common-interface.js', '/common/common-util.js', '/common/common-realtime.js', diff --git a/www/settings/inner.js b/www/settings/inner.js index 6fe07e3fe..416f99bb8 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -9,7 +9,7 @@ define([ '/common/common-hash.js', '/customize/messages.js', '/common/hyperscript.js', - '/customize/credential.js', + '/common/common-credential.js', '/customize/application_config.js', '/api/config', '/common/make-backup.js', From 61e66e9add2dfd59fd323e85d26c2a687794e3bc Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 12:05:08 +0200 Subject: [PATCH 02/15] WIP invitation link generation --- www/common/outer/invitation.js | 95 ++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 www/common/outer/invitation.js diff --git a/www/common/outer/invitation.js b/www/common/outer/invitation.js new file mode 100644 index 000000000..c6199bf4c --- /dev/null +++ b/www/common/outer/invitation.js @@ -0,0 +1,95 @@ +(function () { +var factory = function (Util, Cred, nThen) { + nThen = nThen; // XXX + var Invite = {}; + +/* + TODO key derivation + + scrypt(seed, passwd) => { + curve: { + private, + public, + }, + ed: { + private, + public, + } + cryptKey, + channel + } +*/ + + 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") + ); + } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { + define([ + '/common/common-util.js', + '/common/common-credential.js', + '/bower_components/nthen/index.js', + ], function (Util, Cred, nThen) { + return factory(Util, nThen); + }); + } +}()); From 777d9eff5f9af74e3627ea0833c58adc98773ac4 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 12:05:56 +0200 Subject: [PATCH 03/15] pass arguments to 'throttle' --- www/common/common-util.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/www/common/common-util.js b/www/common/common-util.js index b4255996f..ba9fa2f03 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -5,6 +5,10 @@ window.atob = window.atob || function (str) { return Buffer.from(str, 'base64').toString('binary'); }; // jshint ignore:line window.btoa = window.btoa || function (str) { return new Buffer(str, 'binary').toString('base64'); }; // jshint ignore:line + Util.slice = function (A, start, end) { + return Array.prototype.slice.call(A, start, end); + }; + Util.bake = function (f, args) { if (typeof(args) === 'undefined') { args = []; } if (!Array.isArray(args)) { args = [args]; } @@ -265,7 +269,7 @@ var to; var g = function () { window.clearTimeout(to); - to = window.setTimeout(f, ms); + to = window.setTimeout(Util.bake(f, Util.slice(arguments)), ms); }; return g; }; From 717a7d550c1a78a1b2a6b587ae466d82e7cdbdc4 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 12:40:37 +0200 Subject: [PATCH 04/15] increase size of pdfs rendered via media-tags in code preview pane --- www/code/app-code.less | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/code/app-code.less b/www/code/app-code.less index f6c3aa311..f07ef534d 100644 --- a/www/code/app-code.less +++ b/www/code/app-code.less @@ -92,8 +92,10 @@ * { max-width:100%; } - iframe[type="application/pdf"] { - max-height:50vh; + iframe[src$=".pdf"] { + width: 100%; + height: 80vh; + max-height: 90vh; } } .markdown_main(); From 252b2729a6c9a3f568da5e485dec123d48eeb95a Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 13:20:53 +0200 Subject: [PATCH 05/15] supply plan information with support ticket submissions --- www/support/inner.js | 6 +++++- www/support/ui.js | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/www/support/inner.js b/www/support/inner.js index ded97b272..701d01817 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -236,6 +236,10 @@ define([ APP.$rightside = $('
', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container); var sFrameChan = common.getSframeChannel(); sFrameChan.onReady(waitFor()); + common.getPinUsage(null, waitFor(function (err, data) { + if (err) { return void console.error(err); } + APP.pinUsage = data; + })); }).nThen(function (/*waitFor*/) { createToolbar(); metadataMgr = common.getMetadataMgr(); @@ -244,7 +248,7 @@ define([ APP.origin = privateData.origin; APP.readOnly = privateData.readOnly; - APP.support = Support.create(common, false); + APP.support = Support.create(common, false, APP.pinUsage); // Content var $rightside = APP.$rightside; diff --git a/www/support/ui.js b/www/support/ui.js index 6b0ffdcbe..ca707c413 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -25,6 +25,14 @@ define([ edPublic: privateData.edPublic, notifications: user.notifications, }; + + if (typeof(ctx.pinUsage) === 'object') { + // pass pin.usage, pin.limit, and pin.plan if supplied + Object.keys(ctx.pinUsage).forEach(function (k) { + data.sender[k] = ctx.pinUsage[k]; + }); + } + data.id = id; data.time = +new Date(); @@ -202,11 +210,12 @@ define([ ]); }; - var create = function (common, isAdmin) { + var create = function (common, isAdmin, pinUsage) { var ui = {}; var ctx = { common: common, isAdmin: isAdmin + pinUsage: pinUsage || false, }; ui.sendForm = function (id, form, dest) { From 069776e80473c0d155820a6f74b3148423e4ff87 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 13:30:17 +0200 Subject: [PATCH 06/15] use fast implementations of tweetnacl serverside --- lib/client/index.js | 2 +- rpc.js | 2 +- scripts/tests/test-rpc.js | 2 +- storage/tasks.js | 2 +- www/common/common-hash.js | 2 +- www/common/rpc.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/client/index.js b/lib/client/index.js index 8faf8f4a2..3e4bc1225 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -4,7 +4,7 @@ var nThen = require("nthen"); var Util = require("../../www/common/common-util"); -var Nacl = require("tweetnacl"); +var Nacl = require("tweetnacl/nacl-fast"); var Client = module.exports; diff --git a/rpc.js b/rpc.js index 5a97cd572..d4bd41c9c 100644 --- a/rpc.js +++ b/rpc.js @@ -1,7 +1,7 @@ /*@flow*/ /*jshint esversion: 6 */ /* Use Nacl for checking signatures of messages */ -var Nacl = require("tweetnacl"); +var Nacl = require("tweetnacl/nacl-fast"); /* globals Buffer*/ /* globals process */ diff --git a/scripts/tests/test-rpc.js b/scripts/tests/test-rpc.js index 178a07c0d..ddfe06655 100644 --- a/scripts/tests/test-rpc.js +++ b/scripts/tests/test-rpc.js @@ -3,7 +3,7 @@ var Client = require("../../lib/client/"); var Crypto = require("../../www/bower_components/chainpad-crypto"); var Mailbox = Crypto.Mailbox; -var Nacl = require("tweetnacl"); +var Nacl = require("tweetnacl/nacl-fast"); var nThen = require("nthen"); var Pinpad = require("../../www/common/pinpad"); var Rpc = require("../../www/common/rpc"); diff --git a/storage/tasks.js b/storage/tasks.js index 4db1c7642..2209b3d59 100644 --- a/storage/tasks.js +++ b/storage/tasks.js @@ -1,7 +1,7 @@ var Fs = require("fs"); var Fse = require("fs-extra"); var Path = require("path"); -var nacl = require("tweetnacl"); +var nacl = require("tweetnacl/nacl-fast"); var nThen = require("nthen"); var Tasks = module.exports; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index e59604136..fa9feb7e1 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -530,7 +530,7 @@ Version 1 }; if (typeof(module) !== 'undefined' && module.exports) { - module.exports = factory(require("./common-util"), require("chainpad-crypto"), require("tweetnacl")); + module.exports = factory(require("./common-util"), require("chainpad-crypto"), require("tweetnacl/nacl-fast")); } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { define([ '/common/common-util.js', diff --git a/www/common/rpc.js b/www/common/rpc.js index cb7790602..53d969d3b 100644 --- a/www/common/rpc.js +++ b/www/common/rpc.js @@ -404,7 +404,7 @@ var factory = function (Util, Nacl) { }; if (typeof(module) !== 'undefined' && module.exports) { - module.exports = factory(require("./common-util"), require("tweetnacl")); + module.exports = factory(require("./common-util"), require("tweetnacl/nacl-fast")); } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { define([ '/common/common-util.js', From c968d1cdae74b8bb5fc9427edecc3292647bf7e6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 13:31:42 +0200 Subject: [PATCH 07/15] use nacl-fast in history keeper --- historyKeeper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/historyKeeper.js b/historyKeeper.js index d90b8c113..53ce46807 100644 --- a/historyKeeper.js +++ b/historyKeeper.js @@ -3,7 +3,7 @@ ;(function () { 'use strict'; const nThen = require('nthen'); -const Nacl = require('tweetnacl'); +const Nacl = require('tweetnacl/nacl-fast'); const Crypto = require('crypto'); const Once = require("./lib/once"); const Meta = require("./lib/metadata"); From 648980ad50d07e5c728c604465214aed2339adeb Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 13:32:57 +0200 Subject: [PATCH 08/15] a few more slow nacl imports that I missed --- scripts/check-account-deletion.js | 2 +- scripts/generate-admin-keys.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/check-account-deletion.js b/scripts/check-account-deletion.js index 020e3c254..cf271a1da 100644 --- a/scripts/check-account-deletion.js +++ b/scripts/check-account-deletion.js @@ -2,7 +2,7 @@ const Fs = require('fs'); const nThen = require('nthen'); const Pinned = require('./pinned'); -const Nacl = require('tweetnacl'); +const Nacl = require('tweetnacl/nacl-fast'); const Path = require('path'); const Pins = require('../lib/pins'); const Config = require('../lib/load-config'); diff --git a/scripts/generate-admin-keys.js b/scripts/generate-admin-keys.js index 26d26537e..f99c756d7 100644 --- a/scripts/generate-admin-keys.js +++ b/scripts/generate-admin-keys.js @@ -1,6 +1,6 @@ /* jshint esversion: 6, node: true */ -const Nacl = require('tweetnacl'); +const Nacl = require('tweetnacl/nacl-fast'); const keyPair = Nacl.box.keyPair(); console.log("You've just generated a new key pair for your support mailbox."); From 69e61ec2f2a290ed9b20ea1f52f1f7fde789a185 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Sep 2019 14:28:59 +0200 Subject: [PATCH 09/15] ignore metadata commands which add invalid owners --- lib/metadata.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/metadata.js b/lib/metadata.js index 6231ea224..de40043af 100644 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -15,6 +15,10 @@ var deduplicate = require("./deduplicate"); var commands = {}; +var isValidOwner = function (owner) { + return typeof(owner) === 'string' && owner.length === 44; +}; + // ["ADD_OWNERS", ["7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I="], 1561623438989] commands.ADD_OWNERS = function (meta, args) { // bail out if args isn't an array @@ -30,6 +34,7 @@ commands.ADD_OWNERS = function (meta, args) { var changed = false; args.forEach(function (owner) { + if (!isValidOwner(owner)) { return; } if (meta.owners.indexOf(owner) >= 0) { return; } meta.owners.push(owner); changed = true; @@ -90,6 +95,7 @@ commands.ADD_PENDING_OWNERS = function (meta, args) { } // or fill it args.forEach(function (owner) { + if (!isValidOwner(owner)) { return; } if (meta.pending_owners.indexOf(owner) >= 0) { return; } meta.pending_owners.push(owner); changed = true; @@ -134,7 +140,7 @@ commands.RESET_OWNERS = function (meta, args) { } // overwrite the existing owners with the new one - meta.owners = deduplicate(args); + meta.owners = deduplicate(args.filter(isValidOwner)); return true; }; From fd31fd3096a21ed8b24202ed125dfed9f341a279 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Sep 2019 11:50:01 +0200 Subject: [PATCH 10/15] roster changes: validate roles when describing. interpret assignment of null as deletion. prevent a typeError when describing somebody that doesn't exist --- www/common/outer/roster.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index a8a3335e8..0f446706e 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -241,6 +241,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { var current = Util.clone(members[curve]); + if (typeof(data.role) === 'string') { // they're trying to change the role... + // throw if they're trying to upgrade to something greater + if (!canAddRole(author, data.role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); } + } + // DESCRIBE commands must initialize a displayName if it isn't already present if (typeof(current.displayName) !== 'string' && typeof(data.displayName) !== 'string') { throw new Error('DISPLAYNAME_REQUIRED'); } @@ -256,7 +261,9 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { var data = args[curve]; Object.keys(data).forEach(function (key) { - if (current[key] === data[key]) { return; } + // when null is passed as new data and it wasn't considered an invalid change + // remove it from the map. This is how you delete things properly + if (typeof(current[key]) !== 'undefined' && data[key] === null) { return void delete current[key]; } current[key] = data[key]; }); @@ -608,14 +615,19 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); } var data = Util.clone(_data); - Object.keys(data).forEach(function (curve) { + if (Object.keys(data).some(function (curve) { var member = data[curve]; if (!isMap(member)) { delete data[curve]; } + // validate that you're trying to describe a user that is present + if (!isMap(state.members[curve])) { return true; } // don't send fields that won't result in a change Object.keys(member).forEach(function (k) { if (member[k] === state.members[curve][k]) { delete member[k]; } }); - }); + })) { + // returning true in the above loop indicates that something was invalid + return void cb("INVALID_ARGUMENTS"); + } send(['DESCRIBE', data], cb); }; From 8e07b035813d3c54f7d4511b415e69f1294b2eb2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Sep 2019 14:51:34 +0200 Subject: [PATCH 11/15] fix parse error --- www/support/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/support/ui.js b/www/support/ui.js index ca707c413..05efc2935 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -214,7 +214,7 @@ define([ var ui = {}; var ctx = { common: common, - isAdmin: isAdmin + isAdmin: isAdmin, pinUsage: pinUsage || false, }; From 59a361449dfaa01d2f8fbf48f183549e375dd1b0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Sep 2019 14:56:16 +0200 Subject: [PATCH 12/15] don't allow deletion of displayName or notifications via describe command --- www/common/outer/roster.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index 0f446706e..ca7c48554 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -245,12 +245,22 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { // throw if they're trying to upgrade to something greater if (!canAddRole(author, data.role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); } } - // DESCRIBE commands must initialize a displayName if it isn't already present - if (typeof(current.displayName) !== 'string' && typeof(data.displayName) !== 'string') { throw new Error('DISPLAYNAME_REQUIRED'); } + if (typeof(current.displayName) !== 'string' && typeof(data.displayName) !== 'string') { + throw new Error('DISPLAYNAME_REQUIRED'); + } + + if (['undefined', 'string'].indexOf(typeof(data.displayName)) === -1) { + throw new Error("INVALID_DISPLAYNAME"); + } // DESCRIBE commands must initialize a mailbox channel if it isn't already present - if (typeof(current.notifications) !== 'string' && typeof(data.displayName) !== 'string') { throw new Error('NOTIFICATIONS_REQUIRED'); } + if (typeof(current.notifications) !== 'string' && typeof(data.notifications) !== 'string') { + throw new Error('NOTIFICATIONS_REQUIRED'); + } + if (['undefined', 'string'].indexOf(typeof(data.notifications)) === -1) { + throw new Error("INVALID_NOTIFICATIONS"); + } }); var changed = false; From 8761e2071aaa69c8d80cd7fe1ce2ea00794f3859 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Sep 2019 14:57:15 +0200 Subject: [PATCH 13/15] test many more features of roster.js via rpc tests --- scripts/tests/test-rpc.js | 134 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 4 deletions(-) diff --git a/scripts/tests/test-rpc.js b/scripts/tests/test-rpc.js index ddfe06655..368c5314c 100644 --- a/scripts/tests/test-rpc.js +++ b/scripts/tests/test-rpc.js @@ -388,6 +388,108 @@ nThen(function (w) { roster.add(data, w(function (err) { if (err) { return void console.error(err); } })); +}).nThen(function (w) { + var data = {}; + data[alice.curveKeys.curvePublic] = { + role: "OWNER", + }; + + alice.roster.describe(data, w(function (err) { + if (!err) { + console.log("Members should not be able to add themselves as owners!"); + process.exit(1); + } + console.log("Alice failed to promote herself to owner, as expected"); + })); +}).nThen(function (w) { + var data = {}; + data[alice.curveKeys.curvePublic] = { + role: "ADMIN", + }; + + alice.roster.describe(data, w(function (err) { + if (!err) { + console.log("Members should not be able to add themselves as admins!"); + process.exit(1); + } + console.log("Alice failed to promote herself to admin, as expected"); + })); +}).nThen(function (w) { + var data = {}; + data[alice.curveKeys.curvePublic] = { + test: true, + }; + alice.roster.describe(data, w(function (err) { + if (err) { + console.log("Unexpected error while describing an arbitrary attribute"); + process.exit(1); + } + })); +}).nThen(function (w) { + var state = alice.roster.getState(); + + var alice_state = state.members[alice.curveKeys.curvePublic]; + //console.log(alice_state); + + if (typeof(alice_state.test) !== 'boolean') { + console.error("Arbitrary boolean attribute was not set"); + process.exit(1); + } + + var data = {}; + data[alice.curveKeys.curvePublic] = { + test: null, + }; + alice.roster.describe(data, w(function (err) { + if (err) { + console.error(err); + console.error("Expected removal of arbitrary attribute to be successfully applied"); + console.log(alice.roster.getState()); + process.exit(1); + } + })); +}).nThen(function (w) { + var data = {}; + data[alice.curveKeys.curvePublic] = { + notifications: null, + }; + alice.roster.describe(data, w(function (err) { + if (!err) { + console.error("Expected deletion of notifications channel to fail"); + process.exit(1); + } + if (err !== 'INVALID_NOTIFICATIONS') { + console.log("UNEXPECTED ERROR 1231241245"); + console.error(err); + process.exit(1); + } + console.log("Deletion of notifications channel failed as expected"); + })); +}).nThen(function (w) { + var data = {}; + data[alice.curveKeys.curvePublic] = { + displayName: null, + }; + alice.roster.describe(data, w(function (err) { + if (!err) { + console.error("Expected deletion of displayName to fail"); + process.exit(1); + } + if (err !== 'INVALID_DISPLAYNAME') { + console.log("UNEXPECTED ERROR 12352623465"); + console.error(err); + process.exit(1); + } + console.log("Deletion of displayName failed as expected"); + })); +}).nThen(function (w) { + alice.roster.checkpoint(w(function (err) { + if (!err) { + console.error("Members should not be able to send checkpoints!"); + process.exit(0); + } + console.error("checkpoint by member failed as expected"); + })); }).nThen(function (w) { console.log("STATE =", JSON.stringify(oscar.roster.getState(), null, 2)); @@ -399,10 +501,6 @@ nThen(function (w) { if (err) { return void console.log(err); } console.log("STATE =", JSON.stringify(oscar.roster.getState(), null, 2)); })); -}).nThen(function () { - - - }).nThen(function (w) { // oscar sends a checkpoint oscar.roster.checkpoint(w(function (err) { @@ -427,6 +525,20 @@ nThen(function (w) { } console.log("Promoted Alice to ADMIN"); })); +}).nThen(function (w) { + alice.roster.checkpoint(w(function (err) { + if (!err) { return; } + console.error("Checkpoint by an admin failed unexpectedly"); + console.error(err); + process.exit(1); + })); +}).nThen(function (w) { + oscar.roster.checkpoint(w(function (err) { + if (!err) { return; } + console.error("Checkpoint by an owner failed unexpectedly"); + console.error(err); + process.exit(1); + })); }).nThen(function (w) { // bob finally connects, this time with the lastKnownHash provided by oscar var rosterKeys = Crypto.Team.deriveMemberKeys(sharedConfig.rosterSeed, bob.curveKeys); @@ -455,6 +567,20 @@ nThen(function (w) { roster.stop(); }); })); +}).nThen(function (w) { + var bogus = {}; + var curveKeys = makeCurveKeys(); + bogus[curveKeys.curvePublic] = { + displayName: "chewbacca", + notifications: Hash.createChannelId(), + }; + bob.roster.add(bogus, w(function (err) { + if (!err) { + console.error("Expected 'add' by member to fail"); + process.exit(1); + } + console.log("'add' by member failed as expected"); + })); }).nThen(function (w) { bob.roster.remove([ oscar.curveKeys.curvePublic, From 2a809cf120ac0382814f1afc22e6425323af9124 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Sep 2019 15:35:27 +0200 Subject: [PATCH 14/15] roster changes: * validate that a user can add a role before adding them * support deletion of non-required metadata attributes via null --- scripts/tests/test-rpc.js | 125 ++++++++++++++++++++++++++++++++++++- www/common/outer/roster.js | 25 ++++++-- 2 files changed, 145 insertions(+), 5 deletions(-) diff --git a/scripts/tests/test-rpc.js b/scripts/tests/test-rpc.js index 368c5314c..70ea13053 100644 --- a/scripts/tests/test-rpc.js +++ b/scripts/tests/test-rpc.js @@ -525,6 +525,26 @@ nThen(function (w) { } console.log("Promoted Alice to ADMIN"); })); +}).nThen(function (w) { + var data = {}; + data[bob.curveKeys.curvePublic] = { + notifications: Hash.createChannelId(), + displayName: "BORB", + }; + + alice.roster.add(data, w(function (err) { + if (err === 'ALREADY_PRESENT' || err === 'NO_CHANGE') { + return void console.log("Duplicate add command failed as expected"); + } + if (err) { + console.error("Unexpected error", err); + process.exit(1); + } + if (!err) { + console.log("Duplicate add succeeded unexpectedly"); + process.exit(1); + } + })); }).nThen(function (w) { alice.roster.checkpoint(w(function (err) { if (!err) { return; } @@ -539,6 +559,16 @@ nThen(function (w) { console.error(err); process.exit(1); })); +}).nThen(function (w) { + alice.roster.remove([ + oscar.curveKeys.curvePublic, + ], w(function (err) { + if (!err) { + console.error("Removal of owner by admin succeeded unexpectedly"); + process.exit(1); + } + console.log("Removal of owner by admin failed as expected"); + })); }).nThen(function (w) { // bob finally connects, this time with the lastKnownHash provided by oscar var rosterKeys = Crypto.Team.deriveMemberKeys(sharedConfig.rosterSeed, bob.curveKeys); @@ -581,16 +611,109 @@ nThen(function (w) { } console.log("'add' by member failed as expected"); })); +}).nThen(function (w) { + bob.roster.remove([ + alice.curveKeys.curvePublic, + ], w(function (err) { + if (!err) { + console.error("Removal of admin by member succeeded unexpectedly"); + process.exit(1); + } + console.log("Removal of admin by member failed as expected"); + })); }).nThen(function (w) { bob.roster.remove([ oscar.curveKeys.curvePublic, - alice.curveKeys.curvePublic + //alice.curveKeys.curvePublic ], w(function (err) { if (err) { return void console.log("command failed as expected"); } w.abort(); console.log("Expected command to fail!"); process.exit(1); })); +}).nThen(function (w) { + var data = {}; + data[bob.curveKeys.curvePublic] = { + displayName: 'BORB', + }; + + bob.roster.describe(data, w(function (err) { + if (err) { + console.error("self-description by a member failed unexpectedly"); + process.exit(1); + } + })); +}).nThen(function (w) { + var data = {}; + data[oscar.curveKeys.curvePublic] = { + displayName: 'NULL', + }; + + bob.roster.describe(data, w(function (err) { + if (!err) { + console.error("description of an owner by a member succeeded unexpectedly"); + process.exit(1); + } + console.log("description of an owner by a member failed as expected"); + })); +}).nThen(function (w) { + var data = {}; + data[alice.curveKeys.curvePublic] = { + displayName: 'NULL', + }; + + bob.roster.describe(data, w(function (err) { + if (!err) { + console.error("description of an admin by a member succeeded unexpectedly"); + process.exit(1); + } + console.log("description of an admin by a member failed as expected"); + })); +}).nThen(function (w) { + var data = {}; + data[bob.curveKeys.curvePublic] = { + displayName: "NULL", + }; + + alice.roster.describe(data, w(function (err) { + if (err) { + console.error("Description of member by admin failed unexpectedly"); + console.error(err); + process.exit(1); + } + })); +}).nThen(function (w) { + alice.roster.metadata({ + name: "BEST TEAM", + topic: "Champions de monde!", + cheese: "Camembert", + }, w(function (err) { + if (err) { + console.error("Metadata change by admin failed unexpectedly"); + console.error(err); + process.exit(1); + } + })); +}).nThen(function (w) { + bob.roster.metadata({ + name: "WORST TEAM", + topic: "not a good team", + }, w(function (err) { + if (!err) { + console.error("Metadata change by member should have failed"); + process.exit(1); + } + })); +}).nThen(function (w) { + oscar.roster.metadata({ + cheese: null, // delete a field that you don't want presenet + }, w(function (err) { + if (err) { + console.error(err); + process.exit(1); + } + + })); }).nThen(function (w) { alice.roster.remove([bob.curveKeys.curvePublic], w(function (err) { if (err) { diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index ca7c48554..f2de26da7 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -171,6 +171,10 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { // if no role was provided, assume MEMBER if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; } + if (!canAddRole(author, data.role, members)) { + throw new Error("INSUFFICIENT_PERMISSIONS"); + } + if (typeof(data.displayName) !== 'string') { throw new Error("DISPLAYNAME_REQUIRED"); } if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); } }); @@ -178,12 +182,9 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { var changed = false; // then iterate again and apply it Object.keys(args).forEach(function (curve) { - var data = args[curve]; - if (!canAddRole(author, data.role, members)) { return; } - // this will result in a change changed = true; - members[curve] = data; + members[curve] = args[curve]; }); return changed; @@ -322,6 +323,12 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { return true; }; + var MANDATORY_METADATA_FIELDS = [ + 'avatar', + 'name', + 'topic', + ]; + // only admin/owner can change group metadata commands.METADATA = function (args, author, roster) { if (!isMap(args)) { throw new Error("INVALID_ARGS"); } @@ -330,6 +337,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { // validate inputs Object.keys(args).forEach(function (k) { + if (args[k] === null) { + if (MANDATORY_METADATA_FIELDS.indexOf(k) === -1) { return; } + throw new Error('CANNOT_REMOVE_MANDATORY_METADATA'); + } + // can't set metadata to anything other than strings // use empty string to unset a value if you must if (typeof(args[k]) !== 'string') { throw new Error("INVALID_ARGUMENTS"); } @@ -338,6 +350,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { var changed = false; // {topic, name, avatar} are all strings... Object.keys(args).forEach(function (k) { + if (typeof(roster.state.metadata[k]) !== 'undefined' && args[k] === null) { + changed = true; + delete roster.state.metadata[k]; + } + // ignore things that won't cause changes if (args[k] === roster.state.metadata[k]) { return; } From 16e8c962101b8e68b0730a804082ca8ca3ec92af Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Sep 2019 15:41:11 +0200 Subject: [PATCH 15/15] allow for selection of user data in admin side of support tickets --- www/support/ui.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/support/ui.js b/www/support/ui.js index 05efc2935..7cbf1e225 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -177,6 +177,8 @@ define([ ]); $(userData).click(function () { $(userData).find('pre').toggle(); + }).find('pre').click(function (ev) { + ev.stopPropagation(); }); var name = Util.fixHTML(content.sender.name) || Messages.anonymous;