diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js
index a5e75c6cf..80778864d 100644
--- a/NetfluxWebsocketSrv.js
+++ b/NetfluxWebsocketSrv.js
@@ -1,5 +1,6 @@
;(function () { 'use strict';
const Crypto = require('crypto');
+const Nacl = require('tweetnacl');
const LogStore = require('./storage/LogStore');
@@ -12,6 +13,7 @@ const USE_FILE_BACKUP_STORAGE = true;
let dropUser;
+let historyKeeperKeys = {};
const now = function () { return (new Date()).getTime(); };
@@ -25,6 +27,16 @@ const sendMsg = function (ctx, user, msg) {
}
};
+const storeMessage = function (ctx, channel, msg) {
+ ctx.store.message(channel.id, msg, function (err) {
+ if (err && typeof(err) !== 'function') {
+ // ignore functions because older datastores
+ // might pass waitFors into the callback
+ console.log("Error writing message: " + err);
+ }
+ });
+};
+
const sendChannelMessage = function (ctx, channel, msgStruct) {
msgStruct.unshift(0);
channel.forEach(function (user) {
@@ -33,13 +45,17 @@ const sendChannelMessage = function (ctx, channel, msgStruct) {
}
});
if (USE_HISTORY_KEEPER && msgStruct[2] === 'MSG') {
- ctx.store.message(channel.id, JSON.stringify(msgStruct), function (err) {
- if (err && typeof(err) !== 'function') {
- // ignore functions because older datastores
- // might pass waitFors into the callback
- console.log("Error writing message: " + err);
+ if (historyKeeperKeys[channel.id]) {
+ let signedMsg = msgStruct[4].replace(/^cp\|/, '');
+ signedMsg = Nacl.util.decodeBase64(signedMsg);
+ let validateKey = Nacl.util.decodeBase64(historyKeeperKeys[channel.id]);
+ let validated = Nacl.sign.open(signedMsg, validateKey);
+ if (!validated) {
+ console.log("Signed message rejected");
+ return;
}
- });
+ }
+ storeMessage(ctx, channel, JSON.stringify(msgStruct));
}
};
@@ -68,6 +84,7 @@ dropUser = function (ctx, user) {
if (chan.length === 0) {
console.log("Removing empty channel ["+chanName+"]");
delete ctx.channels[chanName];
+ delete historyKeeperKeys[chanName];
/* Call removeChannel if it is a function and channel removal is
set to true in the config file */
@@ -94,8 +111,15 @@ dropUser = function (ctx, user) {
const getHistory = function (ctx, channelName, handler, cb) {
var messageBuf = [];
+ var messageKey;
ctx.store.getMessages(channelName, function (msgStr) {
- messageBuf.push(JSON.parse(msgStr));
+ var parsed = JSON.parse(msgStr);
+ if (parsed.validateKey) {
+ historyKeeperKeys[channelName] = parsed.validateKey;
+ handler(parsed);
+ return;
+ }
+ messageBuf.push(parsed);
}, function (err) {
if (err) {
console.log("Error getting messages " + err.stack);
@@ -120,7 +144,7 @@ const getHistory = function (ctx, channelName, handler, cb) {
// no checkpoints.
for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); }
}
- cb();
+ cb(messageBuf);
});
};
@@ -166,7 +190,13 @@ const handleMessage = function (ctx, user, msg) {
sendMsg(ctx, user, [seq, 'ACK']);
getHistory(ctx, parsed[1], function (msg) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]);
- }, function () {
+ }, function (messages) {
+ // parsed[2] is a validation key if it exists
+ if (messages.length === 0 && parsed[2] && !historyKeeperKeys[parsed[1]]) {
+ var key = {channel: parsed[1], validateKey: parsed[2]};
+ storeMessage(ctx, ctx.channels[parsed[1]], JSON.stringify(key));
+ historyKeeperKeys[parsed[1]] = parsed[2];
+ }
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, 0]);
});
}
diff --git a/customize.dist/main.js b/customize.dist/main.js
index 3e9eab3b5..d9220a8d7 100644
--- a/customize.dist/main.js
+++ b/customize.dist/main.js
@@ -106,9 +106,17 @@ define([
});
});
+ var readOnly = false;
+ if (pad.href.indexOf('#') !== -1) {
+ var modeArray = pad.href.split('#')[1].split('/');
+ if (modeArray.length >= 3 && modeArray[2] === 'view') {
+ readOnly = true;
+ }
+ }
+ var readOnlyText = readOnly ? '(' + Messages.readonly + ') ' : '';
$row
.append($('
').text(name))
- .append($(' | ').append($('', {
+ .append($(' | ').append(readOnlyText).append($('', {
href: pad.href,
title: pad.title,
}).text(shortTitle)))
@@ -134,6 +142,9 @@ define([
if (hasRecent) {
$('table').attr('style', '');
+ // Race condition here, this is triggered before the localization in HTML
+ // so we have to remove the data-localization attr
+ $tryit.removeAttr('data-localization');
$tryit.text(Messages.recentPads);
}
});
diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css
index 61b592eda..b03039473 100644
--- a/customize.dist/toolbar.css
+++ b/customize.dist/toolbar.css
@@ -83,3 +83,8 @@
.cryptpad-spinner {
float: left;
}
+.cryptpad-readonly {
+ margin-right: 20px;
+ font-weight: bold;
+ text-transform: uppercase;
+}
\ No newline at end of file
diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js
index df37b3d63..6f5fc6c8d 100644
--- a/customize.dist/translations/messages.fr.js
+++ b/customize.dist/translations/messages.fr.js
@@ -3,6 +3,11 @@ define(function () {
out.main_title = "Cryptpad: Editeur collaboratif en temps réel, zero knowledge";
+ out.type = {};
+ out.type.pad = 'Pad';
+ out.type.code = 'Code';
+ out.type.poll = 'Sondage';
+ out.type.slide = 'Présentation';
out.errorBox_errorType_disconnected = 'Connexion perdue';
out.errorBox_errorExplanation_disconnected = [
@@ -20,6 +25,12 @@ define(function () {
out.synchronizing = 'Synchronisation';
out.reconnecting = 'Reconnexion...';
out.lag = 'Latence';
+ out.readonly = 'Lecture seule';
+ out.nobodyIsEditing = "Personne n'édite le document";
+ out.onePersonIsEditing = 'Une personne édite le document';
+ out.peopleAreEditing = '{0} personnes éditent le document';
+ out.oneViewer = '1 lecteur';
+ out.viewers = '{0} lecteurs';
out.importButton = 'IMPORTER';
out.importButtonTitle = 'Importer un document depuis un fichier local';
@@ -54,6 +65,10 @@ define(function () {
out.commitButton = 'VALIDER';
+ out.getViewButton = 'LECTURE SEULE';
+ out.getViewButtonTitle = "Obtenir l'adresse d'accès à ce document en lecture seule";
+ out.readonlyUrl = 'URL de lecture seule';
+
out.disconnectAlert = 'Perte de la connexion au réseau !';
out.tryIt = 'Essayez-le !';
@@ -65,12 +80,6 @@ define(function () {
out.loginText = ' Votre nom d\'utilisateur et votre mot de passe sont utilisés pour générer une clé unique qui reste inconnue de notre serveur. \n' +
'Faites attention de ne pas oublier vos identifiants puisqu\'ils seront impossible à récupérer. ';
- out.type = {};
- out.type.pad = 'Pad';
- out.type.code = 'Code';
- out.type.poll = 'Sondage';
- out.type.slide = 'Présentation';
-
out.forget = "Oublier";
// Polls
diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js
index 27c9af85d..f13409b72 100644
--- a/customize.dist/translations/messages.js
+++ b/customize.dist/translations/messages.js
@@ -3,6 +3,11 @@ define(function () {
out.main_title = "Cryptpad: Zero Knowledge, Collaborative Real Time Editing";
+ out.type = {};
+ out.type.pad = 'Pad';
+ out.type.code = 'Code';
+ out.type.poll = 'Poll';
+ out.type.slide = 'Presentation';
out.errorBox_errorType_disconnected = 'Connection Lost';
out.errorBox_errorExplanation_disconnected = [
@@ -20,6 +25,12 @@ define(function () {
out.synchronizing = 'Synchronizing';
out.reconnecting = 'Reconnecting...';
out.lag = 'Lag';
+ out.readonly = 'Read only';
+ out.nobodyIsEditing = 'Nobody is editing';
+ out.onePersonIsEditing = 'One person is editing';
+ out.peopleAreEditing = '{0} people are editing';
+ out.oneViewer = '1 viewer';
+ out.viewers = '{0} viewers';
out.importButton = 'IMPORT';
out.importButtonTitle = 'Import a document from a local file';
@@ -54,6 +65,10 @@ define(function () {
out.commitButton = 'COMMIT';
+ out.getViewButton = 'READ-ONLY URL';
+ out.getViewButtonTitle = 'Get the read-only URL for this document';
+ out.readonlyUrl = 'Read only URL';
+
out.disconnectAlert = 'Network connection lost!';
out.tryIt = 'Try it out!';
@@ -65,13 +80,6 @@ define(function () {
out.loginText = 'Your username and password are used to generate a unique key which is never known by our server. \n' +
'Be careful not to forget your credentials, as they are impossible to recover ';
- // TODO : move at the beginning
- out.type = {};
- out.type.pad = 'Pad';
- out.type.code = 'Code';
- out.type.poll = 'Poll';
- out.type.slide = 'Presentation';
-
out.forget = "Forget";
// Polls
diff --git a/package.json b/package.json
index c61266ee7..ec9dbf734 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,8 @@
"dependencies": {
"express": "~4.10.1",
"ws": "^1.0.1",
- "nthen": "~0.1.0"
+ "nthen": "~0.1.0",
+ "tweetnacl": "~0.12.2"
},
"devDependencies": {
"jshint": "~2.9.1",
diff --git a/www/code/main.js b/www/code/main.js
index b68fa5254..41e089e70 100644
--- a/www/code/main.js
+++ b/www/code/main.js
@@ -38,6 +38,10 @@ define([
var toolbar;
var secret = Cryptpad.getSecrets();
+ var readOnly = secret.keys && !secret.keys.editKeyStr;
+ if (!secret.keys) {
+ secret.keys = secret.key;
+ }
var andThen = function (CMeditor) {
var CodeMirror = module.CodeMirror = CMeditor;
@@ -105,6 +109,7 @@ define([
}());
var setEditable = module.setEditable = function (bool) {
+ if (readOnly && bool) { return; }
editor.setOption('readOnly', !bool);
};
@@ -131,7 +136,10 @@ define([
initialState: '{}',
websocketURL: Config.websocketURL,
channel: secret.channel,
- crypto: Crypto.createEncryptor(secret.key),
+ // our public key
+ validateKey: secret.keys.validateKey || undefined,
+ readOnly: readOnly,
+ crypto: Crypto.createEncryptor(secret.keys),
setMyID: setMyID,
transformFunction: JsonOT.validate
};
@@ -142,6 +150,7 @@ define([
var onLocal = config.onLocal = function () {
if (initializing) { return; }
+ if (readOnly) { return; }
editor.save();
var textValue = canonicalize($textarea.val());
@@ -275,12 +284,21 @@ define([
var config = {
userData: userList,
changeNameID: Toolbar.constants.changeName,
+ readOnly: readOnly
};
+ if (readOnly) {delete config.changeNameID; }
toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, info.userList, config);
- createChangeName(Toolbar.constants.changeName, $bar);
+ if (!readOnly) { createChangeName(Toolbar.constants.changeName, $bar); }
var $rightside = $bar.find('.' + Toolbar.constants.rightside);
+ var editHash;
+ var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
+
+ if (!readOnly) {
+ editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
+ }
+
/* add an export button */
var $export = $(' |