Allow anonymous answers

pull/1/head
yflory 4 years ago
parent 833bcc93cc
commit fc8ce9cb0e

@ -76,7 +76,7 @@ define([
postMessage("GET", {
key: ['edPrivate'],
}, waitFor(function (obj) {
if (obj.error) { return; }
if (!obj || obj.error) { return; }
try {
keys.push({
edPrivate: obj,
@ -89,7 +89,7 @@ define([
postMessage("GET", {
key: ['teams'],
}, waitFor(function (obj) {
if (obj.error) { return; }
if (!obj || obj.error) { return; }
Object.keys(obj || {}).forEach(function (id) {
var t = obj[id];
var _keys = t.keys.drive || {};
@ -104,13 +104,26 @@ define([
};
common.getFormKeys = function (cb) {
postMessage("GET", {
key: ['curvePrivate'],
}, function (obj) {
if (obj.error) { return void cb(); }
var curvePrivate;
var formSeed;
Nthen(function (waitFor) {
postMessage("GET", {
key: ['curvePrivate'],
}, waitFor(function (obj) {
if (!obj || obj.error) { return; }
curvePrivate = obj;
}));
postMessage("GET", {
key: ['form_seed'],
}, waitFor(function (obj) {
if (!obj || obj.error) { return; }
formSeed = obj;
}));
}).nThen(function () {
cb({
curvePrivate: obj,
curvePublic: Hash.getCurvePublicFromPrivate(obj)
curvePrivate: curvePrivate,
curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate),
formSeed: formSeed
});
});
};
@ -124,7 +137,8 @@ define([
key: ['forms', data.channel],
value: {
hash: data.hash,
curvePrivate: data.curvePrivate
curvePrivate: data.curvePrivate,
anonymous: data.anonymous
}
}, function (obj) {
if (obj && obj.error) { console.error(obj.error); }

@ -629,6 +629,7 @@ define([
if (!proxy.uid) {
store.noDriveUid = store.noDriveUid || Hash.createChannelId();
}
var metadata = {
// "user" is shared with everybody via the userlist
user: {
@ -655,7 +656,7 @@ define([
accountName: proxy.login_name || '',
offline: store.proxy && store.offline,
teams: teams,
plan: account.plan
plan: account.plan,
}
};
cb(JSON.parse(JSON.stringify(metadata)));
@ -2710,6 +2711,10 @@ define([
if (!proxy.settings) { proxy.settings = NEW_USER_SETTINGS; }
if (!proxy.forms) { proxy.forms = {}; }
if (!proxy.friends_pending) { proxy.friends_pending = {}; }
// Form seed is used to generate a box encryption keypair when
// answering a form anonymously
if (!proxy.form_seed) { proxy.form_seed = Hash.createChannelId(); }
// Call onCacheReady if the manager is not yet defined
if (!manager) {

@ -110,6 +110,12 @@ define([
Messages.form_isClosed = "This form was closed on {0}";
Messages.form_willClose = "This form will close on {0}";
Messages.form_anonymous_on = "Anonymous answers are allowed";
Messages.form_anonymous_off = "Anonymous answers are blocked";
Messages.form_anonymous_button_on = "Block anonymous answers";
Messages.form_anonymous_button_off = "Allow anonymous answers";
Messages.form_anonymous_blocked = "Anonymous responses are blocked for this form. You must log in or register to submit answers.";
Messages.form_defaultOption = "Option {0}";
Messages.form_defaultItem = "Item {0}";
Messages.form_newOption = "New option";
@ -119,6 +125,7 @@ define([
Messages.form_addMultiple = "Add all";
Messages.form_clear = "Clear";
Messages.form_anonymousBox = "Answer anonymously";
var MAX_OPTIONS = 15; // XXX
var MAX_ITEMS = 10; // XXX
@ -544,7 +551,6 @@ define([
});
var dayValues = Object.keys(_days).map(function (d) { return _days[d]; });
var minDay = Math.min.apply(null, dayValues);
console.log(_days, minDay);
Object.keys(_days).forEach(function (day) {
days.push(h('div.cp-poll-cell.cp-poll-time-day', {
style: 'flex-grow:'+(_days[day]-1)+';'
@ -1061,13 +1067,10 @@ define([
return cell;
});
// Name input
var nameInput = h('input', { value: username || '' });
var nameInput = h('input', { value: username || Messages.anonymous });
addLine.unshift(h('div.cp-poll-cell', nameInput));
// XXX Submit button here?
lines.push(h('div', addLine));
var tag = h('div.cp-form-type-poll', lines);
var $tag = $(tag);
@ -1150,6 +1153,20 @@ define([
return results;
};
var makeFormControls = function (framework, content, update) {
var loggedIn = framework._.sfCommon.isLoggedIn();
if (!loggedIn && !content.answers.anonymous) { return; }
var cbox;
if (loggedIn) {
cbox = UI.createCheckbox('cp-form-anonymous',
Messages.form_anonymousBox, false, { mark: { tabindex:1 } });
if (!content.answers.anonymous) {
$(cbox).find('input').attr('disabled', 'disabled');
}
}
var send = h('button.cp-open.btn.btn-primary', update ? Messages.form_update : Messages.form_submit);
var reset = h('button.cp-open.btn.btn-danger-alt', Messages.form_reset);
$(reset).click(function () {
@ -1162,10 +1179,12 @@ define([
$send.attr('disabled', 'disabled');
var results = getFormResults();
if (!results) { return; }
var sframeChan = framework._.sfCommon.getSframeChannel();
sframeChan.query('Q_FORM_SUBMIT', {
mailbox: content.answers,
results: results
results: results,
anonymous: !loggedIn || Util.isChecked($(cbox).find('input'))
}, function (err, data) {
$send.attr('disabled', 'disabled');
if (err || (data && data.error)) {
@ -1186,7 +1205,8 @@ define([
var sframeChan = framework._.sfCommon.getSframeChannel();
var $v = $(viewResults).click(function () {
$v.attr('disabled', 'disabled');
sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, answers) {
sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, obj) {
var answers = obj && obj.results;
if (answers) { APP.answers = answers; }
$v.removeAttr('disabled');
$('body').addClass('cp-app-form-results');
@ -1200,7 +1220,10 @@ define([
reset = undefined;
}
return h('div.cp-form-send-container', [send, reset, viewResults]);
return h('div.cp-form-send-container', [
cbox ? h('div', cbox) : undefined,
send, reset, viewResults
]);
};
var updateForm = function (framework, content, editable, answers, temp) {
var $container = $('div.cp-form-creator-content');
@ -1401,6 +1424,7 @@ define([
var andThen = function (framework) {
framework.start();
var evOnChange = Util.mkEvent();
var content = {};
var sframeChan = framework._.sfCommon.getSframeChannel();
@ -1416,26 +1440,54 @@ define([
$toolbarContainer.after(helpMenu.menu);
// XXX refresh form settings on remote change
var makeFormSettings = function () {
var makePublic = h('button.btn.btn-primary', Messages.form_makePublic);
if (content.answers.privateKey) { makePublic = undefined; }
var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate;
var resultsType = h('div.cp-form-results-type-container', [
h('span.cp-form-results-type', publicText),
makePublic
]);
var $makePublic = $(makePublic).click(function () {
UI.confirm(Messages.form_makePublicWarning, function (yes) {
if (!yes) { return; }
content.answers.privateKey = priv.form_private;
// Private / public status
var resultsType = h('div.cp-form-results-type-container');
var $results = $(resultsType);
var refreshPublic = function () {
$results.empty();
var makePublic = h('button.btn.btn-primary', Messages.form_makePublic);
if (content.answers.privateKey) { makePublic = undefined; }
var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate;
$results.append(h('span.cp-form-results-type', publicText));
$results.append(makePublic);
var $makePublic = $(makePublic).click(function () {
UI.confirm(Messages.form_makePublicWarning, function (yes) {
if (!yes) { return; }
$makePublic.attr('disabled', 'disabled');
content.answers.privateKey = priv.form_private;
framework.localChange();
framework._.cpNfInner.chainpad.onSettle(function () {
UI.log(Messages.saved);
refreshPublic();
});
});
});
};
refreshPublic();
// Allow anonymous answers
var privacyContainer = h('div.cp-form-privacy-container');
var $privacy = $(privacyContainer);
var refreshPrivacy = function () {
$privacy.empty();
var anonymous = content.answers.anonymous;
var key = anonymous ? 'on' : 'off';
var button = h('button.btn.btn-secondary', Messages['form_anonymous_button_'+key]);
var $b = $(button).click(function () {
$b.attr('disabled', 'disabled');
content.answers.anonymous = !anonymous;
framework.localChange();
framework._.cpNfInner.chainpad.onSettle(function () {
UI.log(Messages.saved);
$makePublic.remove();
$(resultsType).find('.cp-form-results-type').text(Messages.form_isPublic);
refreshPrivacy();
});
});
});
$privacy.append(h('div.cp-form-status', Messages['form_anonymous_'+key]));
$privacy.append(h('div.cp-form-actions', button));
};
refreshPrivacy();
// End date / Closed state
var endDateContainer = h('div.cp-form-status-container');
@ -1511,7 +1563,8 @@ define([
channel: content.answers.channel,
validateKey: content.answers.validateKey,
publicKey: content.answers.publicKey
}, function (err, answers) {
}, function (err, obj) {
var answers = obj && obj.results;
if (answers) { APP.answers = answers; }
$v.removeAttr('disabled');
$body.addClass('cp-app-form-results');
@ -1519,8 +1572,14 @@ define([
});
});
evOnChange.reg(refreshPublic);
evOnChange.reg(refreshPrivacy);
evOnChange.reg(refreshEndDate);
return [
endDateContainer,
privacyContainer,
resultsType,
viewResults,
];
@ -1665,9 +1724,10 @@ define([
publicKey: content.answers.publicKey,
privateKey: priv.form_auditorKey
}, function (err, obj) {
if (obj) { APP.answers = obj; }
var answers = obj && obj.results;
if (answers) { APP.answers = answers; }
$body.addClass('cp-app-form-results');
renderResults(content, obj);
renderResults(content, answers);
});
return;
}
@ -1678,16 +1738,21 @@ define([
validateKey: content.answers.validateKey,
publicKey: content.answers.publicKey
}, function (err, obj) {
if (obj) { APP.answers = obj; }
var answers = obj && obj.results;
if (answers) { APP.answers = answers; }
checkIntegrity(false);
updateForm(framework, content, true);
});
return;
}
refreshEndDateBanner();
var loggedIn = framework._.sfCommon.isLoggedIn();
if (!loggedIn && !content.answers.anonymous) {
UI.alert(Messages.form_anonymous_blocked);
}
// If the results are public and there is at least one doodle, fetch the results now
if (content.answers.privateKey && Object.keys(content.form).some(function (uid) {
return content.form[uid].type === "poll";
@ -1698,12 +1763,19 @@ define([
publicKey: content.answers.publicKey,
privateKey: content.answers.privateKey,
}, function (err, obj) {
if (obj) { APP.answers = obj; }
var answers = obj && obj.results;
if (answers) { APP.answers = answers; }
checkIntegrity(false);
var myAnswers;
if (user.curvePublic && obj && obj[user.curvePublic]) { // XXX ANONYMOUS
myAnswers = obj[user.curvePublic].msg;
var curve1 = user.curvePublic
var curve2 = obj && obj.myKey; // Anonymous answer key
if (answers) {
var myAnswersObj = answers[curve1] || answers[curve2] || undefined;
if (myAnswersObj) {
myAnswers = myAnswersObj.msg;
}
}
console.warn(obj);
updateForm(framework, content, false, myAnswers);
});
return;
@ -1726,8 +1798,8 @@ define([
});
framework.onContentUpdate(function (newContent) {
console.log(newContent);
content = newContent;
evOnChange.fire();
refreshEndDateBanner();
var answers, temp;
if (!APP.isEditor) { answers = getFormResults(); }

@ -63,6 +63,7 @@ define([
});
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) {
var myKeys = {};
var myFormKeys;
var CPNetflux;
var network;
nThen(function (w) {
@ -81,12 +82,19 @@ define([
}
});
}));
Cryptpad.getFormKeys(w(function (keys) {
myFormKeys = keys;
}));
Cryptpad.makeNetwork(w(function (err, nw) {
network = nw;
}));
}).nThen(function () {
if (!network) { return void cb({error: "E_CONNECT"}); }
if (myFormKeys.formSeed) {
myFormKeys = getAnonymousKeys(myFormKeys.formSeed, data.channel);
}
var keys = Utils.secret && Utils.secret.keys;
var crypto = Utils.Crypto.Mailbox.createEncryptor({
@ -105,7 +113,15 @@ define([
};
var results = {};
config.onReady = function () {
cb(results);
var myKey;
// If we have submitted an anonymous answer, retrieve it
if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) {
myKey = myFormKeys.curvePublic;
}
cb({
myKey: myKey,
results: results
});
network.disconnect();
};
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
@ -135,6 +151,10 @@ define([
answer = obj;
}));
}).nThen(function () {
if (answer.anonymous) {
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
myKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
}
Cryptpad.getHistoryRange({
channel: data.channel,
lastKnownHash: answer.hash,
@ -154,6 +174,16 @@ define([
});
});
var getAnonymousKeys = function (formSeed, channel) {
var array = Nacl.util.decodeBase64(formSeed + channel);
var hash = Nacl.hash(array);
var secretKey = Nacl.util.encodeBase64(hash.subarray(32));
var publicKey = Utils.Hash.getCurvePublicFromPrivate(secretKey);
return {
curvePrivate: secretKey,
curvePublic: publicKey,
};
};
sframeChan.on("Q_FORM_SUBMIT", function (data, cb) {
var box = data.mailbox;
var myKeys;
@ -162,7 +192,15 @@ define([
myKeys = keys;
}));
}).nThen(function () {
// XXX if we are a registered user (myKeys.curvePrivate exists), we may
// have already answered anonymously. We should send a "proof" to show
// that the existing anonymous answer are ours (using myKeys.formSeed).
// Even if we never answered anonymously, the keyPair would be unique to
// the current channel so it wouldn't leak anything.
if (data.anonymous) {
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
myKeys = getAnonymousKeys(myKeys.formSeed, box.channel);
}
var keys = Utils.secret && Utils.secret.keys;
myKeys.signingKey = keys.secondarySignKey;
@ -182,7 +220,8 @@ define([
Cryptpad.storeFormAnswer({
channel: box.channel,
hash: hash,
curvePrivate: ephemeral_private
curvePrivate: ephemeral_private,
anonymous: Boolean(data.anonymous)
});
cb({error: err, response: response, hash: hash});
});

Loading…
Cancel
Save