From 96a00f89dfa3abe9cc5580de933a34c470d7ae43 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Jan 2020 15:16:07 +0100 Subject: [PATCH] Generate burn after reading link for pads --- www/assert/main.js | 8 + www/common/common-hash.js | 9 +- www/common/common-interface.js | 6 + www/common/common-ui-elements.js | 264 +++++++++++++++++++++--------- www/common/outer/invitation.js | 1 + www/common/sframe-common-outer.js | 23 ++- 6 files changed, 219 insertions(+), 92 deletions(-) diff --git a/www/assert/main.js b/www/assert/main.js index ceff9f58b..c29b3bfa3 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -279,6 +279,14 @@ define([ parsed.hashData.embed && parsed.hashData.password); }, "test support for owner key in version 2 hash failed to parse"); + assert(function (cb) { + var secret = Hash.parsePadUrl('/file/#/1/TRplGM-WsVkXR+LkJ0tD3D45A1YFZ-Cy/eO4RJwh8yHEEDhl1aHfuwQ2IzosPBZx-HDaWc1lW+hY=/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/'); + return cb(secret.hashData.version === 1 && + secret.hashData.channel === "TRplGM/WsVkXR+LkJ0tD3D45A1YFZ/Cy" && + secret.hashData.key === "eO4RJwh8yHEEDhl1aHfuwQ2IzosPBZx/HDaWc1lW+hY=" && + secret.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" && + !secret.hashData.present); + }, "test support for owner key in version 1 file hash failed to parse"); assert(function (cb) { var secret = Hash.parsePadUrl('/invite/#/2/invite/edit/oRE0oLCtEXusRDyin7GyLGcS/p/'); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 8f6471f1b..902a91793 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -16,15 +16,16 @@ var factory = function (Util, Crypto, Nacl) { }; // XXX move this code? - Hash.generateSignPair = function (safe) { + Hash.generateSignPair = function () { var ed = Nacl.sign.keyPair(); var makeSafe = function (key) { - if (!safe) { return key; } return Crypto.b64RemoveSlashes(key).replace(/=+$/g, ''); }; return { - validateKey: makeSafe(encode64(ed.publicKey)), - signKey: makeSafe(encode64(ed.secretKey)), + validateKey: Hash.encodeBase64(ed.publicKey), + signKey: Hash.encodeBase64(ed.secretKey), + safeValidateKey: makeSafe(Hash.encodeBase64(ed.publicKey)), + safeSignKey: makeSafe(Hash.encodeBase64(ed.secretKey)), }; }; diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 4528b7a1b..55d7e6939 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -209,10 +209,16 @@ define([ $(title).prepend(' ').prepend(icon); } $(title).click(function () { + var old = tabs[active]; + if (old.onHide) { old.onHide(); } titles.forEach(function (t) { $(t).removeClass('alertify-tabs-active'); }); contents.forEach(function (c) { $(c).removeClass('alertify-tabs-content-active'); }); + if (tab.onShow) { + tab.onShow(); + } $(title).addClass('alertify-tabs-active'); $(content).addClass('alertify-tabs-content-active'); + active = i; }); titles.push(title); contents.push(content); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6323be780..d9f0af36c 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -917,60 +917,79 @@ define([ className: 'primary cp-share-with-friends', name: Messages.share_withFriends, onClick: function () { - var href = Hash.getRelativeHref(linkGetter()); - var $friends = $div.find('.cp-usergrid-user.cp-selected'); - $friends.each(function (i, el) { - var curve = $(el).attr('data-curve'); - // Check if the selected element is a friend or a team - if (curve) { // Friend - if (!curve || !friends[curve]) { return; } - var friend = friends[curve]; - if (!friend.notifications || !friend.curvePublic) { return; } - common.mailbox.sendTo("SHARE_PAD", { + var href; + NThen(function (waitFor) { + var w = waitFor(); + // linkGetter can be async if this is a burn after reading URL + var res = linkGetter({}, function (url) { + if (!url) { + waitFor.abort(); + return; + } + console.warn('BAR'); + href = url; + setTimeout(w); + }); + if (res && /^http/.test(res)) { + href = Hash.getRelativeHref(res); + setTimeout(w); + return; + } + }).nThen(function () { + var $friends = $div.find('.cp-usergrid-user.cp-selected'); + $friends.each(function (i, el) { + var curve = $(el).attr('data-curve'); + // Check if the selected element is a friend or a team + if (curve) { // Friend + if (!curve || !friends[curve]) { return; } + var friend = friends[curve]; + if (!friend.notifications || !friend.curvePublic) { return; } + common.mailbox.sendTo("SHARE_PAD", { + href: href, + password: config.password, + isTemplate: config.isTemplate, + name: myName, + title: title + }, { + channel: friend.notifications, + curvePublic: friend.curvePublic + }); + return; + } + // Team + var ed = $(el).attr('data-ed'); + var team = teams[ed]; + if (!team) { return; } + sframeChan.query('Q_STORE_IN_TEAM', { href: href, password: config.password, - isTemplate: config.isTemplate, - name: myName, - title: title - }, { - channel: friend.notifications, - curvePublic: friend.curvePublic + path: config.isTemplate ? ['template'] : undefined, + title: title, + teamId: team.id + }, function (err) { + if (err) { return void console.error(err); } }); - return; - } - // Team - var ed = $(el).attr('data-ed'); - var team = teams[ed]; - if (!team) { return; } - sframeChan.query('Q_STORE_IN_TEAM', { - href: href, - password: config.password, - path: config.isTemplate ? ['template'] : undefined, - title: title, - teamId: team.id - }, function (err) { - if (err) { return void console.error(err); } }); - }); - UI.findCancelButton().click(); - - // Update the "recently shared with" array: - // Get the selected curves - var curves = $friends.toArray().map(function (el) { - return ($(el).attr('data-curve') || '').slice(0,8); - }).filter(function (x) { return x; }); - // Prepend them to the "order" array - Array.prototype.unshift.apply(order, curves); - order = Util.deduplicateString(order); - // Make sure we don't have "old" friends and save - order = order.filter(function (curve) { - return smallCurves.indexOf(curve) !== -1; + UI.findCancelButton().click(); + + // Update the "recently shared with" array: + // Get the selected curves + var curves = $friends.toArray().map(function (el) { + return ($(el).attr('data-curve') || '').slice(0,8); + }).filter(function (x) { return x; }); + // Prepend them to the "order" array + Array.prototype.unshift.apply(order, curves); + order = Util.deduplicateString(order); + // Make sure we don't have "old" friends and save + order = order.filter(function (curve) { + return smallCurves.indexOf(curve) !== -1; + }); + common.setAttribute(['general', 'share-friends'], order); + if (onShare) { + onShare.fire(); + } }); - common.setAttribute(['general', 'share-friends'], order); - if (onShare) { - onShare.fire(); - } }, keys: [13] }; @@ -1049,6 +1068,29 @@ define([ } }; + var makeBurnAfterReadingUrl = function (common, href, channel, cb) { + var keyPair = Hash.generateSignPair(); + var parsed = Hash.parsePadUrl(href); + console.error(href, parsed); + var newHref = parsed.getUrl({ + ownerKey: keyPair.safeSignKey + }); + var sframeChan = common.getSframeChannel(); + NThen(function (waitFor) { + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_OWNERS', + value: [keyPair.validateKey] + }, waitFor(function (err) { + if (err) { + waitFor.abort(); + UI.warn(Messages.error); + } + })); + }).nThen(function () { + cb(newHref); + }); + }; UIElements.createShareModal = function (config) { var origin = config.origin; var pathname = config.pathname; @@ -1078,6 +1120,7 @@ define([ var parsed = Hash.parsePadUrl(pathname); var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1; + var burnAfterReading; var rights = h('div.msg.cp-inline-radio-group', [ h('label', Messages.share_linkAccess), h('div.radio-group',[ @@ -1086,9 +1129,33 @@ define([ canPresent ? UI.createRadio('accessRights', 'cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined, UI.createRadio('accessRights', 'cp-share-editable-true', - Messages.share_linkEdit, false, { mark: {tabindex:1} })]) + Messages.share_linkEdit, false, { mark: {tabindex:1} })]), + burnAfterReading = hashes.viewHash ? UI.createRadio('accessRights', 'cp-share-bar', + 'BAR', false, { mark: {tabindex:1}, label: {style: "display: none;"} }) : undefined // XXX ]); + // Burn after reading + // Check if we are an owner of this pad. If we are, we can show the burn after reading option. + // When BAR is selected, display a red message indicating the consequence and add + // the options to generate the BAR url + var barAlert = h('div.alert.alert-danger.cp-alertify-bar-selected', { + style: 'display: none;' + }, " You have set this pad to self-destruct. Once a recipient opens this pad, it will be permanently deleted from the server. "); // XXX + var channel = Hash.getSecrets('pad', hash, config.password).channel; + common.getPadMetadata({ + channel: channel + }, function (obj) { + if (!obj || obj.error) { return; } + var priv = common.getMetadataMgr().getPrivateData(); + // Not an owner: don't display the burn after reading option + if (!Array.isArray(obj.owners) || obj.owners.indexOf(priv.edPublic) === -1) { + $(burnAfterReading).remove(); + return; + } + // When the burn after reading option is selected, transform the modal buttons + $(burnAfterReading).show(); + }); + var $rights = $(rights); var saveValue = function () { @@ -1100,13 +1167,25 @@ define([ }); }; - var getLinkValue = function (initValue) { + var burnAfterReadingUrl; + + var getLinkValue = function (initValue, cb) { var val = initValue || {}; var edit = val.edit !== undefined ? val.edit : Util.isChecked($rights.find('#cp-share-editable-true')); var embed = val.embed; var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present')); + var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar')); + if (burnAfterReading && !burnAfterReadingUrl) { + if (cb) { // Called from the contacts tab, "share" button + var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash); + return makeBurnAfterReadingUrl(common, barHref, channel, function (url) { + cb(url); + }); + } + return 'XXX Click on the button below to generate a link'; // XXX + } var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; - var href = origin + pathname + '#' + hash; + var href = burnAfterReading ? burnAfterReadingUrl : (origin + pathname + '#' + hash); var parsed = Hash.parsePadUrl(href); return origin + parsed.getUrl({embed: embed, present: present}); }; @@ -1160,8 +1239,8 @@ define([ }); }); - - + + linkContent.push($(barAlert).clone()[0]); // Burn after reading var link = h('div.cp-share-modal', linkContent); var $link = $(link); @@ -1169,7 +1248,7 @@ define([ var linkButtons = [ makeCancelButton(), !config.sharedFolder && { - className: 'secondary', + className: 'secondary cp-nobar', name: Messages.share_linkOpen, onClick: function () { saveValue(); @@ -1180,9 +1259,8 @@ define([ return true; }, keys: [[13, 'ctrl']] - }, - { - className: 'primary', + }, { + className: 'primary cp-nobar', name: Messages.share_linkCopy, onClick: function () { saveValue(); @@ -1193,26 +1271,26 @@ define([ if (success) { UI.log(Messages.shareSuccess); } }, keys: [13] + }, { + className: 'primary cp-bar', + name: 'GENERATE LINK', + onClick: function () { + var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash); + makeBurnAfterReadingUrl(common, barHref, channel, function (url) { + burnAfterReadingUrl = url; + $rights.find('input[type="radio"]').trigger('change'); + }); + return true; + }, + keys: [] } ]; - // update values for link preview when radio btns change - $link.find('#cp-share-link-preview').val(getLinkValue()); - $rights.find('input[type="radio"]').on('change', function () { - $link.find('#cp-share-link-preview').val(getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - })); - }); - $link.find('input[type="checkbox"]').on('change', function () { - $link.find('#cp-share-link-preview').val(getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - })); - }); - var frameLink = UI.dialog.customModal(link, { buttons: linkButtons, onClose: config.onClose, }); + $(frameLink).find('.cp-bar').hide(); // Share with contacts tab @@ -1240,6 +1318,7 @@ define([ ])); } + $(contactsContent).append($(barAlert).clone()); // Burn after reading var contactButtons = friendsObject.buttons; contactButtons.unshift(makeCancelButton()); @@ -1282,21 +1361,52 @@ define([ keys: [13] }]; + var onShowEmbed = function () { + $rights.find('#cp-share-bar').closest('label').hide(); + $rights.find('input[type="radio"]:enabled').first().prop('checked', 'checked'); + $rights.find('input[type="radio"]').trigger('change'); + }; + var embed = h('div.cp-share-modal', embedContent); var $embed = $(embed); - // update values for link preview when radio btns change + var frameEmbed = UI.dialog.customModal(embed, { + buttons: embedButtons, + onClose: config.onClose, + }); + + // update values for link and embed preview when radio btns change $embed.find('#cp-embed-link-preview').val(getEmbedValue()); + $link.find('#cp-share-link-preview').val(getLinkValue()); $rights.find('input[type="radio"]').on('change', function () { + $link.find('#cp-share-link-preview').val(getLinkValue({ + embed: Util.isChecked($link.find('#cp-share-embed')) + })); + // Hide or show the burn after reading alert + if (Util.isChecked($rights.find('#cp-share-bar'))) { + $('.cp-alertify-bar-selected').show(); + // Show burn after reading button + $('.alertify').find('.cp-bar').show(); + $('.alertify').find('.cp-nobar').hide(); + return; + } $embed.find('#cp-embed-link-preview').val(getEmbedValue()); + // Hide burn after reading button + $('.alertify').find('.cp-nobar').show(); + $('.alertify').find('.cp-bar').hide(); + $('.cp-alertify-bar-selected').hide(); }); - - var frameEmbed = UI.dialog.customModal(embed, { - buttons: embedButtons, - onClose: config.onClose, + $link.find('input[type="checkbox"]').on('change', function () { + $link.find('#cp-share-link-preview').val(getLinkValue({ + embed: Util.isChecked($link.find('#cp-share-embed')) + })); }); + // Create modal + var resetTab = function () { + $rights.find('label.cp-radio').show(); + }; var tabs = [{ title: Messages.share_contactCategory, icon: "fa fa-address-book", @@ -1310,7 +1420,9 @@ define([ }, { title: Messages.share_embedCategory, icon: "fa fa-code", - content: frameEmbed + content: frameEmbed, + onShow: onShowEmbed, + onHide: resetTab }]; if (typeof(AppConfig.customizeShareOptions) === 'function') { AppConfig.customizeShareOptions(hashes, tabs, { diff --git a/www/common/outer/invitation.js b/www/common/outer/invitation.js index 603cae3c1..d84d7a81f 100644 --- a/www/common/outer/invitation.js +++ b/www/common/outer/invitation.js @@ -17,6 +17,7 @@ var factory = function (Util, Cred, Nacl) { }; }; + // XXX move this function? Invite.generateSignPair = function () { var ed = Nacl.sign.keyPair(); return { diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 05cafbdec..9308f05cb 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -507,6 +507,17 @@ define([ } }); + sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) { + if (!data || !data.channel) { + data = { + channel: secret.channel + }; + } + Cryptpad.getPadMetadata(data, cb); + }); + sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) { + Cryptpad.setPadMetadata(data, cb); + }); }; addCommonRpc(sframeChan); @@ -1170,18 +1181,6 @@ define([ }); }); - sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) { - if (!data || !data.channel) { - data = { - channel: secret.channel - }; - } - Cryptpad.getPadMetadata(data, cb); - }); - sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) { - Cryptpad.setPadMetadata(data, cb); - }); - if (cfg.messaging) { Notifier.getPermission();