Merge branch 'msg' into staging

pull/1/head
yflory 8 years ago
commit 159d72f33b

@ -489,6 +489,10 @@ define([
return loadingScreen();
};
Pages['/friends/'] = Pages['/friends/index.html'] = function () {
return loadingScreen();
};
Pages['/pad/'] = Pages['/pad/index.html'] = function () {
return loadingScreen();
};
@ -501,5 +505,9 @@ define([
return loadingScreen();
};
Pages['/invite/'] = Pages['/invite/index.html'] = function () {
return loadingScreen();
};
return Pages;
});

@ -21,6 +21,7 @@
.slideColor { color: @toolbar-slide-bg; }
.pollColor { color: @toolbar-poll-bg; }
.fileColor { color: @toolbar-file-bg; }
.friendsColor { color: @toolbar-friends-bg; }
.whiteboardColor { color: @toolbar-whiteboard-bg; }
.driveColor { color: @toolbar-drive-bg; }
.defaultColor { color: @toolbar-default-bg; }
@ -255,6 +256,11 @@ body {
@color: @toolbar-file-color;
.addToolbarColors(@color, @bgcolor);
}
&.app-friends {
@bgcolor: @toolbar-friends-bg;
@color: @toolbar-friends-color;
.addToolbarColors(@color, @bgcolor);
}
}

@ -97,6 +97,8 @@
@toolbar-drive-color: #fff;
@toolbar-file-bg: #cd2532;
@toolbar-file-color: #fff;
@toolbar-friends-bg: #607B8D;
@toolbar-friends-color: #fff;
@toolbar-default-bg: #ddd;
@toolbar-default-color: #000;

@ -12,7 +12,7 @@ $(function () {
var Messages = Cryptpad.Messages;
var $body = $('body');
var isMainApp = function () {
return /^\/(pad|code|slide|poll|whiteboard|file|media|drive)\/$/.test(location.pathname);
return /^\/(pad|code|slide|poll|whiteboard|file|media|friends|drive)\/$/.test(location.pathname);
};
var rightLink = function (ref, loc, txt) {
@ -148,6 +148,9 @@ $(function () {
} else if (/file/.test(pathname)) {
$('body').append(h('body', Pages[pathname]()).innerHTML);
require([ '/file/main.js' ], ready);
} else if (/friends/.test(pathname)) {
$('body').append(h('body', Pages[pathname]()).innerHTML);
require([ '/friends/main.js' ], ready);
} else if (/pad/.test(pathname)) {
$('body').append(h('body', Pages[pathname]()).innerHTML);
require([ '/pad/main.js' ], ready);
@ -183,6 +186,8 @@ $(function () {
} else if (/^\/($|^\/index\.html$)/.test(pathname)) {
// TODO use different top bar
require([ '/customize/main.js', ], function () {});
} else if (/invite/.test(pathname)) {
require([ '/invite/main.js'], function () {});
} else {
require([ '/customize/main.js', ], function () {});
}

@ -256,6 +256,11 @@ define(function () {
out.profile_description = "Description";
out.profile_fieldSaved = 'New value saved: {0}';
out.profile_inviteButton = "Connect";
out.profile_inviteButtonTitle ='Create a link that will invite this user to connect with you.';
out.profile_inviteExplanation = "Clicking <strong>OK</strong> will create a link to a secure messaging session that <em>only {0} will be able to redeem.</em><br><br>The link will be copied to your clipboard and can be shared publicly.";
out.profile_viewMyProfile = "View my profile";
// File manager
out.fm_rootName = "Documents";

@ -54,7 +54,7 @@ Version 1
if (!hash) { return; }
var parsed = {};
var hashArr = fixDuplicateSlashes(hash).split('/');
if (['media', 'file', 'user'].indexOf(type) === -1) {
if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) {
parsed.type = 'pad';
if (hash.slice(0,1) !== '/' && hash.length >= 56) {
// Old hash
@ -93,6 +93,16 @@ Version 1
}
return parsed;
}
if (['invite'].indexOf(type) !== -1) {
parsed.type = 'invite';
if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1;
parsed.channel = hashArr[2];
parsed.pubkey = hashArr[3].replace(/-/g, '/');
return parsed;
}
return parsed;
}
return;
};
var parsePadUrl = Hash.parsePadUrl = function (href) {
@ -320,5 +330,11 @@ Version 1
return hash;
};
Hash.createInviteUrl = function (curvePublic, channel) {
channel = channel || Hash.createChannelId();
return window.location.origin + '/invite/#/1/' + channel +
'/' + curvePublic.replace(/\//g, '-') + '/';
};
return Hash;
});

@ -0,0 +1,444 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/common/curve.js'
], function ($, Crypto, Curve) {
var Msg = {};
var Types = {
message: 'MSG'
};
// TODO: pin the chat channel!
// TODO: new Types
// - send a rename message to the chat
// - petnames
// - close a chat / remove a friend
// TODO
// - mute a channel (hide notifications or don't open it?)
//
var pending = {};
var createData = Msg.createData = function (common, hash) {
var proxy = common.getProxy();
return {
channel: hash || common.createChannelId(),
displayName: proxy[common.displayNameKey],
profile: proxy.profile.view,
edPublic: proxy.edPublic,
curvePublic: proxy.curvePublic,
avatar: proxy.profile.avatar
};
};
var getFriend = function (common, pubkey) {
var proxy = common.getProxy();
if (pubkey === proxy.edPublic) {
var data = createData(common);
delete data.channel;
return data;
}
return proxy.friends ? proxy.friends[pubkey] : undefined;
};
var getFriendList = Msg.getFriendList = function (common) {
var proxy = common.getProxy();
return proxy.friends || {};
};
Msg.getFriendChannelsList = function (common) {
var friends = getFriendList(common);
var list = [];
Object.keys(friends).forEach(function (key) {
list.push(friends[key].channel);
});
return list;
};
// Messaging tools
var avatars = {};
Msg.getFriendListUI = function (common, open) {
var proxy = common.getProxy();
var $block = $('<div>');
var friends = proxy.friends || {};
Object.keys(friends).forEach(function (f) {
var data = friends[f];
var $friend = $('<div>', {'class': 'friend'}).appendTo($block);
$friend.data('key', f);
var $rightCol = $('<span>', {'class': 'right-col'});
$('<span>', {'class': 'name'}).text(data.displayName).appendTo($rightCol);
$friend.dblclick(function () {
window.open('/profile/#' + data.profile);
});
$friend.click(function () {
open(data.edPublic);
});
if (data.avatar && avatars[data.avatar]) {
$friend.append(avatars[data.avatar]);
$friend.append($rightCol);
} else {
common.displayAvatar($friend, data.avatar, data.displayName, function ($img) {
if (data.avatar && $img) {
avatars[data.avatar] = $img[0].outerHTML;
}
$friend.append($rightCol);
});
}
});
return $block;
};
Msg.createOwnedChannel = function (common, channelId, validateKey, owners, cb) {
var network = common.getNetwork();
network.join(channelId).then(function (wc) {
var cfg = {
validateKey: validateKey,
owners: owners
};
var msg = ['GET_HISTORY', wc.id, cfg];
network.sendto(network.historyKeeper, JSON.stringify(msg)).then(cb, function (err) {
throw new Error(err);
});
}, function (err) {
throw new Error(err);
});
};
var channels = Msg.channels = window.channels = {};
var pushMsg = function (channel, cryptMsg) {
var msg = channel.encryptor.decrypt(cryptMsg);
var parsedMsg = JSON.parse(msg);
if (parsedMsg[0] !== Types.message) { return; }
parsedMsg.shift();
channel.messages.push([cryptMsg.slice(0,64), parsedMsg]);
};
var onDirectMessage = function (msg, sender) {
if (sender !== Msg.hk) { return; }
var parsed = JSON.parse(msg);
if ((parsed.validateKey || parsed.owners) && parsed.channel) {
return;
}
if (parsed.state && parsed.state === 1 && parsed.channel) {
if (channels[parsed.channel]) {
// parsed.channel is Ready
// TODO: call a function that shows that the channel is ready? (remove a spinner, ...)
// channel[parsed.channel].ready();
channels[parsed.channel].ready = true;
}
return;
}
var chan = parsed[3];
if (!chan || !channels[chan]) { return; }
pushMsg(channels[chan], parsed[4]);
};
var onMessage = function (msg, sender, chan) {
if (!channels[chan.id]) { return; }
pushMsg(channels[chan.id], msg);
channels[chan.id].notify();
channels[chan.id].refresh();
};
var createChatBox = function (common, $container, edPublic) {
var data = getFriend(common, edPublic);
var proxy = common.getProxy();
var $header = $('<div>', {'class': 'header'}).appendTo($container);
$('<div>', {'class': 'messages'}).appendTo($container);
var $inputBlock = $('<div>', {'class': 'input'}).appendTo($container);
// Input
var channel = channels[data.channel];
var $input = $('<input>', {type: 'text'}).appendTo($inputBlock);
var send = function () {
if (!$input.val()) { return; }
var msg = [Types.message, proxy.edPublic, +new Date(), $input.val()];
var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr);
channel.wc.bcast(cryptMsg).then(function () {
$input.val('');
pushMsg(channel, cryptMsg);
channel.refresh();
}, function (err) {
console.error(err);
});
};
$('<button>').text('TODO: Send').appendTo($inputBlock).click(send); // XXX
$input.on('keypress', function (e) {
if (e.which === 13) { send(); }
});
// Header
var $rightCol = $('<span>', {'class': 'right-col'});
$('<span>', {'class': 'name'}).text(data.displayName).appendTo($rightCol);
if (data.avatar && avatars[data.avatar]) {
$header.append(avatars[data.avatar]);
$header.append($rightCol);
} else {
common.displayAvatar($header, data.avatar, data.displayName, function ($img) {
if (data.avatar && $img) {
avatars[data.avatar] = $img[0].outerHTML;
}
$header.append($rightCol);
});
}
};
Msg.init = function (common, $listContainer, $msgContainer) {
var network = common.getNetwork();
var proxy = common.getProxy();
Msg.hk = network.historyKeeper;
var friends = getFriendList(common);
network.on('message', onDirectMessage);
// Refresh the active channel
var refresh = function (edPublic) {
if (Msg.active !== edPublic) { return; }
var data = friends[edPublic];
var channel = channels[data.channel];
if (!channel) { return; }
var $chat = $msgContainer.find('.chat').filter(function (idx, el) {
return $(el).data('key') === edPublic;
});
if (!$chat.length) { return; }
// Add new messages
var messages = channel.messages;
var $messages = $chat.find('.messages');
var $msg, msg, date, name;
var last = channel.lastDisplayed || -1;
for (var i = last + 1; i<messages.length; i++) {
msg = messages[i][1]; // 0 is the hash, 1 the array
$msg = $('<div>', {'class': 'message'}).appendTo($messages);
// date
date = msg[1] ? new Date(msg[1]).toLocaleString() : '?';
//$('<div>', {'class':'date'}).text(date).appendTo($msg);
$msg.attr('title', date);
// name
if (msg[0] !== channel.lastSender) {
name = getFriend(common, msg[0]).displayName;
$('<div>', {'class':'sender'}).text(name).appendTo($msg);
}
channel.lastSender = msg[0];
// content
$('<div>', {'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 = $('<div>', {'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;
});

@ -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();

@ -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();

@ -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;
});

@ -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;
});

@ -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();

@ -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

@ -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);
});

@ -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 = $('<span>', {'class': 'name'}).text(name);
var $span = $('<span>', {'title': name, 'class': 'avatar'});
var $rightCol = $('<span>', {'class': 'right-col'});
$('<span>', {'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 = $('<button>', {'class': 'friend'}).appendTo($rightCol);
$button.text('Add friend').click(function (e) {
e.stopPropagation();
Cryptpad.inviteFromUserlist(Cryptpad, data.netfluxId);
});
}
}
if (data.profile) {
$span.addClass('clickable');
$span.click(function () {
@ -216,13 +228,13 @@ define([
}
if (data.avatar && avatars[data.avatar]) {
$span.append(avatars[data.avatar]);
$span.append($name);
$span.append($rightCol);
} else {
Cryptpad.displayAvatar($span, data.avatar, name, function ($img) {
if (data.avatar && $img) {
avatars[data.avatar] = $img[0].outerHTML;
}
$span.append($name);
$span.append($rightCol);
});
}
$span.data('uid', data.uid);
@ -664,7 +676,7 @@ define([
$('<span>', {'class': 'bar3'}).appendTo($container);
$('<span>', {'class': 'bar4'}).appendTo($container);
$('<span>', {'class': 'disconnected fa fa-exclamation-circle'}).appendTo($a);
if (config.realtime) {
if (config.network) {
checkLag(toolbar, config, $a);
setInterval(function () {
if (!toolbar.connected) { return; }

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html class="cp pad">
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
</style>
</head>
<body>
<iframe id="pad-iframe"></iframe><script src="/common/noscriptfix.js"></script>

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<script async data-bootload="/friends/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
<style>.loading-hidden, .loading-hidden * {display: none !important;}</style>
</head>
<body class="loading-hidden">
<div id="toolbar" class="toolbar-container"></div>
<div id="app">
<div id="friendList"></div>
<div id="messaging"></div>
</div>
</body>
</html>

@ -0,0 +1,13 @@
define([
'jquery',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'less!/friends/main.less',
'less!/customize/src/less/toolbar.less',
], function ($) {
$('.loading-hidden').removeClass('loading-hidden');
// dirty hack to get rid the flash of the lock background
setTimeout(function () {
$('#app').addClass('ready');
}, 100);
});

@ -0,0 +1,51 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar2.js',
'/common/cryptpad-common.js',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less/cryptpad.less',
], function ($, Crypto, Toolbar, Cryptpad) {
//var Messages = Cryptpad.Messages;
var APP = window.APP = {
Cryptpad: Cryptpad
};
$(function () {
var andThen = function () {
Cryptpad.addLoadingScreen();
var ifrw = $('#pad-iframe')[0].contentWindow;
var $iframe = $('#pad-iframe').contents();
//var $appContainer = $iframe.find('#app');
var $list = $iframe.find('#friendList');
var $messages = $iframe.find('#messaging');
var $bar = $iframe.find('.toolbar-container');
var displayed = ['useradmin', 'newpad', 'limit', 'lag', 'spinner'];
var configTb = {
displayed: displayed,
ifrw: ifrw,
common: Cryptpad,
$container: $bar,
network: Cryptpad.getNetwork()
};
var toolbar = APP.toolbar = Toolbar.create(configTb);
toolbar.$rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar
Cryptpad.initMessaging(Cryptpad, $list, $messages);
Cryptpad.removeLoadingScreen();
};
Cryptpad.ready(function () {
andThen();
Cryptpad.reportAppUsage();
});
});
});

@ -0,0 +1,178 @@
@import "/customize/src/less/variables.less";
@import "/customize/src/less/mixins.less";
@button-border: 2px;
@bg-color: @toolbar-friends-bg;
@color: @toolbar-friends-color;
html, body {
margin: 0px;
height: 100%;
}
#toolbar {
display: flex; // We need this to remove a 3px border at the bottom of the toolbar
}
body {
display: flex;
flex-flow: column;
}
#app {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-height: 0;
}
#app.ready {
//background: url('/customize/bg3.jpg') no-repeat center center;
background-size: cover;
background-position: center;
}
.cryptpad-toolbar {
padding: 0px;
display: inline-block;
}
@keyframes example {
0% {
background: rgba(0,0,0,0.1);
}
50% {
background: rgba(0,0,0,0.3);
}
100% {
background: rgba(0,0,0,0.1);
}
}
#friendList {
width: 350px;
height: 100%;
background-color: lighten(@bg-color, 10%);
.friend {
background: rgba(0,0,0,0.1);
padding: 5px;
margin: 10px;
cursor: pointer;
&:hover {
background-color: rgba(0,0,0,0.3);
}
&.notify {
animation: example 2s ease-in-out infinite;
}
}
}
#friendList .friend, #messaging .header {
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
display: flex;
align-items: center;
cursor: pointer;
color: @color;
.default, media-tag {
display: inline-flex;
width: 50px;
height: 50px;
justify-content: center;
align-items: center;
margin-right: 5px;
border-radius: 10px / 6px;
overflow: hidden;
border: 1px solid black;
box-sizing: content-box;
}
.default {
.unselectable();
background: white;
color: black;
font-size: 40px;
}
.right-col {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
flex-flow: column;
.name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.friend {
padding: 0;
}
}
media-tag {
min-height: 50px;
min-width: 50px;
max-height: 50px;
max-width: 50px;
img {
color: #000;
min-width: 100%;
min-height: 100%;
max-width: none;
max-height: none; // To override 'media-tag img' in slide.less
}
}
}
#messaging {
flex: 1;
height: 100%;
background-color: lighten(@bg-color, 20%);
min-width: 0;
.header {
background-color: lighten(@bg-color, 15%);
padding: 10px;
}
.chat {
height: 100%;
display: flex;
flex-flow: column;
.messages {
padding: 0 20px;
margin: 10px 0;
flex: 1;
overflow-x: auto;
.message {
& > div {
padding: 0 10px;
}
.content {
overflow: hidden;
word-wrap: break-word;
}
.date {
display: none;
font-style: italic;
}
.sender {
margin-top: 10px;
font-weight: bold;
}
}
}
}
.input {
background-color: lighten(@bg-color, 15%);
height: 50px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 75px;
input {
flex: 1;
}
}
}

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html class="cp">
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
<head>
<title data-localization="main_title">Cryptpad: Zero Knowledge, Collaborative Real Time Editing</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/fold/foldgutter.css" />
</head>
<body class="html">
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</html>

@ -0,0 +1,86 @@
define([
'jquery',
'/common/cryptpad-common.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/common/curve.js',
'less!/invite/main.less',
], function ($, Cryptpad, Listmap, Curve) {
var APP = window.APP = {};
//var Messages = Cryptpad.Messages;
var onInit = function () {};
var onDisconnect = function () {};
var onChange = function () {};
var andThen = function () {
var hash = window.location.hash.slice(1);
var info = Cryptpad.parseTypeHash('invite', hash);
console.log(info);
if (!info.pubkey) {
Cryptpad.removeLoadingScreen();
Cryptpad.alert('invalid invite');
return;
}
var proxy = Cryptpad.getProxy();
var mySecret = proxy.curvePrivate;
var keys = Curve.deriveKeys(info.pubkey, mySecret);
var encryptor = Curve.createEncryptor(keys);
Cryptpad.removeLoadingScreen();
var listmapConfig = {
data: {},
network: Cryptpad.getNetwork(),
channel: info.channel,
readOnly: false,
validateKey: keys.validateKey,
crypto: encryptor,
userName: 'profile',
logLevel: 1,
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('create', onInit)
.on('ready', function () {
APP.initialized = true;
console.log(JSON.stringify(lm.proxy));
})
.on('disconnect', onDisconnect)
.on('change', [], onChange);
};
$(function () {
var $main = $('#mainBlock');
// Language selector
var $sel = $('#language-selector');
Cryptpad.createLanguageSelector(undefined, $sel);
$sel.find('button').addClass('btn').addClass('btn-secondary');
$sel.show();
// User admin menu
var $userMenu = $('#user-menu');
var userMenuCfg = {
$initBlock: $userMenu
};
var $userAdmin = Cryptpad.createUserAdminMenu(userMenuCfg);
$userAdmin.find('button').addClass('btn').addClass('btn-secondary');
$(window).click(function () {
$('.cryptpad-dropdown').hide();
});
// main block is hidden in case javascript is disabled
$main.removeClass('hidden');
APP.$container = $('#container');
Cryptpad.ready(function () {
Cryptpad.reportAppUsage();
andThen();
});
});
});

@ -0,0 +1,137 @@
.cp {
#mainBlock {
z-index: 1;
width: 1000px;
max-width: 90%;
margin: auto;
#container {
font-size: 25px;
width: 100%;
}
}
#header {
display: flex;
#rightside {
flex: 1;
display: flex;
flex-flow: column;
}
}
#avatar {
width: 300px;
//height: 350px;
margin: 10px;
margin-right: 20px;
text-align: center;
&> span {
display: inline-block;
text-align: center;
height: 300px;
width: 300px;
border: 1px solid black;
border-radius: 10px;
overflow: hidden;
position: relative;
.delete {
right: 0;
position: absolute;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
}
img {
max-width: 100%;
max-height: 100%;
vertical-align: top;
}
media-tag {
height: 100%;
width: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
img {
min-width: 100%;
min-height: 100%;
max-width: none;
max-height: none;
flex-shrink: 0;
}
}
button {
height: 40px;
margin: 5px;
}
}
#displayName, #link {
width: 100%;
height: 40px;
margin: 10px 0;
input {
width: 100%;
font-size: 20px;
box-sizing: border-box;
padding-right: 30px;
}
input:focus ~ .edit {
display: none;
}
.edit {
position: absolute;
margin-left: -25px;
margin-top: 8px;
}
.temp {
font-weight: 400;
font-family: sans-serif;
}
.displayName {
font-weight: bold;
font-size: 30px;
}
.displayName, .link {
line-height: 40px;
}
}
#description {
position: relative;
font-size: 16px;
border: 1px solid #DDD;
margin-bottom: 20px;
.rendered {
padding: 0 15px;
}
.ok, .spin {
position: absolute;
top: 2px;
right: 2px;
display: none;
z-index: 1000;
}
textarea {
width: 100%;
height: 300px;
}
.CodeMirror {
border: 1px solid #DDD;
font-family: monospace;
font-size: 16px;
line-height: initial;
pre {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
}
}
#createProfile {
height: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}
}

@ -91,6 +91,9 @@ define([
proxy.edPrivate = result.edPrivate;
proxy.edPublic = result.edPublic;
proxy.curvePrivate = result.curvePrivate;
proxy.curvePublic = result.curvePublic;
Cryptpad.feedback('LOGIN', true);
Cryptpad.whenRealtimeSyncs(result.realtime, function() {
Cryptpad.login(result.userHash, result.userName, function () {

@ -78,8 +78,11 @@ define([
var CREATE_ID = "createProfile";
var HEADER_ID = "header";
var HEADER_RIGHT_ID = "rightside";
var CREATE_INVITE_BUTTON = 'inviteButton';
var VIEW_PROFILE_BUTTON = 'viewProfileButton';
var createEditableInput = function ($block, name, ph, getValue, setValue, realtime) {
var createEditableInput = function ($block, name, ph, getValue, setValue, realtime, fallbackValue) {
fallbackValue = fallbackValue || ''; // don't ever display 'null' or 'undefined'
var lastVal;
getValue(function (value) {
lastVal = value;
@ -104,7 +107,7 @@ define([
if (err) { return void console.error(err); }
Cryptpad.whenRealtimeSyncs(realtime, function () {
lastVal = newVal;
Cryptpad.log(Messages._getKey('profile_fieldSaved', [newVal]));
Cryptpad.log(Messages._getKey('profile_fieldSaved', [newVal || fallbackValue]));
editing = false;
});
});
@ -150,8 +153,69 @@ define([
createEditableInput($block, DISPLAYNAME_ID, placeholder, 32, getValue, setValue, rt);
};
*/
var addCreateInviteLinkButton = function ($container) {
var obj = APP.lm.proxy;
var proxy = Cryptpad.getProxy();
var userViewHash = Cryptpad.find(proxy, ['profile', 'view']);
if (!APP.readOnly || !obj.curveKey || userViewHash === window.location.hash.slice(1)) {
console.log("edit mode or missing curve key, or you're viewing your own profile");
return;
}
// sanitize user inputs
var unsafeName = obj.name || '';
console.log(unsafeName);
var name = Cryptpad.fixHTML(unsafeName) || Messages.anonymous;
console.log(name);
console.log("Creating invite button");
var $button = $("<button>", {
id: CREATE_INVITE_BUTTON,
title: Messages.profile_inviteButtonTitle,
})
.addClass('btn btn-success')
.text(Messages.profile_inviteButton)
.click(function (e) {
Cryptpad.confirm(Messages._getKey('profile_inviteExplanation', [name]), function (yes) {
if (!yes) { return; }
console.log(obj.curveKey);
Cryptpad.alert("TODO");
// TODO create a listmap object using your curve keys
// TODO fill the listmap object with your invite data
// TODO generate link to invite object
// TODO copy invite link to clipboard
}, null, true);
})
.appendTo($container);
};
var addViewButton = function ($container) {
if (!Cryptpad.isLoggedIn() || window.location.hash) {
return;
}
var hash = Cryptpad.find(Cryptpad.getProxy(), ['profile', 'view']);
var url = '/profile/#' + hash;
var $button = $('<button>', {
'class': 'btn btn-success',
id: VIEW_PROFILE_BUTTON,
})
.text(Messages.profile_viewMyProfile)
.click(function () {
window.open(url, '_blank');
});
$container.append($button);
};
var addDisplayName = function ($container) {
var $block = $('<div>', {id: DISPLAYNAME_ID}).appendTo($container);
var getValue = function (cb) {
cb(APP.lm.proxy.name);
};
@ -161,6 +225,8 @@ define([
getValue(function (value) {
$span.text(value || Messages.anonymous);
});
addCreateInviteLinkButton($block);
return;
}
var setValue = function (value, cb) {
@ -168,7 +234,7 @@ define([
cb();
};
var rt = Cryptpad.getStore().getProxy().info.realtime;
createEditableInput($block, DISPLAYNAME_ID, placeholder, getValue, setValue, rt);
createEditableInput($block, DISPLAYNAME_ID, placeholder, getValue, setValue, rt, Messages.anonymous);
};
var addLink = function ($container) {
@ -330,9 +396,18 @@ define([
$container.append($block);
};
var onReady = function () {
APP.$container.find('#'+CREATE_ID).remove();
var obj = APP.lm && APP.lm.proxy;
if (!APP.readOnly) {
var pubKeys = Cryptpad.getPublicKeys();
if (pubKeys && pubKeys.curve) {
obj.curveKey = pubKeys.curve;
}
}
if (!APP.initialized) {
var $header = $('<div>', {id: HEADER_ID}).appendTo(APP.$container);
addAvatar($header);
@ -340,6 +415,7 @@ define([
addDisplayName($rightside);
addLink($rightside);
addDescription(APP.$container);
addViewButton(APP.$container); //$rightside);
addPublicKey(APP.$container);
APP.initialized = true;
}
@ -377,7 +453,7 @@ define([
if (obj.profile && obj.profile.view && obj.profile.edit) {
return void andThen(obj.profile.edit);
}
// If the user doesn't have a public profile, ask him if he wants to create one
// If the user doesn't have a public profile, ask them if they want to create one
var todo = function () {
var secret = Cryptpad.getSecrets();
obj.profile = {};

@ -58,7 +58,7 @@
min-height: 100%;
max-width: none;
max-height: none;
flex-shrink: 0;
flex: 1;
}
}
button {
@ -96,6 +96,15 @@
line-height: 40px;
}
}
// I tried using flexbox but messed with how the pencil icon was displayed
#inviteButton {
float: right;
}
#viewProfileButton {
margin-bottom: 20px;
float: right;
}
#description {
position: relative;
font-size: 16px;

@ -75,6 +75,8 @@ define([
var proxy = result.proxy;
proxy.edPublic = result.edPublic;
proxy.edPrivate = result.edPrivate;
proxy.curvePublic = result.curvePublic;
proxy.curvePrivate = result.curvePrivate;
Cryptpad.feedback('REGISTRATION', true);

Loading…
Cancel
Save