', {'class':'content'}).text(msg[2]).appendTo($msg);
+ }
+ $messages.scrollTop($messages[0].scrollHeight);
+ channel.lastDisplayed = i-1;
+ channel.unnotify();
+
+ if (messages.length > 10) {
+ var lastKnownMsg = messages[messages.length - 11];
+ data.lastKnownHash = lastKnownMsg[0];
+ }
+ };
+ // Display a new channel
+ var display = function (edPublic) {
+ var isNew = false;
+ var $chat = $msgContainer.find('.chat').filter(function (idx, el) {
+ return $(el).data('key') === edPublic;
+ });
+ if (!$chat.length) {
+ $chat = $('
', {'class':'chat'}).data('key', edPublic).appendTo($msgContainer);
+ createChatBox(common, $chat, edPublic);
+ isNew = true;
+ }
+ // Show the correct div
+ $msgContainer.find('.chat').hide();
+ $chat.show();
+
+ Msg.active = edPublic;
+
+ refresh(edPublic);
+ };
+
+ // Display friend list
+ common.getFriendListUI(common, display).appendTo($listContainer);
+
+ // Notify on new messages
+ var notify = function (edPublic) {
+ if (Msg.active === edPublic) { return; }
+ var $friend = $listContainer.find('.friend').filter(function (idx, el) {
+ return $(el).data('key') === edPublic;
+ });
+ $friend.addClass('notify');
+ };
+ var unnotify = function (edPublic) {
+ var $friend = $listContainer.find('.friend').filter(function (idx, el) {
+ return $(el).data('key') === edPublic;
+ });
+ $friend.removeClass('notify');
+ };
+
+ // Open the channels
+ Object.keys(friends).forEach(function (f) {
+ var data = friends[f];
+ var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate);
+ var encryptor = Curve.createEncryptor(keys);
+ channels[data.channel] = {
+ keys: keys,
+ encryptor: encryptor,
+ messages: [],
+ refresh: function () { refresh(data.edPublic); },
+ notify: function () { notify(data.edPublic); },
+ unnotify: function () { unnotify(data.edPublic); }
+ };
+ network.join(data.channel).then(function (chan) {
+ channels[data.channel].wc = chan;
+ chan.on('message', function (msg, sender) {
+ onMessage(msg, sender, chan);
+ });
+ var cfg = {
+ validateKey: keys.validateKey,
+ owners: [proxy.edPublic, data.edPublic],
+ lastKnownHash: data.lastKnownHash
+ };
+ var msg = ['GET_HISTORY', chan.id, cfg];
+ network.sendto(network.historyKeeper, JSON.stringify(msg))
+ .then($.noop, function (err) {
+ throw new Error(err);
+ });
+ }, function (err) {
+ console.error(err);
+ });
+ });
+ };
+
+ // Invitation
+
+ // Remove should be called from the friend app at the moment
+ // The other user will know it from the private channel ("REMOVE_FRIEND" message?)
+ Msg.removeFromFriendList = function (common, edPublic, cb) {
+ var proxy = common.getProxy();
+ if (!proxy.friends) {
+ return;
+ }
+ var friends = proxy.friends;
+ delete friends[edPublic];
+ common.whenRealtimeSyncs(common.getRealtime(), cb);
+ };
+
+ var addToFriendList = Msg.addToFriendList = function (common, data, cb) {
+ var proxy = common.getProxy();
+ if (!proxy.friends) {
+ proxy.friends = {};
+ }
+ var friends = proxy.friends;
+ var pubKey = data.edPublic;
+
+ if (pubKey === proxy.edPublic) { return void cb("E_MYKEY"); }
+ if (friends[pubKey]) { return void cb("E_EXISTS"); }
+
+ friends[pubKey] = data;
+ common.whenRealtimeSyncs(common.getRealtime(), function () {
+ common.pinPads([data.channel], cb);
+ });
+ common.changeDisplayName(proxy[common.displayNameKey]);
+ };
+
+ Msg.addDirectMessageHandler = function (common) {
+ var network = common.getNetwork();
+ if (!network) { return void console.error('Network not ready'); }
+ network.on('message', function (message, sender) {
+ var msg;
+ if (sender === network.historyKeeper) { return; }
+ try {
+ var parsed = common.parsePadUrl(window.location.href);
+ if (!parsed.hashData) { return; }
+ var chan = parsed.hashData.channel;
+ // Decrypt
+ var keyStr = parsed.hashData.key;
+ var cryptor = Crypto.createEditCryptor(keyStr);
+ var key = cryptor.cryptKey;
+ var decryptMsg = Crypto.decrypt(message, key);
+ // Parse
+ msg = JSON.parse(decryptMsg);
+ if (msg[1] !== parsed.hashData.channel) { return; }
+ var msgData = msg[2];
+ var msgStr;
+ if (msg[0] === "FRIEND_REQ") {
+ msg = ["FRIEND_REQ_NOK", chan];
+ var existing = getFriend(common, msgData.edPublic);
+ if (existing) {
+ msg = ["FRIEND_REQ_OK", chan, createData(common, existing.channel)];
+ msgStr = Crypto.encrypt(JSON.stringify(msg), key);
+ network.sendto(sender, msgStr);
+ return;
+ }
+ common.confirm("Accept friend?", function (yes) { // XXX
+ if (yes) {
+ pending[sender] = msgData;
+ msg = ["FRIEND_REQ_OK", chan, createData(common, msgData.channel)];
+ }
+ msgStr = Crypto.encrypt(JSON.stringify(msg), key);
+ network.sendto(sender, msgStr);
+ });
+ return;
+ }
+ if (msg[0] === "FRIEND_REQ_OK") {
+ // XXX
+ addToFriendList(common, msgData, function (err) {
+ if (err) {
+ return void common.log('Error while adding that friend to the list');
+ }
+ common.log('Friend invite accepted.');
+ var msg = ["FRIEND_REQ_ACK", chan];
+ var msgStr = Crypto.encrypt(JSON.stringify(msg), key);
+ network.sendto(sender, msgStr);
+ });
+ return;
+ }
+ if (msg[0] === "FRIEND_REQ_NOK") {
+ // XXX
+ common.log('Friend invite rejected');
+ return;
+ }
+ if (msg[0] === "FRIEND_REQ_ACK") {
+ // XXX
+ var data = pending[sender];
+ if (!data) { return; }
+ addToFriendList(common, data, function (err) {
+ if (err) {
+ return void common.log('Error while adding that friend to the list');
+ }
+ common.log('Friend added to the list.');
+ });
+ return;
+ }
+ // TODO: timeout ACK: warn the user
+ } catch (e) {
+ console.error("Cannot parse direct message", msg || message, "from", sender, e);
+ }
+ });
+ };
+
+ Msg.inviteFromUserlist = function (common, netfluxId) {
+ var network = common.getNetwork();
+ var parsed = common.parsePadUrl(window.location.href);
+ if (!parsed.hashData) { return; }
+ // Message
+ var chan = parsed.hashData.channel;
+ var myData = createData(common);
+ var msg = ["FRIEND_REQ", chan, myData];
+ // Encryption
+ var keyStr = parsed.hashData.key;
+ var cryptor = Crypto.createEditCryptor(keyStr);
+ var key = cryptor.cryptKey;
+ var msgStr = Crypto.encrypt(JSON.stringify(msg), key);
+ // Send encrypted message
+ network.sendto(netfluxId, msgStr);
+ };
+
+ return Msg;
+});
diff --git a/www/common/common-userlist.js b/www/common/common-userlist.js
index 2e1fe4adf..fc0f40931 100644
--- a/www/common/common-userlist.js
+++ b/www/common/common-userlist.js
@@ -52,7 +52,8 @@ define(function () {
name: exp.myUserName,
uid: Cryptpad.getUid(),
avatar: Cryptpad.getAvatarUrl(),
- profile: Cryptpad.getProfileUrl()
+ profile: Cryptpad.getProfileUrl(),
+ edPublic: Cryptpad.getProxy().edPublic
};
addToUserData(myData);
Cryptpad.setAttribute('username', exp.myUserName, function (err) {
@@ -81,7 +82,8 @@ define(function () {
name: "",
uid: Cryptpad.getUid(),
avatar: Cryptpad.getAvatarUrl(),
- profile: Cryptpad.getProfileUrl()
+ profile: Cryptpad.getProfileUrl(),
+ edPublic: Cryptpad.getProxy().edPublic
};
addToUserData(myData);
onLocal();
diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js
index 748aedf68..71c8dfcdb 100644
--- a/www/common/cryptpad-common.js
+++ b/www/common/cryptpad-common.js
@@ -10,6 +10,7 @@ define([
'/common/common-userlist.js',
'/common/common-title.js',
'/common/common-metadata.js',
+ '/common/common-messaging.js',
'/common/common-codemirror.js',
'/common/common-file.js',
'/file/file-crypto.js',
@@ -19,7 +20,7 @@ define([
'/customize/application_config.js',
'/common/media-tag.js',
], function ($, Config, Messages, Store, Util, Hash, UI, History, UserList, Title, Metadata,
- CodeMirror, Files, FileCrypto, Clipboard, Pinpad, AppConfig, MediaTag) {
+ Messaging, CodeMirror, Files, FileCrypto, Clipboard, Pinpad, AppConfig, MediaTag) {
/* This file exposes functionality which is specific to Cryptpad, but not to
any particular pad type. This includes functions for committing metadata
@@ -107,6 +108,17 @@ define([
common.findWeaker = Hash.findWeaker;
common.findStronger = Hash.findStronger;
common.serializeHash = Hash.serializeHash;
+ common.createInviteUrl = Hash.createInviteUrl;
+
+ // Messaging
+ common.initMessaging = Messaging.init;
+ common.addDirectMessageHandler = Messaging.addDirectMessageHandler;
+ common.inviteFromUserlist = Messaging.inviteFromUserlist;
+ common.createOwnedChannel = Messaging.createOwnedChannel;
+ common.getFriendList = Messaging.getFriendList;
+ common.getFriendChannelsList = Messaging.getFriendChannelsList;
+ common.getFriendListUI = Messaging.getFriendListUI;
+ common.createData = Messaging.createData;
// Userlist
common.createUserList = UserList.create;
@@ -148,6 +160,14 @@ define([
}
return;
};
+ common.getUserlist = function () {
+ if (store) {
+ if (store.getProxy() && store.getProxy().info) {
+ return store.getProxy().info.userList;
+ }
+ }
+ return;
+ };
common.getProfileUrl = function () {
if (store && store.getProfile()) {
return store.getProfile().view;
@@ -158,6 +178,12 @@ define([
return store.getProfile().avatar;
}
};
+ common.getDisplayName = function () {
+ if (getProxy()) {
+ return getProxy()[common.displayNameKey] || '';
+ }
+ return '';
+ };
var randomToken = function () {
return Math.random().toString(16).replace(/0./, '');
@@ -330,6 +356,21 @@ define([
typeof(proxy.edPublic) === 'string';
};
+ common.hasCurveKeys = function (proxy) {
+ return typeof(proxy) === 'object' &&
+ typeof(proxy.curvePrivate) === 'string' &&
+ typeof(proxy.curvePublic) === 'string';
+ };
+
+ common.getPublicKeys = function (proxy) {
+ proxy = proxy || common.getProxy();
+ if (!proxy || !proxy.edPublic || !proxy.curvePublic) { return; }
+ return {
+ curve: proxy.curvePublic,
+ ed: proxy.edPublic,
+ };
+ };
+
common.isArray = $.isArray;
/*
@@ -737,6 +778,11 @@ define([
if (avatarChan) { list.push(avatarChan); }
}
+ if (getProxy().friends) {
+ var fList = common.getFriendChannelsList(common);
+ list = list.concat(fList);
+ }
+
list.push(common.base64ToHex(userChannel));
list.sort();
@@ -1211,7 +1257,6 @@ define([
return button;
};
-
var emoji_patt = /([\uD800-\uDBFF][\uDC00-\uDFFF])/;
var isEmoji = function (str) {
return emoji_patt.test(str);
@@ -1731,6 +1776,8 @@ define([
Store.ready(function (err, storeObj) {
store = common.store = env.store = storeObj;
+ common.addDirectMessageHandler(common);
+
var proxy = getProxy();
var network = getNetwork();
diff --git a/www/common/curve-put.js b/www/common/curve-put.js
new file mode 100644
index 000000000..450b62564
--- /dev/null
+++ b/www/common/curve-put.js
@@ -0,0 +1,51 @@
+define([
+ '/common/curve.js',
+ '/bower_components/chainpad-listmap/chainpad-listmap.js',
+], function (Curve, Listmap) {
+ var Edit = {};
+
+ Edit.create = function (config, cb) { //network, channel, theirs, mine, cb) {
+ var network = config.network;
+ var channel = config.channel;
+ var keys = config.keys;
+
+ try {
+ var encryptor = Curve.createEncryptor(keys);
+ var lm = Listmap.create({
+ network: network,
+ data: {},
+ channel: channel,
+ readOnly: false,
+ validateKey: keys.validateKey || undefined,
+ crypto: encryptor,
+ userName: 'lol',
+ logLevel: 1,
+ });
+
+ var done = function () {
+ // TODO make this abort and disconnect the session after the
+ // user has finished making changes to the object, and they
+ // have propagated.
+ };
+
+ lm.proxy
+ .on('create', function () {
+ console.log('created');
+ })
+ .on('ready', function () {
+ console.log('ready');
+ cb(lm, done);
+ })
+ .on('disconnect', function () {
+ console.log('disconnected');
+ })
+ .on('change', [], function (o, n, p) {
+ console.log(o, n, p);
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ return Edit;
+});
diff --git a/www/common/curve.js b/www/common/curve.js
new file mode 100644
index 000000000..b33823fd5
--- /dev/null
+++ b/www/common/curve.js
@@ -0,0 +1,86 @@
+define([
+ '/bower_components/tweetnacl/nacl-fast.min.js',
+], function () {
+ var Nacl = window.nacl;
+ var Curve = {};
+
+ var concatenateUint8s = function (A) {
+ var len = 0;
+ var offset = 0;
+ A.forEach(function (uints) {
+ len += uints.length || 0;
+ });
+ var c = new Uint8Array(len);
+ A.forEach(function (x) {
+ c.set(x, offset);
+ offset += x.length;
+ });
+ return c;
+ };
+
+ var encodeBase64 = Nacl.util.encodeBase64;
+ var decodeBase64 = Nacl.util.decodeBase64;
+ var decodeUTF8 = Nacl.util.decodeUTF8;
+ var encodeUTF8 = Nacl.util.encodeUTF8;
+
+ Curve.encrypt = function (message, secret) {
+ var buffer = decodeUTF8(message);
+ var nonce = Nacl.randomBytes(24);
+ var box = Nacl.box.after(buffer, nonce, secret);
+ return encodeBase64(nonce) + '|' + encodeBase64(box);
+ };
+
+ Curve.decrypt = function (packed, secret) {
+ var unpacked = packed.split('|');
+ var nonce = decodeBase64(unpacked[0]);
+ var box = decodeBase64(unpacked[1]);
+ var message = Nacl.box.open.after(box, nonce, secret);
+ return encodeUTF8(message);
+ };
+
+ Curve.signAndEncrypt = function (msg, cryptKey, signKey) {
+ var packed = Curve.encrypt(msg, cryptKey);
+ return encodeBase64(Nacl.sign(decodeUTF8(packed), signKey));
+ };
+
+ Curve.openSigned = function (msg, cryptKey /*, validateKey STUBBED*/) {
+ var content = decodeBase64(msg).subarray(64);
+ return Curve.decrypt(encodeUTF8(content), cryptKey);
+ };
+
+ Curve.deriveKeys = function (theirs, mine) {
+ var pub = decodeBase64(theirs);
+ var secret = decodeBase64(mine);
+
+ var sharedSecret = Nacl.box.before(pub, secret);
+ var salt = decodeUTF8('CryptPad.signingKeyGenerationSalt');
+
+ // 64 uint8s
+ var hash = Nacl.hash(concatenateUint8s([salt, sharedSecret]));
+ var signKp = Nacl.sign.keyPair.fromSeed(hash.subarray(0, 32));
+ var cryptKey = hash.subarray(32, 64);
+
+ return {
+ cryptKey: encodeBase64(cryptKey),
+ signKey: encodeBase64(signKp.secretKey),
+ validateKey: encodeBase64(signKp.publicKey)
+ };
+ };
+
+ Curve.createEncryptor = function (keys) {
+ var cryptKey = decodeBase64(keys.cryptKey);
+ var signKey = decodeBase64(keys.signKey);
+ var validateKey = decodeBase64(keys.validateKey);
+
+ return {
+ encrypt: function (msg) {
+ return Curve.signAndEncrypt(msg, cryptKey, signKey);
+ },
+ decrypt: function (packed) {
+ return Curve.openSigned(packed, cryptKey, validateKey);
+ }
+ };
+ };
+
+ return Curve;
+});
diff --git a/www/common/fsStore.js b/www/common/fsStore.js
index b47d3c50e..48267e756 100644
--- a/www/common/fsStore.js
+++ b/www/common/fsStore.js
@@ -206,7 +206,8 @@ define([
}
// if the user is logged in, but does not have signing keys...
- if (Cryptpad.isLoggedIn() && !Cryptpad.hasSigningKeys(proxy)) {
+ if (Cryptpad.isLoggedIn() && (!Cryptpad.hasSigningKeys(proxy) ||
+ !Cryptpad.hasCurveKeys(proxy))) {
return void requestLogin();
}
@@ -218,8 +219,11 @@ define([
// Trigger userlist update when the avatar has changed
Cryptpad.changeDisplayName(proxy[Cryptpad.displayNameKey]);
});
+ proxy.on('change', ['friends'], function () {
+ // Trigger userlist update when the avatar has changed
+ Cryptpad.changeDisplayName(proxy[Cryptpad.displayNameKey]);
+ });
proxy.on('change', [tokenKey], function () {
- console.log('wut');
var localToken = tryParsing(localStorage.getItem(tokenKey));
if (localToken !== proxy[tokenKey]) {
return void requestLogin();
diff --git a/www/common/login.js b/www/common/login.js
index 004837ea1..fdb58c1d5 100644
--- a/www/common/login.js
+++ b/www/common/login.js
@@ -22,7 +22,12 @@ define([
// 16 bytes for a deterministic channel key
var channelSeed = dispense(16);
// 32 bytes for a curve key
- opt.curveSeed = dispense(32);
+ var curveSeed = dispense(32);
+
+ var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
+ opt.curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
+ opt.curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
+
// 32 more for a signing key
var edSeed = opt.edSeed = dispense(32);
@@ -109,6 +114,9 @@ define([
res.edPrivate = opt.edPrivate;
res.edPublic = opt.edPublic;
+ res.curvePrivate = opt.curvePrivate;
+ res.curvePublic = opt.curvePublic;
+
// they tried to just log in but there's no such user
if (!isRegister && isProxyEmpty(rt.proxy)) {
rt.network.disconnect(); // clean up after yourself
diff --git a/www/common/rpc.js b/www/common/rpc.js
index 0ce061dcf..de1b3b07e 100644
--- a/www/common/rpc.js
+++ b/www/common/rpc.js
@@ -200,7 +200,8 @@ types of messages:
return sendMsg(ctx, data, cb);
};
- network.on('message', function (msg) {
+ network.on('message', function (msg, sender) {
+ if (sender !== network.historyKeeper) { return; }
onMsg(ctx, msg);
});
@@ -304,7 +305,8 @@ types of messages:
}
};
- network.on('message', function (msg) {
+ network.on('message', function (msg, sender) {
+ if (sender !== network.historyKeeper) { return; }
onAnonMsg(ctx, msg);
});
diff --git a/www/common/toolbar2.js b/www/common/toolbar2.js
index 322a83fd0..c5d1279b5 100644
--- a/www/common/toolbar2.js
+++ b/www/common/toolbar2.js
@@ -148,7 +148,8 @@ define([
//if (user !== userNetfluxId) {
var data = userData[user] || {};
var userId = data.uid;
- if (!data.uid) { return; }
+ if (!userId) { return; }
+ data.netfluxId = user;
if (uids.indexOf(userId) === -1) {// && (!myUid || userId !== myUid)) {
uids.push(userId);
list.push(data);
@@ -206,8 +207,19 @@ define([
// Editors
editUsersNames.forEach(function (data) {
var name = data.name || Messages.anonymous;
- var $name = $('
', {'class': 'name'}).text(name);
var $span = $('', {'title': name, 'class': 'avatar'});
+ var $rightCol = $('', {'class': 'right-col'});
+ $('', {'class': 'name'}).text(name).appendTo($rightCol);
+ var proxy = Cryptpad.getProxy();
+ if (Cryptpad.isLoggedIn() && data.edPublic && data.edPublic !== proxy.edPublic) {
+ if (!proxy.friends || !proxy.friends[data.edPublic]) {
+ var $button = $('