From d4b38ba71d149a1931f13a7fbd9c900d02c601a6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 24 Jun 2020 13:49:49 -0400 Subject: [PATCH 1/5] start testing a new format for 'Public Signing Keys' --- www/assert/main.js | 8 ++++++++ www/common/common-hash.js | 19 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/www/assert/main.js b/www/assert/main.js index c29b3bfa3..bfbee3b3a 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -516,6 +516,14 @@ define([ }, "tracker should not timeout"); }()); + (function () { + assert(function (cb) { + cb(false); + + + }, "new format of users' public signing key strings should parse"); // XXX + }()); + Drive.test(assert); assert(function (cb) { diff --git a/www/common/common-hash.js b/www/common/common-hash.js index b60ab3306..232d273b4 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -92,8 +92,23 @@ var factory = function (Util, Crypto, Nacl) { } }; - Hash.getUserHrefFromKeys = function (origin, username, pubkey) { - return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-'); +/* + +0. usernames may contain spaces or many other wacky characters, so enclose the whole thing in square braces so we know its boundaries. If the formatted string does not include these we know it is either a _v1 public key string_ or _an incomplete string_. Start parsing by removing them. +1. public keys should have a fixed length, so slice them off of the end of the string. +2. domains cannot include `@`, so find the last occurence of it in the signing key and slice everything thereafter. +3. the username is everything before the `@`. + +*/ + Hash.getUserHrefFromKeys = function (origin, username, pubkey) { // XXX + return '[' + + username + + '@' + + origin.replace(/https*:\/\//, '') + + '/' + + pubkey.replace(/\//g, '-') + + ']'; + // return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-'); }; var fixDuplicateSlashes = function (s) { From ebcc9a069b54090f1f1879fc9aa741258d1a52e8 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 25 Jun 2020 13:14:26 -0400 Subject: [PATCH 2/5] add server-side support for the new format of public signing keys --- lib/commands/quota.js | 18 +++++++--- lib/keys.js | 81 +++++++++++++++++++++++++++++++++++++++++++ server.js | 9 +++-- 3 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 lib/keys.js diff --git a/lib/commands/quota.js b/lib/commands/quota.js index 5c39a8d93..97213e02b 100644 --- a/lib/commands/quota.js +++ b/lib/commands/quota.js @@ -3,6 +3,7 @@ const Quota = module.exports; const Util = require("../common-util"); +const Keys = require("../keys"); const Package = require('../../package.json'); const Https = require("https"); @@ -19,11 +20,18 @@ Quota.applyCustomLimits = function (Env) { var customLimits = (function (custom) { var limits = {}; Object.keys(custom).forEach(function (k) { - k.replace(/\/([^\/]+)$/, function (all, safeKey) { - var id = Util.unescapeKeyCharacters(safeKey || ''); - limits[id] = custom[k]; - return ''; - }); + var user; + try { + user = Keys.parseUser(k); + } catch (err) { + return void Env.Log.error("PARSE_CUSTOM_LIMIT_BLOCK", { + user: k, + error: err.message, + }); + } + + var unsafeKey = user.pubkey; + limits[unsafeKey] = custom[k]; }); return limits; }(Env.customLimits || {})); diff --git a/lib/keys.js b/lib/keys.js new file mode 100644 index 000000000..c4be44c85 --- /dev/null +++ b/lib/keys.js @@ -0,0 +1,81 @@ +(function () { +var factory = function () { + var Keys = {}; + +/* Parse the new format of "Signing Public Keys". + If anything about the input is found to be invalid, return; + this will fall back to the old parsing method + + +*/ + var parseNewUser = function (userString) { + if (!/^\[.*?@.*\]$/.test(userString)) { return; } + var temp = userString.slice(1, -1); + var domain, username, pubkey; + + temp = temp + .replace(/\/([a-zA-Z0-9+-]{43}=)$/, function (all, k) { + pubkey = k.replace(/-/g, '/'); + return ''; + }); + if (!pubkey) { return; } + + var index = temp.lastIndexOf('@'); + if (index < 1) { return; } + + domain = temp.slice(index + 1); + username = temp.slice(0, index); + + return { + domain: domain, + user: username, + pubkey: pubkey + }; + }; + + var isValidUser = function (parsed) { + if (!parsed) { return; } + if (!(parsed.domain && parsed.user && parsed.pubkey)) { return; } + return true; + }; + + Keys.parseUser = function (user) { + var parsed = parseNewUser(user); + if (isValidUser(parsed)) { return parsed; } + + var domain, username, pubkey; + user.replace(/^https*:\/\/([^\/]+)\/user\/#\/1\/([^\/]+)\/([a-zA-Z0-9+-]{43}=)$/, + function (a, d, u, k) { + domain = d; + username = u; + pubkey = k.replace(/-/g, '/'); + return ''; + }); + if (!domain) { throw new Error("Could not parse user id [" + user + "]"); } + return { + domain: domain, + user: username, + pubkey: pubkey + }; + }; + + Keys.serialize = function (origin, username, pubkey) { + return '[' + + username + + '@' + + origin.replace(/https*:\/\//, '') + + '/' + + pubkey.replace(/\//g, '-') + + ']'; + // return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-'); + }; + + return Keys; +}; + + if (typeof(module) !== 'undefined' && module.exports) { + module.exports = factory(); + } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { + define([], factory); + } +}()); diff --git a/server.js b/server.js index 8508eba83..b721afa86 100644 --- a/server.js +++ b/server.js @@ -9,6 +9,7 @@ var Path = require("path"); var nThen = require("nthen"); var Util = require("./lib/common-util"); var Default = require("./lib/defaults"); +var Keys = require("./lib/keys"); var config = require("./lib/load-config"); @@ -201,9 +202,11 @@ app.use(/^\/[^\/]*$/, Express.static('customize.dist')); var admins = []; try { admins = (config.adminKeys || []).map(function (k) { - k = k.replace(/\/+$/, ''); - var s = k.split('/'); - return s[s.length-1].replace(/-/g, '/'); + // return each admin's "unsafeKey" + // this might throw and invalidate all the other admin's keys + // but we want to get the admin's attention anyway. + // breaking everything is a good way to accomplish that. + return Keys.parseUser(k).pubkey; }); } catch (e) { console.error("Can't parse admin keys"); } From fd8044115171500facc2d64ba20c6d4cfa842b6b Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 25 Jun 2020 13:15:57 -0400 Subject: [PATCH 3/5] rename the 'getUserHrefFromKeys' function since it's no longer an 'href' --- www/common/common-hash.js | 2 +- www/settings/inner.js | 2 +- www/teams/inner.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 232d273b4..20e229fdf 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -100,7 +100,7 @@ var factory = function (Util, Crypto, Nacl) { 3. the username is everything before the `@`. */ - Hash.getUserHrefFromKeys = function (origin, username, pubkey) { // XXX + Hash.getPublicSigningKeyString = function (origin, username, pubkey) { return '[' + username + '@' + diff --git a/www/settings/inner.js b/www/settings/inner.js index 234402ab5..df24e71d2 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -168,7 +168,7 @@ define([ var publicKey = privateData.edPublic; if (publicKey) { var $key = $('
', { 'class': 'cp-sidebarlayout-element' }).appendTo($div); - var userHref = Hash.getUserHrefFromKeys(privateData.origin, accountName, publicKey); + var userHref = Hash.getPublicSigningKeyString(privateData.origin, accountName, publicKey); var $pubLabel = $('', { 'class': 'label' }) .text(Messages.settings_publicSigningKey); $key.append($pubLabel).append(UI.dialog.selectable(userHref)); diff --git a/www/teams/inner.js b/www/teams/inner.js index 30612f3c3..0f3db0829 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -910,7 +910,7 @@ define([ var name = team.name; if (publicKey) { var $key = $('
', {'class': 'cp-sidebarlayout-element'}).appendTo($div); - var userHref = Hash.getUserHrefFromKeys(privateData.origin, name, publicKey); + var userHref = Hash.getPublicSigningKeyString(privateData.origin, name, publicKey); var $pubLabel = $('', {'class': 'label'}) .text(Messages.settings_publicSigningKey); $key.append($pubLabel).append(UI.dialog.selectable(userHref)); From 1a6dc200e20c44282074d6928f5582d1d5297160 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 30 Jun 2020 14:23:05 -0400 Subject: [PATCH 4/5] remove a boilerplate test that got implemented elsewhere --- www/assert/main.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/www/assert/main.js b/www/assert/main.js index bfbee3b3a..c29b3bfa3 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -516,14 +516,6 @@ define([ }, "tracker should not timeout"); }()); - (function () { - assert(function (cb) { - cb(false); - - - }, "new format of users' public signing key strings should parse"); // XXX - }()); - Drive.test(assert); assert(function (cb) { From 03be102ce41397ce4fed12fd2f4889d96c203762 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 30 Jun 2020 14:31:17 -0400 Subject: [PATCH 5/5] deduplicate some code by reusing the client definition on the server --- lib/keys.js | 82 +--------------------------- www/common/common-hash.js | 33 ++++-------- www/common/common-signing-keys.js | 89 +++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 103 deletions(-) create mode 100644 www/common/common-signing-keys.js diff --git a/lib/keys.js b/lib/keys.js index c4be44c85..7eb4ce447 100644 --- a/lib/keys.js +++ b/lib/keys.js @@ -1,81 +1 @@ -(function () { -var factory = function () { - var Keys = {}; - -/* Parse the new format of "Signing Public Keys". - If anything about the input is found to be invalid, return; - this will fall back to the old parsing method - - -*/ - var parseNewUser = function (userString) { - if (!/^\[.*?@.*\]$/.test(userString)) { return; } - var temp = userString.slice(1, -1); - var domain, username, pubkey; - - temp = temp - .replace(/\/([a-zA-Z0-9+-]{43}=)$/, function (all, k) { - pubkey = k.replace(/-/g, '/'); - return ''; - }); - if (!pubkey) { return; } - - var index = temp.lastIndexOf('@'); - if (index < 1) { return; } - - domain = temp.slice(index + 1); - username = temp.slice(0, index); - - return { - domain: domain, - user: username, - pubkey: pubkey - }; - }; - - var isValidUser = function (parsed) { - if (!parsed) { return; } - if (!(parsed.domain && parsed.user && parsed.pubkey)) { return; } - return true; - }; - - Keys.parseUser = function (user) { - var parsed = parseNewUser(user); - if (isValidUser(parsed)) { return parsed; } - - var domain, username, pubkey; - user.replace(/^https*:\/\/([^\/]+)\/user\/#\/1\/([^\/]+)\/([a-zA-Z0-9+-]{43}=)$/, - function (a, d, u, k) { - domain = d; - username = u; - pubkey = k.replace(/-/g, '/'); - return ''; - }); - if (!domain) { throw new Error("Could not parse user id [" + user + "]"); } - return { - domain: domain, - user: username, - pubkey: pubkey - }; - }; - - Keys.serialize = function (origin, username, pubkey) { - return '[' + - username + - '@' + - origin.replace(/https*:\/\//, '') + - '/' + - pubkey.replace(/\//g, '-') + - ']'; - // return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-'); - }; - - return Keys; -}; - - if (typeof(module) !== 'undefined' && module.exports) { - module.exports = factory(); - } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { - define([], factory); - } -}()); +module.exports = require("../www/common/common-signing-keys"); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 20e229fdf..cc4344413 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -1,5 +1,5 @@ (function (window) { -var factory = function (Util, Crypto, Nacl) { +var factory = function (Util, Crypto, Keys, Nacl) { var Hash = window.CryptPad_Hash = {}; var uint8ArrayToHex = Util.uint8ArrayToHex; @@ -92,24 +92,7 @@ var factory = function (Util, Crypto, Nacl) { } }; -/* - -0. usernames may contain spaces or many other wacky characters, so enclose the whole thing in square braces so we know its boundaries. If the formatted string does not include these we know it is either a _v1 public key string_ or _an incomplete string_. Start parsing by removing them. -1. public keys should have a fixed length, so slice them off of the end of the string. -2. domains cannot include `@`, so find the last occurence of it in the signing key and slice everything thereafter. -3. the username is everything before the `@`. - -*/ - Hash.getPublicSigningKeyString = function (origin, username, pubkey) { - return '[' + - username + - '@' + - origin.replace(/https*:\/\//, '') + - '/' + - pubkey.replace(/\//g, '-') + - ']'; - // return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-'); - }; + Hash.getPublicSigningKeyString = Keys.serialize; var fixDuplicateSlashes = function (s) { return s.replace(/\/+/g, '/'); @@ -583,14 +566,20 @@ Version 1 }; if (typeof(module) !== 'undefined' && module.exports) { - module.exports = factory(require("./common-util"), require("chainpad-crypto"), require("tweetnacl/nacl-fast")); + module.exports = factory( + require("./common-util"), + require("chainpad-crypto"), + require("./common-signing-keys"), + require("tweetnacl/nacl-fast") + ); } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { define([ '/common/common-util.js', '/bower_components/chainpad-crypto/crypto.js', + '/common/common-signing-keys.js', '/bower_components/tweetnacl/nacl-fast.min.js' - ], function (Util, Crypto) { - return factory(Util, Crypto, window.nacl); + ], function (Util, Crypto, Keys) { + return factory(Util, Crypto, Keys, window.nacl); }); } else { // unsupported initialization diff --git a/www/common/common-signing-keys.js b/www/common/common-signing-keys.js new file mode 100644 index 000000000..15adec7df --- /dev/null +++ b/www/common/common-signing-keys.js @@ -0,0 +1,89 @@ +(function () { +var factory = function () { + var Keys = {}; + +/* Parse the new format of "Signing Public Keys". + If anything about the input is found to be invalid, return; + this will fall back to the old parsing method + + +*/ + var parseNewUser = function (userString) { + if (!/^\[.*?@.*\]$/.test(userString)) { return; } + var temp = userString.slice(1, -1); + var domain, username, pubkey; + + temp = temp + .replace(/\/([a-zA-Z0-9+-]{43}=)$/, function (all, k) { + pubkey = k.replace(/-/g, '/'); + return ''; + }); + if (!pubkey) { return; } + + var index = temp.lastIndexOf('@'); + if (index < 1) { return; } + + domain = temp.slice(index + 1); + username = temp.slice(0, index); + + return { + domain: domain, + user: username, + pubkey: pubkey + }; + }; + + var isValidUser = function (parsed) { + if (!parsed) { return; } + if (!(parsed.domain && parsed.user && parsed.pubkey)) { return; } + return true; + }; + + Keys.parseUser = function (user) { + var parsed = parseNewUser(user); + if (isValidUser(parsed)) { return parsed; } + + var domain, username, pubkey; + user.replace(/^https*:\/\/([^\/]+)\/user\/#\/1\/([^\/]+)\/([a-zA-Z0-9+-]{43}=)$/, + function (a, d, u, k) { + domain = d; + username = u; + pubkey = k.replace(/-/g, '/'); + return ''; + }); + if (!domain) { throw new Error("Could not parse user id [" + user + "]"); } + return { + domain: domain, + user: username, + pubkey: pubkey + }; + }; + +/* + +0. usernames may contain spaces or many other wacky characters, so enclose the whole thing in square braces so we know its boundaries. If the formatted string does not include these we know it is either a _v1 public key string_ or _an incomplete string_. Start parsing by removing them. +1. public keys should have a fixed length, so slice them off of the end of the string. +2. domains cannot include `@`, so find the last occurence of it in the signing key and slice everything thereafter. +3. the username is everything before the `@`. + +*/ + Keys.serialize = function (origin, username, pubkey) { + return '[' + + username + + '@' + + origin.replace(/https*:\/\//, '') + + '/' + + pubkey.replace(/\//g, '-') + + ']'; + // return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-'); + }; + + return Keys; +}; + + if (typeof(module) !== 'undefined' && module.exports) { + module.exports = factory(); + } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { + define([], factory); + } +}());