Revocation: share with user

This commit is contained in:
yflory 2023-04-13 14:30:22 +02:00
parent b0d65139ce
commit 3c6819ec4a
9 changed files with 211 additions and 72 deletions

View File

@ -47,6 +47,9 @@
display: flex;
max-width: 100%;
align-items: center;
& > input {
margin-bottom: 0;
}
&:not(:last-child) {
margin-bottom: 10px;
}

View File

@ -457,7 +457,7 @@ revocationCommands.UPDATE_ACCESS = function (meta, args, myKey) {
} else if (!oldValue) { // Create
newValue.from = myKey;
access[user] = newValue;
return;
return true;
} else { // Update
oldValue.rights = newValue.rights;
oldValue.from = myKey; // XXX to decide: when updating an access, put it under your tree

View File

@ -726,7 +726,8 @@ Version 5: Revocable mailbox
Hash.hrefToHexChannelId = function (href, password) {
var parsed = Hash.parsePadUrl(href);
if (!parsed || !parsed.hash) { return; }
var secret = Hash.getSecrets(parsed.type, parsed.hash, password);
var secret = parsed.revocable ? Hash.getRevocable(parsed.type, parsed.hashData.key)
: Hash.getSecrets(parsed.type, parsed.hash, password);
return secret.channel;
};

View File

@ -222,6 +222,10 @@ define([
}
onSelect();
});
} else if (config.picker) {
$div.on('click', '.cp-usergrid-user', function () {
onSelect($(this));
});
}
return {

View File

@ -368,6 +368,11 @@ define([
(function () {
var bypassHashChange = function (key) {
return function (value) {
if (currentPad.type && currentPad.type !== 'link' && /^\/5\//.test(value)) {
// XXX REVOCATION decide if we store user access before opening it
console.error('Preserve safe link for revocable access', key);
return;
}
var ohc = window.onhashchange;
window.onhashchange = function () {};
window.location[key] = value;

View File

@ -321,6 +321,8 @@ define([
var hasFriends = opts.hasFriends;
var onFriendShare = Util.mkEvent();
var myName = common.getMetadataMgr().getUserData().name;
var title = opts.title;
var metadataMgr = common.getMetadataMgr();
var priv = metadataMgr.getPrivateData();
@ -344,15 +346,21 @@ define([
var viewAs = h('div.cp-share-access-as');
var list = h('div.cp-share-access-list');
var content = h('div.cp-share-access-list-container', [viewAs, list]);
var $content = $(list);
var buttonContainer = h('div');
var content = h('div.cp-share-access-list-container', [viewAs, list, buttonContainer]);
var $list = $(list);
var $viewAs = $(viewAs);
var usergrid = h('div.cp-share-access-usergrid');
var $usergrid = $(usergrid);
var $content = $(content);
var channel = rev.channel;
var revocation = common.makeUniversal('revocation');
var updateAccess = function () {};
var addAccess = function () {};
var redraw = function () {};
var TYPES = {
user: { icon: '.fa.fa-user', order: 1 },
@ -363,16 +371,18 @@ define([
var makeDD = function (current, editable, maxR) {
var value;
if (current.includes('m')) { value = 'moderate'; }
else if (current.includes('w')) { value = 'write'; }
current = current.replace(/d+$/, '');
if (current.includes('rwm')) { value = 'moderate'; }
else if (current.includes('rw')) { value = 'write'; }
else if (current.includes('r')) { value = 'read'; }
if (!editable) {
return [h('button.btn.btn-secondary', {disabled:'disabled'}, value)]; // XXX
}
var options = ['r', 'w', 'm'].map(function (r, i) {
var options = ['r', 'rw', 'rwm'].map(function (r, i) {
if (maxR === 'r' && i) { return; }
if (maxR === 'w' && i > 1) { return; }
if (maxR === 'rw' && i > 1) { return; }
return {
tag: 'a',
attributes: {
@ -397,9 +407,9 @@ define([
var getRights = function (dd, $d) {
var r = dd.getValue() || 'r';
var d = Util.isChecked($d);
var rights = r === 'm' ? 'rwm' : (r === 'w' ? 'rw' : 'r');
if (d) { rights += 'd'; }
return rights;
//var rights = r === 'm' ? 'rwm' : (r === 'w' ? 'rw' : 'r');
if (d) { r += 'd'; }
return r;
};
var renderAccess = function (edPublic, accessData, editable, maxR, renderedAs) {
var type = accessData.notes.type
@ -451,9 +461,11 @@ define([
]);
};
var addAccessButton = function (maxR, renderedAs) {
var addAccessForm = function (maxR, renderedAs, userData, onCancel) {
// new form
var input = h('input', {placeholder:'Note'}); // XXX
var input = h('input', {
placeholder: (userData && userData.displayName) || 'Note' // XXX
});
var dd = makeDD('r', true, maxR);
var canDestroy = UI.createCheckbox('cp-share-can-destroy', h('i.fa.fa-trash'), false, {});
var $d = $(canDestroy).find('input');
@ -465,8 +477,8 @@ define([
h('i.fa.fa-times')
]);
var temp = h('div.cp-share-access', {order:100, style: 'display:none;'}, [
h('i.fa.fa-plus'),
var temp = h('div.cp-share-access', {order:100}, [
userData ? h('i.fa.fa-user-plus') : h('i.fa.fa-plus'),
input,
dd[0],
canDestroy,
@ -475,6 +487,93 @@ define([
]);
var $temp = $(temp);
$(cancelBtn).click(function () {
onCancel();
$temp.remove();
});
$(saveBtn).click(function () {
var access = getRights(dd, $d);
var note = {
type: 'link',
note: $(input).val()
};
if (userData && userData.edPublic) {
// XXX REVOCATION teams and sf?
note.type = 'user';
note.edPublic = userData.edPublic;
}
// XXX REVOCATION add "share with team when I'm a viewer" (send to mailbox)
addAccess(access, note, renderedAs.key, function (obj) {
redraw();
if (obj && obj.error) { return UI.warn(Messages.error); }
var box = Hash.getRevocable(priv.app, obj.seed);
var hash = Hash.getRevocableHashFromKeys(priv.app, box);
var href = Hash.hashToHref(hash, priv.app);
common.mailbox.sendTo("SHARE_PAD", {
href: href,
isStatic: Boolean(opts.static),
password: opts.password,
isTemplate: opts.isTemplate,
name: myName,
isCalendar: Boolean(opts.calendar),
title: title,
revocable: {
channel: opts.channel,
}
}, {
channel: userData.notifications,
curvePublic: userData.curvePublic
});
});
});
return $temp;
};
var refreshUsergrid = function (cb) {
$usergrid.empty().show();
$content.hide();
var friendsList = UIElements.getUserGrid(Messages.share_linkFriends, {
common: common,
data: opts.friends,
noFilter: true, // XXX
noSelect: true,
picker: true,
large: true
}, function ($friend) {
var curve = $friend.data('curve');
var data = opts.friends[curve];
if (!data || !data.notifications || !data.curvePublic) {
// XXX REVOCATION show error? can't share with this contact
return void redraw();
}
$usergrid.hide();
$content.show();
cb(data);
});
$usergrid.append(friendsList.div);
};
var addUserAccessButton = function (maxR, renderedAs) {
var button = h('button.btn.btn-primary', [
h('i.fa.fa-plus'),
h('span', 'ADD USER') // XXX
]);
var $b = $(button);
var onCancel = function () { $b.show(); };
$b.click(function () {
$b.hide();
refreshUsergrid(function (userData) {
var $form = addAccessForm(maxR, renderedAs, userData, onCancel);
$list.append($form);
});
});
$(buttonContainer).append($b);
return button;
};
var addAccessButton = function (maxR, renderedAs) {
// show form btn
var button = h('button.btn.btn-primary', [
h('i.fa.fa-plus'),
@ -482,34 +581,21 @@ define([
]);
var $b = $(button);
$(saveBtn).click(function () {
var access = getRights(dd, $d);
var note = {
type: 'link',
note: $(input).val()
};
if (false) { note.edPublic = "edpublic"; } // XXX user access
addAccess(access, note, renderedAs.key);
});
$(cancelBtn).click(function () {
$b.show();
$temp.hide();
});
var onCancel = function () { $b.show(); };
$b.click(function () {
$b.hide();
$temp.css('display', 'flex');
var $form = addAccessForm(maxR, renderedAs, null, onCancel);
$list.append($form);
});
$content.append(temp);
var container = h('div', button);
$(content).append(container);
return container;
$(buttonContainer).append($b);
return button;
};
var addButton;
var addButton, addUserButton;
var renderAll = function (obj, renderedAs) {
$content.empty();
$list.empty();
var list = obj.list;
var myAccess = list[renderedAs.key];
var maxRights = myAccess.rights.includes('m') ? 'm' :
@ -517,10 +603,12 @@ define([
Object.keys(list || {}).forEach(function (ed) {
var editable = renderedAs.moderator || renderedAs.key === list[ed].from;
var a = renderAccess(ed, list[ed], editable, maxRights, renderedAs);
$content.append(a);
$list.append(a);
});
if (addButton) { $(addButton).remove(); }
addButton = addAccessButton(maxRights, renderedAs);
if (addUserButton) { $(addUserButton).remove(); }
addUserButton = addUserAccessButton(maxRights, renderedAs);
};
var renderAs = function (obj) {
$viewAs.empty();
@ -554,7 +642,22 @@ define([
renderAll(obj, myKeys[0]);
};
addAccess = function (rights, note, updateAs) {
redraw = function () {
revocation.execCommand('LIST_ACCESS', {
channel: channel
}, function (obj) {
if (obj && obj.error) {
return void UI.warn(Messages.error);
}
if (!obj.myKeys || !obj.myKeys.length) {
console.error('Not a member!');
return void UI.warn(Messages.error);
}
renderAs(obj);
});
};
addAccess = function (rights, note, updateAs, cb) {
revocation.execCommand('ADD_ACCESS', {
type: priv.app,
channel: channel,
@ -562,8 +665,10 @@ define([
note: note,
from: updateAs
}, function (obj) {
console.warn(obj);
// XXX refresh view
if (cb) { return void cb(obj); }
redraw();
if (obj && obj.error) { return UI.warn(Messages.error); }
});
};
updateAccess = function (user, rights, updateAs) {
@ -578,25 +683,15 @@ define([
},
from: updateAs
}, function () {
redraw();
// XXX refresh view
});
};
revocation.execCommand('LIST_ACCESS', {
channel: channel
}, function (obj) {
if (obj && obj.error) {
return void UI.warn(Messages.error);
}
if (!obj.myKeys || !obj.myKeys.length) {
console.error('Not a member!');
return void UI.warn(Messages.error);
}
renderAs(obj);
});
redraw();
cb(void 0, {
content: content,
content: [content, usergrid],
buttons: [{
className: 'cancel',
name: Messages.filePicker_close,
@ -1071,7 +1166,7 @@ define([
opts.hasPassword = parsedHref.hashData.password;
var $rights;
if (!parsedHref.revocable) {
if (!hashes.revocableData) {
// XXX this function adds opts.channel which breaks data
// but we may not need it for revocable pads
$rights = opts.$rights = getRightsHeader(common, opts);

View File

@ -122,12 +122,29 @@ define([
}, defaultDismiss(common, data));
return;
}
var href = msg.content.href;
var obj = {
p: msg.content.isTemplate ? ['template'] : undefined,
t: teamNotification || undefined,
pw: msg.content.password || ''
};
common.openURL(Hash.getNewPadURL(msg.content.href, obj));
if (msg.content.revocable) {
// XXX TODO
// We don't want to leak the user personal access for this pad
// The URL should not be visible in the address bar otherwise the user may try
// to copy it and send it to others.
// Option 1: [ ] Ask the user to store in drive before opening
// Option 2: [x] Hide mailbox data in pad options (instantly removed in sco)
var channel = msg.content.revocable.channel;
var parsed = Hash.parsePadUrl(href);
var hash = Hash.getHiddenHashFromKeys(parsed.type, {channel:channel});
href = Hash.hashToHref(hash, parsed.type);
obj.revocable = {
type: 'user',
seed: parsed.hashData && parsed.hashData.key
};
}
common.openURL(Hash.getNewPadURL(href, obj));
defaultDismiss(common, data)();
};
if (!content.archived) {

View File

@ -661,7 +661,7 @@ updateAccess({
var keys = box.parse.getContent();
getPadMetadata(ctx, obj.channel, function (md) {
if (md && md.error) { return; }
if (md && md.error) { return void cb(md); }
var edPrivate = keys.edPrivate;
var log = Revocable.getSanitizedLog(md);
var last = log[log.length-1];
@ -724,14 +724,15 @@ updateAccess({
moderatorSeedStr: keys.doc.moderator
}, keys.password);
var crypto = Crypto.createEncryptor(doc.keys);
var cryptoSym = crypto.encrypt;
var cryptoSym = crypto.encrypt; // XXX WRONG ==> DONT SIGN THE NOTES? ONLY ENCRYPT
var cryptoAsym = crypto.encrypt; // XXX
var mailboxData = Revocable.createMailbox(type, box.secret, keys.doc, rights);
var access = Revocable.createAccess(type, mailboxData, note, cryptoSym, cryptoAsym);
console.error('NEW HASH', Hash.getRevocableHashFromKeys(type, mailboxData.mailbox)); // XXX
sendInitMsg(ctx, mailboxData.initMsg, function () {
sendInitMsg(ctx, mailboxData.initMsg, function (sendObj) {
if (sendObj && sendObj.error) { return void cb(sendObj); }
updateAccess(ctx, {
add: true,
channel: keys.channel,
@ -740,7 +741,15 @@ console.error('NEW HASH', Hash.getRevocableHashFromKeys(type, mailboxData.mailbo
access: access
},
from: obj.from,
}, clientId, cb);
}, clientId, function (accessObj) {
if (accessObj && accessObj.error) {
// XXX delete mailbox? or let it be deleted after 3 months
return void cb(accessObj);
}
cb({
seed: mailboxData.mailbox.keys.seed
});
});
});
};
@ -839,6 +848,7 @@ console.error(keyHashStr);
nThen(function (waitFor) {
sendInitMsg(ctx, moderator.initMsg, waitFor());
sendInitMsg(ctx, editor.initMsg, waitFor());
// XXX handle errors
}).nThen(function (waitFor) {
cb(data);
});

View File

@ -360,6 +360,7 @@ define([
// New pad options
var options = parsed.getOptions();
if (options.newPadOpts) {
var rev = false;
try {
var newPad = Utils.Hash.decodeDataOptions(options.newPadOpts);
Cryptpad.initialTeam = newPad.t;
@ -381,15 +382,26 @@ define([
delete Cryptpad.fromFileData;
}
}
if (newPad.revocable) {
// XXX REVOCATION fake safe link user access, might change
var seed = newPad.revocable.seed;
var box = Utils.Hash.getRevocable(parsed.type, seed);
currentPad.hash = Utils.Hash.getRevocableHashFromKeys(parsed.type, box);
currentPad.type = newPad.revocable.type;
currentPad.href = Utils.Hash.hashToHref(currentPad.hash, parsed.type);
rev = true;
Cryptpad.setTabHash(parsed.hashData.getHash({}));
}
} catch (e) {
console.error(e, parsed.hashData.newPadOpts);
}
delete options.newPadOpts;
currentPad.href = parsed.getUrl(options);
currentPad.hash = parsed.hashData.getHash ? parsed.hashData.getHash(options)
: '';
if (!rev) {
currentPad.href = parsed.getUrl(options);
currentPad.hash = parsed.hashData.getHash ? parsed.hashData.getHash(options)
: '';
}
var version = parsed.hashData.version;
parsed = Utils.Hash.parsePadUrl(currentPad.href);
Cryptpad.setTabHash(currentPad.hash);
@ -479,6 +491,7 @@ define([
value: ''
};
// Handle pads with revocable access (safe & unsafe)
var getRevocable = function (w) {
var correctPassword = waitFor();
@ -545,7 +558,6 @@ define([
}
};
}));
};
// Hidden hash: can't find the channel in our drives: abort
@ -557,10 +569,6 @@ define([
var expire;
var revocable;
nThen(function (w) {
return;
// XXX REVOCATION
if (parsed.hashData.version !== 5) { return; }
}).nThen(function (w) {
// If we're using an unsafe link, get pad attribute
if (parsed.hashData.key || !parsed.hashData.channel) {
Cryptpad.getPadAttribute('expire', w(function (err, data) {
@ -611,18 +619,14 @@ define([
var chan = parsed.hashData.version === 3 && parsed.hashData.channel;
var url = revocable ? null : parsed.getUrl();
Cryptpad.getPadAttribute('channel', w(function (err, data) {
console.error(err, data);
stored = (!err && typeof (data) === "string");
}), null, chan);
Cryptpad.getPadAttribute('password', w(function (err, val) {
console.error(err, val);
password = val;
}), url, chan);
}).nThen(function (w) {
console.error(expire, password);
// Revocable pad, safe (revocable) or unsafe (parsed.revocable) link
if (parsed.revocable || revocable) {
console.error(parsed.revocable, revocable);
return void getRevocable(w);
}