cryptpad/www/form/main.js

441 lines
20 KiB
JavaScript
Raw Normal View History

2021-05-20 08:43:29 +00:00
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'/common/dom-ready.js',
'/common/sframe-common-outer.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (nThen, ApiConfig, DomReady, SFCommonO) {
var Nacl = window.nacl;
2021-05-28 12:13:16 +00:00
var href, hash;
2021-05-20 08:43:29 +00:00
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
var obj = SFCommonO.initIframe(waitFor, true);
href = obj.href;
hash = obj.hash;
}).nThen(function (/*waitFor*/) {
var privateKey, publicKey;
2021-05-26 12:54:56 +00:00
var channels = {};
var getPropChannels = function () {
return channels;
};
2021-05-20 08:43:29 +00:00
var addData = function (meta, CryptPad, user, Utils) {
var keys = Utils.secret && Utils.secret.keys;
var parsed = Utils.Hash.parseTypeHash('pad', hash.slice(1));
2021-05-26 10:03:32 +00:00
if (parsed && parsed.auditorKey) {
meta.form_auditorKey = parsed.auditorKey;
meta.form_auditorHash = hash;
}
2021-06-14 12:00:09 +00:00
var formData = Utils.Hash.getFormData(Utils.secret);
if (!formData) { return; }
2021-05-20 14:20:15 +00:00
var validateKey = keys.secondaryValidateKey;
meta.form_answerValidateKey = validateKey;
2021-06-14 12:00:09 +00:00
publicKey = meta.form_public = formData.form_public;
privateKey = meta.form_private = formData.form_private;
meta.form_auditorHash = formData.form_auditorHash;
2021-05-20 08:43:29 +00:00
};
var addRpc = function (sframeChan, Cryptpad, Utils) {
2021-05-26 12:54:56 +00:00
sframeChan.on('EV_FORM_PIN', function (data) {
channels.answersChannel = data.channel;
2022-12-07 13:12:13 +00:00
Cryptpad.changeMetadata();
2021-05-26 12:54:56 +00:00
Cryptpad.getPadAttribute('answersChannel', function (err, res) {
// If already stored, don't pin it again
if (res && res === data.channel) { return; }
Cryptpad.pinPads([data.channel], function () {
Cryptpad.setPadAttribute('answersChannel', data.channel, function () {});
});
});
2021-09-24 14:31:26 +00:00
});
sframeChan.on('EV_EXPORT_SHEET', function (data) {
if (!data || !Array.isArray(data.content)) { return; }
sessionStorage.CP_formExportSheet = JSON.stringify(data);
var href = Utils.Hash.hashToHref('', 'sheet');
var a = window.open(href);
if (!a) { sframeChan.event('EV_POPUP_BLOCKED'); }
delete sessionStorage.CP_formExportSheet;
2021-05-26 12:54:56 +00:00
});
2021-05-31 16:23:25 +00:00
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,
};
};
2021-06-01 16:37:29 +00:00
var u8_slice = function (A, start, end) {
return new Uint8Array(Array.prototype.slice.call(A, start, end));
};
var u8_concat = function (A) {
var length = 0;
A.forEach(function (a) { length += a.length; });
var total = new Uint8Array(length);
var offset = 0;
A.forEach(function (a) {
total.set(a, offset);
offset += a.length;
});
return total;
};
var anonProof = function (channel, theirPub, anonKeys) {
var u8_plain = Nacl.util.decodeUTF8(channel);
var u8_nonce = Nacl.randomBytes(Nacl.box.nonceLength);
var u8_cipher = Nacl.box(
u8_plain,
u8_nonce,
Nacl.util.decodeBase64(theirPub),
Nacl.util.decodeBase64(anonKeys.curvePrivate)
);
var u8_bundle = u8_concat([
u8_nonce, // 24 uint8s
u8_cipher, // arbitrary length
]);
return {
key: anonKeys.curvePublic,
proof: Nacl.util.encodeBase64(u8_bundle)
};
};
var checkAnonProof = function (proofObj, channel, curvePrivate) {
var pub = proofObj.key;
var proofTxt = proofObj.proof;
try {
var u8_bundle = Nacl.util.decodeBase64(proofTxt);
var u8_nonce = u8_slice(u8_bundle, 0, Nacl.box.nonceLength);
var u8_cipher = u8_slice(u8_bundle, Nacl.box.nonceLength);
var u8_plain = Nacl.box.open(
u8_cipher,
u8_nonce,
Nacl.util.decodeBase64(pub),
Nacl.util.decodeBase64(curvePrivate)
);
2021-06-03 15:29:03 +00:00
return channel === Nacl.util.encodeUTF8(u8_plain);
} catch (e) {
console.error(e);
return false;
}
2021-06-01 16:37:29 +00:00
};
var deleteLines = false; // "false" to support old forms
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
var cb = Utils.Util.once(_cb);
var myKeys = {};
2021-05-31 15:19:14 +00:00
var myFormKeys;
var accessKeys;
var CPNetflux, Pinpad;
2021-05-20 08:43:29 +00:00
var network;
var noDriveAnswered = false;
2021-05-20 08:43:29 +00:00
nThen(function (w) {
require([
2021-09-06 15:49:14 +00:00
'chainpad-netflux',
'/common/pinpad.js',
], w(function (_CPNetflux, _Pinpad) {
2021-05-20 08:43:29 +00:00
CPNetflux = _CPNetflux;
Pinpad = _Pinpad;
2021-05-20 08:43:29 +00:00
}));
var personalDrive = !Cryptpad.initialTeam || Cryptpad.initialTeam === -1;
2021-05-20 08:43:29 +00:00
Cryptpad.getAccessKeys(w(function (_keys) {
2021-05-21 11:39:33 +00:00
if (!Array.isArray(_keys)) { return; }
accessKeys = _keys;
2021-05-21 11:39:33 +00:00
_keys.some(function (_k) {
if ((personalDrive && !_k.id) || Cryptpad.initialTeam === Number(_k.id)) {
2021-05-21 11:39:33 +00:00
myKeys = _k;
return true;
}
});
2021-05-20 08:43:29 +00:00
}));
2021-05-31 15:19:14 +00:00
Cryptpad.getFormKeys(w(function (keys) {
if (!keys.curvePublic && !keys.formSeed) {
// No drive mode
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
noDriveAnswered = answered.indexOf(data.channel) !== -1;
}
2021-05-31 15:19:14 +00:00
myFormKeys = keys;
}));
2021-05-20 08:43:29 +00:00
Cryptpad.makeNetwork(w(function (err, nw) {
network = nw;
}));
2022-10-25 09:01:01 +00:00
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
if (md && md.deleteLines) { deleteLines = true; }
}));
2021-05-28 12:13:16 +00:00
}).nThen(function () {
2021-05-20 08:43:29 +00:00
if (!network) { return void cb({error: "E_CONNECT"}); }
2021-05-31 15:19:14 +00:00
if (myFormKeys.formSeed) {
myFormKeys = getAnonymousKeys(myFormKeys.formSeed, data.channel);
}
2021-05-20 08:43:29 +00:00
var keys = Utils.secret && Utils.secret.keys;
2021-06-01 16:37:29 +00:00
var curvePrivate = privateKey || data.privateKey;
2021-06-14 13:00:01 +00:00
if (!curvePrivate) { return void cb({error: 'EFORBIDDEN'}); }
2021-05-20 08:43:29 +00:00
var crypto = Utils.Crypto.Mailbox.createEncryptor({
2021-06-01 16:37:29 +00:00
curvePrivate: curvePrivate,
2021-05-21 11:39:33 +00:00
curvePublic: publicKey || data.publicKey,
validateKey: data.validateKey
2021-05-20 08:43:29 +00:00
});
var config = {
network: network,
channel: data.channel,
noChainPad: true,
validateKey: keys.secondaryValidateKey,
2021-05-26 12:54:56 +00:00
owners: [myKeys.edPublic],
2021-05-20 08:43:29 +00:00
crypto: crypto,
metadata: {
deleteLines: true
}
//Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers
2021-05-20 08:43:29 +00:00
};
2021-05-21 11:39:33 +00:00
var results = {};
config.onError = function (info) {
cb({ error: info.type });
};
config.onRejected = function (data, cb) {
if (!Array.isArray(data) || !data.length || data[0].length !== 16) {
return void cb(true);
}
if (!Array.isArray(accessKeys)) { return void cb(true); }
network.historyKeeper = data[0];
nThen(function (waitFor) {
accessKeys.forEach(function (obj) {
Pinpad.create(network, obj, waitFor(function (e) {
if (e) { console.error(e); }
}));
});
}).nThen(function () {
cb();
});
};
2022-12-07 13:12:13 +00:00
config.onReady = function () {
2021-05-31 15:19:14 +00:00
var myKey;
// If we have submitted an anonymous answer, retrieve it
if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) {
myKey = myFormKeys.curvePublic;
}
cb({
noDriveAnswered: noDriveAnswered,
2021-05-31 15:19:14 +00:00
myKey: myKey,
results: results
});
2021-05-20 11:54:20 +00:00
network.disconnect();
2021-05-20 08:43:29 +00:00
};
2021-05-21 11:39:33 +00:00
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
var parsed = Utils.Util.tryParse(msg);
if (!parsed) { return; }
2022-11-30 16:30:24 +00:00
var uid = parsed._uid || '000';
// If we have a "non-anonymous" answer, it may be the edition of a
// previous anonymous answer. Check if a previous anonymous answer exists
// with the same uid and delete it.
2021-06-01 16:37:29 +00:00
if (parsed._proof) {
2021-06-03 15:29:03 +00:00
var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
2022-11-30 16:30:24 +00:00
var theirAnonKey = parsed._proof.key;
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
delete results[theirAnonKey][uid];
2021-06-01 16:37:29 +00:00
}
}
2022-10-25 09:01:01 +00:00
parsed._time = cfg && cfg.time;
if (deleteLines) { parsed._hash = hash; }
2022-11-30 16:30:24 +00:00
if (data.cantEdit && results[senderCurve]
&& results[senderCurve][uid]) { return; }
2022-10-25 09:01:01 +00:00
results[senderCurve] = results[senderCurve] || {};
results[senderCurve][uid] = {
2021-05-21 11:39:33 +00:00
msg: parsed,
hash: hash,
2021-06-08 14:56:40 +00:00
time: cfg && cfg.time
2021-05-21 11:39:33 +00:00
};
2021-05-20 08:43:29 +00:00
};
CPNetflux.start(config);
});
});
2021-05-20 14:20:15 +00:00
sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) {
2022-10-25 09:01:01 +00:00
var answers = [];
2021-05-20 14:20:15 +00:00
var myKeys;
nThen(function (w) {
Cryptpad.getFormKeys(w(function (keys) {
myKeys = keys;
}));
Cryptpad.getFormAnswer({channel: data.channel}, w(function (obj) {
2021-05-20 14:20:15 +00:00
if (!obj || obj.error) {
if (obj && obj.error === "ENODRIVE") {
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
if (answered.indexOf(data.channel) !== -1) {
cb({error:'EANSWERED'});
} else {
cb();
}
return void w.abort();
}
2021-05-20 14:20:15 +00:00
w.abort();
return void cb(obj);
}
2022-10-25 09:01:01 +00:00
// Get the latest edit per uid
var temp = {};
obj.forEach(function (ans) {
var uid = ans.uid || '000';
temp[uid] = ans;
});
answers = Object.values(temp);
2021-05-20 14:20:15 +00:00
}));
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
if (md && md.deleteLines) { deleteLines = true; }
}));
2021-05-28 12:13:16 +00:00
}).nThen(function () {
2022-10-25 09:01:01 +00:00
var n = nThen;
var err;
var all = {};
answers.forEach(function (answer) {
n = n(function(waitFor) {
var finalKeys = myKeys;
if (answer.anonymous) {
if (!myKeys.formSeed) {
err = 'ANONYMOUS_ERROR';
console.error('ANONYMOUS_ERROR', answer);
return;
}
finalKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
}
Cryptpad.getHistoryRange({
channel: data.channel,
lastKnownHash: answer.hash,
toHash: answer.hash,
}, waitFor(function (obj) {
if (obj && obj.error) { err = obj.error; return; }
var messages = obj.messages;
if (!messages.length) {
2023-02-01 17:02:03 +00:00
// TODO delete from drive.forms?
2022-10-25 09:01:01 +00:00
return;
}
if (obj.lastKnownHash !== answer.hash) { return; }
try {
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
validateKey: data.validateKey,
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
my_private: Nacl.util.decodeBase64(finalKeys.curvePrivate),
their_public: Nacl.util.decodeBase64(data.publicKey)
});
var parsed = JSON.parse(res.content);
parsed._isAnon = answer.anonymous;
parsed._time = messages[0].time;
if (deleteLines) { parsed._hash = answer.hash; }
var uid = parsed._uid || '000';
if (all[uid] && !all[uid]._isAnon) { parsed._isAnon = false; }
all[uid] = parsed;
} catch (e) {
err = e;
}
}));
}).nThen;
});
n(function () {
if (err) { return void cb({error: err}); }
cb(all);
2021-05-20 14:20:15 +00:00
});
});
});
var noDriveSeed = Utils.Hash.createChannelId();
2021-05-20 11:54:20 +00:00
sframeChan.on("Q_FORM_SUBMIT", function (data, cb) {
var box = data.mailbox;
var myKeys;
nThen(function (w) {
Cryptpad.getFormKeys(w(function (keys) {
// If formSeed doesn't exists, it means we're probably in noDrive mode.
// We can create a seed in localStorage.
if (!keys.formSeed) {
// No drive mode
keys = { formSeed: noDriveSeed };
}
2021-05-20 11:54:20 +00:00
myKeys = keys;
}));
2021-05-28 12:13:16 +00:00
}).nThen(function () {
2021-06-01 16:37:29 +00:00
var myAnonymousKeys;
2021-05-31 15:19:14 +00:00
if (data.anonymous) {
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
myKeys = getAnonymousKeys(myKeys.formSeed, box.channel);
2021-06-01 16:37:29 +00:00
} else {
myAnonymousKeys = getAnonymousKeys(myKeys.formSeed, box.channel);
2021-05-31 15:19:14 +00:00
}
2021-05-20 11:54:20 +00:00
var keys = Utils.secret && Utils.secret.keys;
myKeys.signingKey = keys.secondarySignKey;
2021-05-20 14:20:15 +00:00
var ephemeral_keypair = Nacl.box.keyPair();
var ephemeral_private = Nacl.util.encodeBase64(ephemeral_keypair.secretKey);
myKeys.ephemeral_keypair = ephemeral_keypair;
2021-06-01 16:37:29 +00:00
if (myAnonymousKeys) {
var proof = anonProof(box.channel, box.publicKey, myAnonymousKeys);
data.results._proof = proof;
}
2021-05-20 11:54:20 +00:00
var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys);
2022-10-25 09:01:01 +00:00
var uid = data.results._uid || Utils.Util.uid();
data.results._uid = uid;
2021-05-20 11:54:20 +00:00
var text = JSON.stringify(data.results);
var ciphertext = crypto.encrypt(text, box.publicKey);
2021-05-26 12:54:56 +00:00
var hash = ciphertext.slice(0,64);
2021-05-20 11:54:20 +00:00
Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [
box.channel,
ciphertext
], function (err, response) {
2021-05-20 14:20:15 +00:00
Cryptpad.storeFormAnswer({
2022-10-25 09:01:01 +00:00
uid: uid,
2021-05-20 14:20:15 +00:00
channel: box.channel,
hash: hash,
2021-05-31 15:19:14 +00:00
curvePrivate: ephemeral_private,
anonymous: Boolean(data.anonymous)
}, function () {
2022-10-06 15:12:23 +00:00
var res = data.results;
res._isAnon = data.anonymous;
res._time = +new Date();
if (deleteLines) { res._hash = hash; }
2022-10-06 15:12:23 +00:00
cb({
error: err,
response: response,
results: res
});
2021-05-20 14:20:15 +00:00
});
2021-05-20 11:54:20 +00:00
});
});
});
2022-10-18 09:12:37 +00:00
sframeChan.on("Q_FORM_DELETE_ALL_ANSWERS", function (data, cb) {
if (!data || !data.channel) { return void cb({error: 'EINVAL'}); }
Cryptpad.clearOwnedChannel(data, cb);
2022-10-18 09:12:37 +00:00
});
2022-10-06 15:12:23 +00:00
sframeChan.on("Q_FORM_DELETE_ANSWER", function (data, cb) {
if (!deleteLines) {
return void cb({error: 'EFORBIDDEN'});
}
2022-10-06 15:12:23 +00:00
Cryptpad.deleteFormAnswers(data, cb);
});
2022-11-30 17:41:16 +00:00
sframeChan.on("Q_FORM_MUTE", function (data, cb) {
if (!Utils.secret) { return void cb({error: 'EINVAL'}); }
Cryptpad.muteChannel(Utils.secret.channel, data.muted, cb);
});
2021-05-20 08:43:29 +00:00
};
SFCommonO.start({
addData: addData,
addRpc: addRpc,
//cache: true,
2021-05-20 08:43:29 +00:00
noDrive: true,
hash: hash,
href: href,
useCreationScreen: true,
2021-05-26 12:54:56 +00:00
messaging: true,
getPropChannels: getPropChannels
2021-05-20 08:43:29 +00:00
});
});
});