Compare commits

...

17 commits

Author SHA1 Message Date
yflory
3b5d3e9907 Revocation: destroy channel 2023-04-17 17:01:48 +02:00
yflory
06c172d9dc Fix issue with new templates 2023-04-17 14:26:33 +02:00
yflory
388729e376 Revocation: rotate keys 2023-04-14 16:17:20 +02:00
yflory
3c6819ec4a Revocation: share with user 2023-04-13 14:30:22 +02:00
yflory
b0d65139ce Revocation: fix creation issues and store moderator box 2023-04-12 16:17:58 +02:00
yflory
de448f0fab Revocation: safe links & multiple tabs 2023-04-12 11:35:52 +02:00
yflory
9156096629 Fix reconnect issue with multiple tabs of the same pad 2023-04-12 11:33:07 +02:00
yflory
1bf606cf49 Revocation: fix add access button 2023-04-11 15:20:53 +02:00
yflory
c0af9cb74d Revocation: store multiple accesses 2023-04-11 15:15:56 +02:00
yflory
519a8861f3 Revocation: enforce edit and view rights server side 2023-04-11 13:36:01 +02:00
yflory
21397022be Revocation: fix private window 2023-04-11 10:27:59 +02:00
yflory
aa9d4e950b Revocation: add/update/revoke access (clientside) 2023-04-06 18:53:41 +02:00
yflory
935cb34f4b Revocation: Update access part 1 2023-04-05 18:50:13 +02:00
yflory
95e8133905 Revocation: Open read-only pad from the drive 2023-04-03 15:30:32 +02:00
yflory
d6a9210190 Revocation: join pad 2023-03-31 18:12:46 +02:00
yflory
bb59b2a6d0 Revocation: pad creation 2023-03-29 17:07:05 +02:00
yflory
36b46e7fc9 Fix mailbox message deletion 2023-03-28 12:20:08 +02:00
34 changed files with 3101 additions and 140 deletions

View file

@ -2,6 +2,7 @@
@import (reference) "./variables.less";
@import (reference) "./browser.less";
@import (reference) "./markdown.less";
@import (reference) "./dropdown.less";
.modals-ui-elements_main() {
--LessLoader_require: LessLoader_currentFile();
@ -34,6 +35,42 @@
}
}
}
div.cp-share-access-list-container {
.dropdown_main();
.cp-dropdown-container {
position: unset;
}
div.cp-share-access-list {
display: flex;
flex-flow: column;
div.cp-share-access {
display: flex;
max-width: 100%;
align-items: center;
& > input {
margin-bottom: 0;
}
&:not(:last-child) {
margin-bottom: 10px;
}
& > *:not(:last-child) {
margin-right: 10px;
}
i {
width: 25px;
text-align: center;
}
.cp-share-access-id {
flex: 1;
min-width: 0;
margin-bottom: 0;
}
.cp-checkmark-mark {
margin-right: 0;
}
}
}
}
// Properties modal
.cp-app-prop {

View file

@ -7,6 +7,7 @@ const Core = require("./core");
const Metadata = require("./metadata");
const HK = require("../hk-util");
const Nacl = require("tweetnacl/nacl-fast");
const Revocable = require("../revocable");
Channel.disconnectChannelMembers = function (Env, Server, channelId, code, cb) {
var done = Util.once(Util.mkAsync(cb));
@ -68,7 +69,7 @@ Channel.clearOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
if (err) { return void cb(err); }
if (!Core.hasOwners(metadata)) { return void cb('E_NO_OWNERS'); }
// Confirm that the channel is owned by the user in question
if (!Core.isOwner(metadata, unsafeKey)) {
if (!Core.canDestroy(metadata, unsafeKey)) {
return void cb('INSUFFICIENT_PERMISSIONS');
}
return void Env.msgStore.clearChannel(channelId, function (e) {
@ -123,7 +124,7 @@ var archiveOwnedChannel = function (Env, safeKey, channelId, __cb, Server) {
Metadata.getMetadata(Env, channelId, function (err, metadata) {
if (err) { return void cb(err); }
if (!Core.hasOwners(metadata)) { return void cb('E_NO_OWNERS'); }
if (!Core.isOwner(metadata, unsafeKey)) {
if (!Core.canDestroy(metadata, unsafeKey)) {
return void cb('INSUFFICIENT_PERMISSIONS');
}
});
@ -185,7 +186,7 @@ Channel.trimHistory = function (Env, safeKey, data, cb) {
w.abort();
return void cb('E_NO_OWNERS');
}
if (!Core.isOwner(metadata, unsafeKey)) {
if (!Core.canDestroy(metadata, unsafeKey)) {
w.abort();
return void cb("INSUFFICIENT_PERMISSIONS");
}
@ -208,6 +209,22 @@ Channel.trimHistory = function (Env, safeKey, data, cb) {
});
};
Channel.onRevocationCommand = function (Env, obj, cb, Server, netfluxId) {
const data = obj.data;
const signature = obj.signature; // sign(string(line) + netfluxId, privKey)
const edPublic = obj.key;
// XXX REVOCATION move to workers (signature verification)
const signed = Revocable.checkLog([JSON.stringify(data), netfluxId], edPublic, signature);
if (!signed) { return void cb('INVALID_SIGNATURE_OR_PUBLIC_KEY'); }
if (data.command === "DESTROY") {
return void Channel.removeOwnedChannel(Env, edPublic, data.channel, cb, Server);
}
return void cb('UNSUPPORTED_COMMAND');
};
// Delete a signed mailbox message. This is used when users want
// to delete their form reponses.
Channel.deleteMailboxMessage = function (Env, data, cb) {
@ -224,7 +241,7 @@ Channel.deleteMailboxMessage = function (Env, data, cb) {
Env.msgStore.deleteChannelLine(channelId, hash, function (msg) {
// Check if you're allowed to delete this hash
try {
const mySecret = new Uint8Array(32);
const mySecret = Env.curvePrivate;
const msgBytes = Nacl.util.decodeBase64(msg).subarray(64); // Remove signature
const theirPublic = msgBytes.subarray(24,56); // 0-24 = nonce; 24-56=publickey (32 bytes)
const hashBytes = Nacl.box.open(proofBytes, nonce, theirPublic, mySecret);
@ -313,6 +330,20 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
metadata.restricted = true;
}
if (metadata && metadata.access) {
const session = HK.getNetfluxSession(Env, netfluxId);
const cfg = parsed[2];
if (cfg.signature) {
const c = Revocable.checkWrite(channelName, userId, signature, metadata);
if (c) {
return void HK.authenticateNetfluxSession(Env, netfluxId, signature.key);
}
}
// Send ERESTRICTED if not in access list
w.abort();
return void cb('INSUFFICIENT_PERMISSIONS');
}
if (!metadata || !metadata.restricted) {
return;
}
@ -320,7 +351,6 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
var session = HK.getNetfluxSession(Env, netfluxId);
var allowed = HK.listAllowedUsers(metadata);
if (HK.isUserSessionAllowed(allowed, session)) { return; }
w.abort();
@ -345,7 +375,7 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
// historyKeeper already knows how to handle metadata and message validation, so we just pass it off here
// if the message isn't valid it won't be stored.
Env.historyKeeper.channelMessage(Server, channelStruct, fullMessage, function (err) {
Env.historyKeeper.channelMessage(Server, channelStruct, netfluxId, fullMessage, function (err) {
if (err) {
// Message not stored...
return void cb(err);

View file

@ -10,7 +10,7 @@ Core.SESSION_EXPIRATION_TIME = 60 * 1000;
Core.isValidId = function (chan) {
return chan && chan.length && /^[a-zA-Z0-9=+-]*$/.test(chan) &&
[32, 33, 48].indexOf(chan.length) > -1;
[32, 33, 43, 48].indexOf(chan.length) > -1;
};
Core.isValidPublicKey = function (owner) {
@ -129,9 +129,21 @@ Core.isValidCookie = function (Sessions, publicKey, cookie) {
return true;
};
Core.isRevocable = function (metadata) {
return Boolean(metadata && metadata.access);
};
// E_NO_OWNERS
Core.hasOwners = function (metadata) {
return Boolean(metadata && Array.isArray(metadata.owners));
let hasModerators;
if (metadata.access) { // Revocable access
try {
hasModerators = Object.keys(metadata.access || {}).some((ed) => {
return metadata.access[ed].rights.includes('m');
});
} catch (e) {}
}
return Boolean(metadata && Array.isArray(metadata.owners)) || hasModerators;
};
Core.hasPendingOwners = function (metadata) {
@ -140,7 +152,22 @@ Core.hasPendingOwners = function (metadata) {
// INSUFFICIENT_PERMISSIONS
Core.isOwner = function (metadata, unsafeKey) {
return metadata.owners.indexOf(unsafeKey) !== -1;
let moderator;
if (metadata.access) { // Revocable access
try {
moderator = metadata.access[unsafeKey].rights.includes('m');
} catch (e) {}
}
return metadata.owners.indexOf(unsafeKey) !== -1 || moderator;
};
Core.canDestroy = function (metadata, unsafeKey) {
if (metadata.access) {
try {
return metadata.access[unsafeKey].rights.includes('d');
} catch (e) { return false; }
}
return Core.isOwner(metadata, unsafeKey);
};
Core.isPendingOwner = function (metadata, unsafeKey) {

View file

@ -5,11 +5,13 @@ const Meta = require("../metadata");
const Core = require("./core");
const Util = require("../common-util");
const HK = require("../hk-util");
const Revocable = require("../revocable");
Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) {
const cb = Util.once(Util.mkAsync(_cb));
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length !== HK.STANDARD_CHANNEL_LENGTH &&
channel.length !== HK.NEW_CHANNEL_LENGTH &&
channel.length !== HK.ADMIN_CHANNEL_LENGTH) { return cb("INVALID_CHAN_LENGTH"); }
// return synthetic metadata for admin broadcast channels as a safety net
@ -63,6 +65,37 @@ Data.getMetadata = function (Env, channel, cb, Server, netfluxId) {
});
};
Data.setRevocationMetadata = function (Env, obj, cb, Server, netfluxId) {
const data = obj.data;
const signature = obj.signature; // sign(string(line) + netfluxId, privKey)
const edPublic = obj.key;
// XXX REVOCATION move to workers (signature verification)
const signed = Revocable.checkLog([JSON.stringify(data), netfluxId], edPublic, signature);
if (!signed) { return void cb('INVALID_SIGNATURE_OR_PUBLIC_KEY'); }
let callback = cb;
if (data.command === "ROTATE_KEYS") {
// When sending a ROTATE_KEYS command, we also send a checkpoint for the current document
const cp = data.value.checkpoint;
delete data.value.checkpoint;
callback = function (err, md) {
if (err) { return void cb(err); }
// ROTATE_KEYS accepted, push patch
const msgStruct = [0, netfluxId, 'MSG', data.channel, cp];
const channelStruct = { id: data.channel };
Env.historyKeeper.channelMessage(Server, channelStruct, netfluxId, msgStruct, (err) => {
console.log('CHANMSG', err);
cb(err);
});
};
}
Data.setMetadata(Env, edPublic, data, callback, Server, true);
};
/* setMetadata
- write a new line to the metadata log if a valid command is provided
- data is an object: {
@ -71,14 +104,20 @@ Data.getMetadata = function (Env, channel, cb, Server, netfluxId) {
value: value
}
*/
Data.setMetadata = function (Env, safeKey, data, cb, Server) {
Data.setMetadata = function (Env, safeKey, data, cb, Server, revocation) {
var unsafeKey = Util.unescapeKeyCharacters(safeKey);
var channel = data.channel;
var command = data.command;
if (!channel || !Core.isValidId(channel)) { return void cb ('INVALID_CHAN'); }
if (!command || typeof (command) !== 'string') { return void cb('INVALID_COMMAND'); }
if (Meta.commands.indexOf(command) === -1) { return void cb('UNSUPPORTED_COMMAND'); }
if (!revocation && Meta.commands.indexOf(command) === -1) {
return void cb('UNSUPPORTED_COMMAND');
}
if (revocation && Meta.revocationCommands.indexOf(command) === -1) {
return void cb('UNSUPPORTED_COMMAND');
}
Env.queueMetadata(channel, function (next) {
Data.getMetadataRaw(Env, channel, function (err, metadata) {
@ -116,16 +155,20 @@ Data.setMetadata = function (Env, safeKey, data, cb, Server) {
// FIXME wacky fallthrough is hard to read
// we could pass this off to a writeMetadataCommand function
// and make the flow easier to follow
} else if (Core.isRevocable(metadata) && revocation) {
// FIXME fallthrough
} else if (!Core.isOwner(metadata, unsafeKey)) {
cb('INSUFFICIENT_PERMISSIONS');
return void next();
}
// Add the new metadata line
var line = [command, data.value, +new Date()];
var line = revocation ? [command, data.value, unsafeKey, +new Date()]
: [command, data.value, +new Date()];
var changed = false;
try {
changed = Meta.handleCommand(metadata, line);
changed = revocation ? Meta.handleRevocationCommand(metadata, line, unsafeKey)
: Meta.handleCommand(metadata, line);
} catch (e) {
cb(e);
return void next();
@ -165,6 +208,12 @@ Data.setMetadata = function (Env, safeKey, data, cb, Server) {
return void Server.channelBroadcast(channel, s_metadata, hk_id);
}
// XXX REVOCATION
// handle revocable channels here
// send error to users whose access has been revoked
// in the client, detect this error and check if they can reauthenticate using another key
// otherwise derive the list of users (unsafeKeys) that are allowed to stay
const allowed = HK.listAllowedUsers(metadata);
// anyone who is not allowed will get the same error message

View file

@ -13,6 +13,8 @@ const Util = require("./common-util");
const Package = require("../package.json");
const Path = require("path");
const Nacl = require("tweetnacl/nacl-fast");
var canonicalizeOrigin = function (s) {
if (typeof(s) === 'undefined') { return; }
return (s || '').trim().replace(/\/+$/, '');
@ -68,6 +70,8 @@ module.exports.create = function (config) {
permittedEmbedders = permittedEmbedders.trim();
}
const curve = Nacl.box.keyPair();
const Env = {
protocol: new URL(httpUnsafeOrigin).protocol,
@ -213,6 +217,9 @@ module.exports.create = function (config) {
lastEviction: +new Date(),
evictionReport: {},
commandTimers: {},
curvePrivate: curve.secretKey,
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
};
(function () {

View file

@ -7,6 +7,7 @@ const Store = require("./storage/file");
const BlobStore = require("./storage/blob");
const Workers = require("./workers/index");
const Core = require("./commands/core");
const Revocable = require("./revocable");
module.exports.create = function (Env, cb) {
const Log = Env.Log;
@ -18,18 +19,18 @@ module.exports.create = function (Env, cb) {
id: Env.id,
channelMessage: function (Server, channel, msgStruct, cb) {
channelMessage: function (Server, channel, userId, msgStruct, cb) {
// netflux-server emits 'channelMessage' events whenever someone broadcasts to a channel
// historyKeeper stores these messages if the channel id indicates that they are
// a channel type with permanent history
HK.onChannelMessage(Env, Server, channel, msgStruct, cb);
HK.onChannelMessage(Env, Server, channel, userId, msgStruct, cb);
},
channelClose: function (channelName) {
// netflux-server emits 'channelClose' events whenever everyone leaves a channel
// we drop cached metadata and indexes at the same time
HK.dropChannel(Env, channelName);
},
channelOpen: function (Server, channelName, userId, wait) {
channelOpen: function (Server, channelName, userId, wait, signature) {
Env.channel_cache[channelName] = Env.channel_cache[channelName] || {};
var sendHKJoinMessage = function () {
@ -51,7 +52,8 @@ module.exports.create = function (Env, cb) {
};
// only conventional channels can be restricted
if ((channelName || "").length !== HK.STANDARD_CHANNEL_LENGTH) {
if ((channelName || "").length !== HK.STANDARD_CHANNEL_LENGTH &&
(channelName || "").length !== HK.NEW_CHANNEL_LENGTH) {
return void cb();
}
@ -63,7 +65,18 @@ module.exports.create = function (Env, cb) {
error: err,
});
}
if (!metadata || (metadata && !metadata.restricted)) {
if (!metadata) { return void cb(); }
// New pad format: check signature when joining the channel
if (metadata.access) {
if (!signature) { return void ('ERESTRICTED'); }
let c = Revocable.checkRead(channelName, userId, signature, metadata);
if (c) { HK.authenticateNetfluxSession(Env, userId, signature.key); }
return void cb(!c ? 'ERESTRICTED' : undefined);
}
if (!metadata.restricted) {
// the channel doesn't have metadata, or it does and it's not restricted
// either way, let them join.
return void cb();
@ -71,6 +84,7 @@ module.exports.create = function (Env, cb) {
// this channel is restricted. verify that the user in question is in the allow list
// LEGACY allow list
// construct a definitive list (owners + allowed)
var allowed = HK.listAllowedUsers(metadata);
// and get the list of keys for which this user has already authenticated

View file

@ -4,6 +4,7 @@ var HK = module.exports;
const nThen = require('nthen');
const Util = require("./common-util");
const Revocable = require("./revocable");
const MetaRPC = require("./commands/metadata");
const Nacl = require('tweetnacl/nacl-fast');
const now = function () { return (new Date()).getTime(); };
@ -34,6 +35,7 @@ const getHash = HK.getHash = function (msg, Log) {
// historyKeeper should explicitly store any channel
// with a 32 character id
const STANDARD_CHANNEL_LENGTH = HK.STANDARD_CHANNEL_LENGTH = 32;
const NEW_CHANNEL_LENGTH = HK.NEW_CHANNEL_LENGTH = 43; // XXX array of valid length?
const ADMIN_CHANNEL_LENGTH = HK.ADMIN_CHANNEL_LENGTH = 33;
// historyKeeper should not store messages sent to any channel
@ -86,6 +88,16 @@ HK.getNetfluxSession = function (Env, netfluxId) {
return Env.netfluxUsers[netfluxId];
};
HK.isUserRevocableSessionAllowed = function (access, session, write) {
if (!session) { return false; }
const check = write ? Revocable.isEditor : Revocable.isViewer;
for (var unsafeKey in session) {
if (access[unsafeKey] && check(access[unsafeKey])) {
return true;
}
}
return false;
};
HK.isUserSessionAllowed = function (allowed, session) {
if (!session) { return false; }
for (var unsafeKey in session) {
@ -560,15 +572,41 @@ const handleRPC = function (Env, Server, seq, userId, parsed) {
we initialize that channel by writing the metadata supplied by the user to its log.
if the provided metadata has an expire time then we also create a task to expire it.
*/
const handleFirstMessage = function (Env, channelName, metadata) {
const handleFirstMessage = function (Env, channelName, metadata, userId, cb) {
// XXX MOVE TO WORKERS
if (metadata && metadata.revocableData) {
const data = metadata.revocableData;
// Check creator key
const msgCheck = [channelName, userId];
const creatorCheck = Revocable.checkLog(msgCheck, channelName, data.creationProof);
if (!creatorCheck) { return void cb('EVERIFY'); }
// Add first moderator and hash of keys to the log
delete metadata.revocableData;
const log = metadata.moderatorsLog = [];
const first = Revocable.firstLog(data.creatorVKey);
const second = Revocable.rotateLog(data.rotate.hash, data.rotate.validateKey,
data.rotate.uid, Revocable.hashMsg(first));
const check = Revocable.checkLog(second, data.creatorVKey, data.rotate.signature);
if (!check) { return void cb('EVERIFY'); }
Revocable.addSignatureLog(second, data.creatorVKey, data.rotate.signature);
log.push(first);
log.push(second);
HK.authenticateNetfluxSession(Env, userId, data.creatorVKey);
}
Env.store.writeMetadata(channelName, JSON.stringify(metadata), function (err) {
if (err) {
// FIXME tell the user that there was a channel error?
cb(err);
return void Env.Log.error('HK_WRITE_METADATA', {
channel: channelName,
error: err,
});
}
cb();
});
// write tasks
@ -704,16 +742,27 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) {
return;
}
if (msgCount === 0 && !metadata_cache[channelName] && Server.channelContainsUser(channelName, userId)) {
// TODO this might be a good place to reject channel creation by anonymous users
handleFirstMessage(Env, channelName, metadata);
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(metadata)]);
}
const endHistory = () => {
// End of history message:
let parsedMsg = {state: 1, channel: channelName, txid: txid};
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg)]);
};
if (msgCount === 0 && !metadata_cache[channelName] && Server.channelContainsUser(channelName, userId)) {
// TODO this might be a good place to reject channel creation by anonymous users
handleFirstMessage(Env, channelName, metadata, userId, (err) => {
if (err) {
return void Server.send(userId,
[seq, 'ERROR', 'METADATA_ERROR', HISTORY_KEEPER_ID]);
}
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(metadata)]);
endHistory();
});
return;
}
endHistory();
});
});
};
@ -844,6 +893,23 @@ HK.onDirectMessage = function (Env, Server, seq, userId, json) {
return;
}
if (metadata.access) {
const session = HK.getNetfluxSession(Env, userId);
const cfg = parsed[2];
if (cfg && cfg.signature) {
const c = Revocable.checkRead(channelName, userId, cfg.signature, metadata);
if (c) { return HK.authenticateNetfluxSession(Env, userId, cfg.signature.key); }
}
// Send ERESTRICTED if not in access list
w.abort();
return void Server.send(userId, [
seq,
'ERROR',
'ERESTRICTED',
HISTORY_KEEPER_ID
]);
}
// jump to handling the command if there's no restriction...
if (!metadata.restricted) { return; }
@ -855,6 +921,7 @@ HK.onDirectMessage = function (Env, Server, seq, userId, json) {
return;
}
/* Anyone in the userlist that isn't in the allow list should have already
been kicked out of the channel. Likewise, disallowed users should not
be able to add themselves to the userlist because JOIN commands respect
@ -894,7 +961,7 @@ HK.onDirectMessage = function (Env, Server, seq, userId, json) {
* adds timestamps to incoming messages
* writes messages to the store
*/
HK.onChannelMessage = function (Env, Server, channel, msgStruct, cb) {
HK.onChannelMessage = function (Env, Server, channel, userId, msgStruct, cb) {
if (typeof(cb) !== "function") { cb = function () {}; }
//console.log(+new Date(), "onChannelMessage");
@ -943,6 +1010,16 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct, cb) {
return void w.abort();
}
}));
}).nThen(function (w) {
// Check write access for revocable pads
// Do it here to avoid signature verification if forbidden
if (!metadata || !metadata.access) { return; }
const session = HK.getNetfluxSession(Env, userId);
const allowed = HK.isUserRevocableSessionAllowed(metadata.access, session, true);
if (!allowed) {
cb('EFORBIDDEN');
return void w.abort();
}
}).nThen(function (w) {
// if there's no validateKey present skip to the next block
if (!(metadata && metadata.validateKey)) { return; }

View file

@ -1,5 +1,6 @@
var Meta = module.exports;
var Core = require("./commands/core");
const Revocable = require("./revocable");
var deduplicate = require("./common-util").deduplicateString;
@ -33,9 +34,18 @@ the owners field is guaranteed to exist.
* RM_MAILBOX
* deleteLines <BOOLEAN>
* ALLOW_LINE_DELETION
Revocable channels
* access <MAP>
* UPDATE_ACCESS (add, update, revoke)
* moderatorsLog <ARRAY>
* UPDATE_ACCESS (when udpdating moderator rights)
* ADD_MODLOG_ENTRY
*/
var commands = {};
var revocationCommands = {};
var isValidPublicKey = Core.isValidPublicKey;
@ -388,6 +398,118 @@ commands.UPDATE_EXPIRATION = function () {
throw new Error("E_NOT_IMPLEMENTED");
};
// Revocation
/*
NOTE: requires signature when adding/removing moderator
["UPDATE_ACCESS", {
user: "7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I=",
access: {rights, etc.},
signature: signature
}, 1561623439989]
*/
revocationCommands.UPDATE_ACCESS = function (meta, args, myKey) {
if (!args || typeof(args) !== "object") {
throw new Error('METADATA_INVALID_ACCESS');
}
// Check revocable pad state
const access = meta.access;
const log = meta.moderatorsLog;
if (!access || !Array.isArray(log) || !log.length) { throw new Error("E_INVALID_CHAN_VERSION"); }
const old = JSON.stringify(access);
const myAccess = access[myKey];
if (!myAccess || !myAccess.rights) { throw new Error("E_FORBIDDEN"); }
const moderator = Revocable.isModerator(myAccess);
// Make sure you're allowed to applied this change
const user = args.user;
const signature = args.signature;
const newValue = args.access;
const oldValue = access[user];
const forbidden = Revocable.isAllowedAccessUpdate(access, myKey, oldValue, newValue);
if (moderator && forbidden) { throw new Error('INVALID_DATA'); }
if (forbidden) { throw new Error('E_FORBIDDEN'); }
// Allowed: Update moderators log if applicable
const wasModerator = Revocable.isModerator(oldValue);
const isModerator = Revocable.isModerator(newValue);
let newLog;
if (wasModerator && !isModerator) {
newLog = Revocable.removeLog(user, Revocable.hashMsg(log[log.length - 1]));
} else if (!wasModerator && isModerator) {
newLog = Revocable.addLog(user, Revocable.hashMsg(log[log.length - 1]));
}
if (newLog) {
const check = Revocable.checkLog(newLog, myKey, signature);
if (!check) { throw new Error('E_SIGNATURE_ERROR'); }
Revocable.addSignatureLog(newLog, myKey, signature);
log.push(newLog);
}
// Update metadata.access map
if (!newValue) { // Revoke
delete access[user];
} else if (!oldValue) { // Create
newValue.from = myKey;
access[user] = newValue;
return true;
} else { // Update
oldValue.rights = newValue.rights;
//oldValue.from = myKey; // XXX to decide: when updating an access, put it under your tree
// XXX for now keep it in the initial tree
}
return old !== JSON.stringify(access);
};
revocationCommands.ROTATE_KEYS = function (meta, args, myKey) {
const access = meta.access;
const log = meta.moderatorsLog;
if (!access || !Array.isArray(log) || !log.length) { throw new Error("E_INVALID_CHAN_VERSION"); }
const myAccess = access[myKey];
if (!myAccess || !myAccess.rights) { throw new Error("E_FORBIDDEN"); }
const moderator = Revocable.isModerator(myAccess);
if (!moderator) { throw new Error("E_FORBIDDEN"); }
if (!args || !args.notes || !args.hash || !args.uid || !args.validateKey
|| !args.signature) { throw new Error("E_MISSING_ARGS"); }
// Make sure all "access" values have updated data
const abort = Object.keys(access).some(function (key) {
return !args.notes[key] || !args.notes[key].mailbox || !args.notes[key].notes;
});
if (abort) { throw new Error('E_MISSING_DATA'); }
// Add ROTATE message to moderatorsLog
const prev = log[log.length - 1];
const msg = Revocable.rotateLog(args.hash, args.validateKey, args.uid, Revocable.hashMsg(prev));
const check = Revocable.checkLog(msg, myKey, args.signature);
if (!check) { throw new Error('E_SIGNATURE_ERROR'); }
Revocable.addSignatureLog(msg, myKey, args.signature);
log.push(msg);
// Update encrypted data in access (mailbox and notes)
Object.keys(access).forEach(function (key) {
const update = args.notes[key]; // we've already checked that it exists
access[key].mailbox = update.mailbox;
access[key].notes = update.notes;
});
// Update validateKey
meta.validateKey = args.validateKey;
return true;
};
revocationCommands.ADD_MODLOG_ENTRY = function (meta, args, user) {
throw new Error("E_NOT_IMPLEMENTED");
};
var handleCommand = Meta.handleCommand = function (meta, line) {
var command = line[0];
var args = line[1];
@ -399,7 +521,20 @@ var handleCommand = Meta.handleCommand = function (meta, line) {
return commands[command](meta, args);
};
var handleRevocationCommand = Meta.handleRevocationCommand = function (meta, line) {
var command = line[0];
var args = line[1];
var key = line[2];
//var time = line[2];
if (typeof(revocationCommands[command]) !== 'function') {
throw new Error("METADATA_UNSUPPORTED_COMMAND");
}
return revocationCommands[command](meta, args, key);
};
Meta.commands = Object.keys(commands);
Meta.revocationCommands = Object.keys(revocationCommands);
Meta.createLineHandler = function (ref, errorHandler) {
ref.meta = {};
@ -427,7 +562,12 @@ Meta.createLineHandler = function (ref, errorHandler) {
if (Array.isArray(line)) {
try {
var command = line[0];
if (Meta.commands.includes(commands)) {
handleCommand(ref.meta, line);
} else {
handleRevocationCommand(ref.meta, line);
}
} catch (err2) {
var code = err2.message;
if (ref.logged[code]) { return; }

1
lib/revocable.js Normal file
View file

@ -0,0 +1 @@
module.exports = require("../www/common/revocable");

View file

@ -22,7 +22,9 @@ const UNAUTHENTICATED_CALLS = {
WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
GET_METADATA: Metadata.getMetadata,
ADD_FIRST_ADMIN: Admin.addFirstAdmin
ADD_FIRST_ADMIN: Admin.addFirstAdmin,
REVOCATION_SET_METADATA: Metadata.setRevocationMetadata,
REVOCATION_COMMAND: Channel.onRevocationCommand
};
var isUnauthenticateMessage = function (msg) {

View file

@ -256,6 +256,7 @@ var serveBroadcast = makeRouteCache(function () {
return [
'define(function(){',
'return ' + JSON.stringify({
curvePublic: Env.curvePublic, // XXX could be in api/config but issue with static config
lastBroadcastHash: Env.lastBroadcastHash,
surveyURL: Env.surveyURL,
maintenance: maintenance

View file

@ -89,6 +89,16 @@ var factory = function (Util, Crypto, Keys, Nacl) {
}
return hash;
};
Hash.getRevocableHashFromKeys = function (type, secret, opts) {
opts = opts || {};
var pass = secret.password ? 'p/' : '';
var hash = '/5/' + type + '/' + secret.keys.seed + '/' + pass;
var hashData = Hash.parseTypeHash(type, hash);
if (hashData && hashData.getHash) {
return hashData.getHash(opts || {});
}
return hash;
};
var getFileHashFromKeys = Hash.getFileHashFromKeys = function (secret) {
var version = secret.version;
@ -180,6 +190,8 @@ Version 3: Safe links
Version 4: Data URL when not a realtime link yet (new pad or "static" app)
/login/#/4/login/newpad=eyJocmVmIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL2NvZGUvIy8yL2NvZGUvZWRpdC91NUFDdnhBWW1odkcwRnRyTm45RklRY2YvIn0%3D/
/drive/#/4/drive/login=e30%3D/
Version 5: Revocable mailbox
/code/#/5/code/xYGXUrZKq23mC+JGpyhLaadcnc7krfIRXatuleVrAZE/p/
*/
var getLoginOpts = function (hashArr) {
@ -306,8 +318,9 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
}
// Version >= 1: more hash options
var slice = 5;
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 5).join('/') + '/';
var hash = hashArr.slice(0, slice).join('/') + '/';
var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey;
if (owner) { hash += owner + '/'; }
if (parsed.password || opts.password) { hash += 'p/'; }
@ -359,6 +372,17 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
return parsed;
}
if (hashArr[1] && hashArr[1] === '5') { // Version 5: Revocable mailbox
parsed.version = 5;
parsed.app = hashArr[2];
parsed.key = hashArr[3];
slice = 4;
options = hashArr.slice(slice);
addOptions();
return parsed;
}
return parsed;
}
parsed.getHash = function () { return hashArr.join('/'); };
@ -494,6 +518,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
if (idx === -1) { return ret; }
ret.hash = href.slice(idx + 2);
ret.hashData = parseTypeHash(ret.type, ret.hash);
if (ret.hashData && ret.hashData.version === 5) { ret.revocable = true; }
return ret;
}
@ -506,6 +531,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
if (idx === -1) { return ret; }
ret.hash = href.slice(idx + 2);
ret.hashData = parseTypeHash(ret.type, ret.hash);
if (ret.hashData && ret.hashData.version === 5) { ret.revocable = true; }
return ret;
};
@ -524,11 +550,34 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
return '/' + parsed.type + '/#' + parsed.hash;
};
Hash.getRevocableSecret = function (data, password) {
var r = Crypto.createRevocable(data, password);
return {
keys: r,
revocable: true,
channel: r.channel,
}
};
/*
* Returns all needed keys for a realtime channel
* - no argument: use the URL hash or create one if it doesn't exist
* - secretHash provided: use secretHash to find the keys
*/
Hash.getRevocable = function (type, seedStr) {
var r = Crypto.createRevocableMailbox(seedStr);
return {
type: type,
version: 5,
keys: r,
channel: base64ToHex(r.chanId),
curvePublic: r.curvePublic,
curvePrivate: r.curvePrivate,
edPublic: r.edPublic,
edPrivate: r.edPrivate
};
};
Hash.getSecrets = function (type, secretHash, password) {
var secret = {};
var generate = function () {
@ -617,6 +666,14 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
} else if (parsed.type === "user") {
throw new Error("User hashes can't be opened (yet)");
}
} else if (parsed.version === 5) {
// New hash
secret.version = 5;
secret.type = type;
secret.password = password;
if (parsed.type !== "pad") { return secret; }
secret.keys = Crypto.createRevocableMailbox(parsed.key);
secret.channel = base64ToHex(secret.keys.chanId);
}
}
return secret;
@ -669,7 +726,8 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
Hash.hrefToHexChannelId = function (href, password) {
var parsed = Hash.parsePadUrl(href);
if (!parsed || !parsed.hash) { return; }
var secret = Hash.getSecrets(parsed.type, parsed.hash, password);
var secret = parsed.revocable ? Hash.getRevocable(parsed.type, parsed.hashData.key)
: Hash.getSecrets(parsed.type, parsed.hash, password);
return secret.channel;
};
@ -689,7 +747,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
};
Hash.isValidChannel = function (channelId) {
return /^[a-zA-Z0-9]{32,48}$/.test(channelId);
return /^[a-zA-Z0-9-+]{32,48}$/.test(channelId);
};
Hash.isValidHref = function (href) {

View file

@ -222,6 +222,10 @@ define([
}
onSelect();
});
} else if (config.picker) {
$div.on('click', '.cp-usergrid-user', function () {
onSelect($(this));
});
}
return {
@ -2591,14 +2595,12 @@ define([
icon: h('span.cptools.cptools-new-template')
});
}*/
if (!privateData.newTemplate) {
allData.unshift({
name: Messages.creation_noTemplate,
id: 0,
//icon: h('span.fa.fa-file')
icon: UI.getFileIcon({type: type})
});
}
var redraw = function (index) {
if (index < 0) { i = 0; }
else if (index > allData.length - 1) { return; }

View file

@ -76,6 +76,7 @@
handlers.push(cb);
},
unreg: function (cb) {
console.error(handlers, handlers.indexOf(cb), cb);
if (handlers.indexOf(cb) === -1) {
return void console.error("event handler was already unregistered");
}

View file

@ -219,10 +219,12 @@ define([
var n = Nthen;
var nacl, theirs;
n = n(function (waitFor) {
require(['/bower_components/tweetnacl/nacl-fast.min.js'], waitFor(function () {
require([
'/api/broadcast?'+ (+new Date()),
'/bower_components/tweetnacl/nacl-fast.min.js'
], waitFor(function (Broadcast) {
nacl = window.nacl;
var s = new Uint8Array(32);
theirs = nacl.box.keyPair.fromSecretKey(s);
theirs = nacl.util.decodeBase64(Broadcast.curvePublic);
}));
}).nThen;
var toDelete = [];
@ -236,7 +238,7 @@ define([
var curve = answer.curvePrivate;
var mySecret = nacl.util.decodeBase64(curve);
var nonce = nacl.randomBytes(24);
var proofBytes = nacl.box(h, nonce, theirs.publicKey, mySecret);
var proofBytes = nacl.box(h, nonce, theirs, mySecret);
var proof = nacl.util.encodeBase64(nonce) +'|'+ nacl.util.encodeBase64(proofBytes);
var lineData = {
channel: data.channel,
@ -366,6 +368,11 @@ define([
(function () {
var bypassHashChange = function (key) {
return function (value) {
if (currentPad.type && currentPad.type !== 'link' && /^\/5\//.test(value)) {
// XXX REVOCATION decide if we store user access before opening it
console.error('Preserve safe link for revocable access', key);
return;
}
var ohc = window.onhashchange;
window.onhashchange = function () {};
window.location[key] = value;
@ -746,11 +753,12 @@ define([
postMessage("SET_DISPLAY_NAME", value, cb);
};
common.setPadAttribute = function (attr, value, cb, href) {
common.setPadAttribute = function (attr, value, cb, href, channel) {
cb = cb || function () {};
href = Hash.getRelativeHref(href || currentPad.href);
postMessage("SET_PAD_ATTRIBUTE", {
href: href,
channel: channel,
attr: attr,
value: value
}, function (obj) {
@ -758,13 +766,14 @@ define([
cb();
});
};
common.getPadAttribute = function (attr, cb, href) {
common.getPadAttribute = function (attr, cb, href, channel) {
href = Hash.getRelativeHref(href || currentPad.href);
if (!href) {
if (!href && !channel) {
return void cb('E404');
}
postMessage("GET_PAD_ATTRIBUTE", {
href: href,
channel: channel,
attr: attr,
}, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
@ -881,7 +890,8 @@ define([
href: href,
title: data.title,
owners: optsPut.owners,
path: ['template']
path: ['template'],
revocable: false // XXX REVOCATION
}, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb();
@ -1097,7 +1107,7 @@ define([
common.setPadTitle = function (data, cb) {
if (!data || typeof (data) !== "object") { return cb ('Data is not an object'); }
var href = data.href || currentPad.href;
var href = data.href || data.accessHref || currentPad.href;
var parsed = Hash.parsePadUrl(href);
if (!parsed.hash) { return cb ('Invalid hash'); }
data.href = parsed.getUrl({present: parsed.present});
@ -1263,6 +1273,15 @@ define([
};
universal.onEvent = Util.mkEvent();
common.universalCommand = function (module, cmd, data, cb) {
universal.execCommand({
type: module,
data: {
cmd: cmd,
data: data
}
}, cb);
};
// Pad RPC
var pad = common.padRpc = {};

View file

@ -1176,7 +1176,7 @@ define([
return void logError("Missing data for the file", el, data);
}
var href = isRo ? data.roHref : (data.href || data.roHref);
var href = isRo ? (data.roHref || data.href) : (data.href || data.roHref);
var parsed = Hash.parsePadUrl(href);
if (parsed.hashData && parsed.hashData.type === 'file' && !app
@ -1186,14 +1186,21 @@ define([
var obj = { t: APP.team };
if (isRo && parsed.revocable) { obj.mode = 'view'; }
var priv = metadataMgr.getPrivateData();
var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']);
if (parsed.revocable) { useUnsafe = false; } // XXX
if (useUnsafe === true || APP.newSharedFolder) {
return void common.openURL(Hash.getNewPadURL(href, obj));
}
// Get hidden hash
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
var secret = parsed.revocable ? { key: true, channel: data.channel }
: Hash.getSecrets(parsed.type, parsed.hash, data.password);
var opts = {};
if (isRo) { opts.view = true; }
var hash = Hash.getHiddenHashFromKeys(parsed.type, secret, opts);
@ -4926,12 +4933,16 @@ define([
var auditorHash;
if (parsed.hash && parsed.type === "form") {
var formData = Hash.getFormData(null, parsed.hash, data.password);
console.log(formData);
if (formData) {
auditorHash = formData.form_auditorHash;
}
}
var rev;
if (parsed.revocable) {
rev = { channel:data.channel }
}
var roParsed = Hash.parsePadUrl(data.roHref);
var padType = parsed.type || roParsed.type;
var ro = !sf || (folders[el] && folders[el].version >= 2);
@ -4944,7 +4955,8 @@ define([
hashes: {
editHash: parsed.hash,
viewHash: ro && roParsed.hash,
fileHash: parsed.hash
fileHash: parsed.hash,
revocableData: rev
},
auditorHash: auditorHash,
fileData: {

View file

@ -36,6 +36,9 @@ define([
var data = {};
nThen(function (waitFor) {
var priv = common.getMetadataMgr().getPrivateData();
var hashes = opts.hashes || priv.hashes;
var base = priv.origin;
// this fetches attributes from your shared worker's memory
common.getPadAttribute('', waitFor(function (err, val) {
@ -70,11 +73,11 @@ define([
Util.extend(data, val);
if (data.href) { data.href = base + data.href; }
if (data.roHref) { data.roHref = base + data.roHref; }
}), opts.href);
}), opts.href, opts.channel);
if (opts.channel) { data.channel = opts.channel; }
// If this is a file, don't try to look for metadata
if (opts.channel && opts.channel.length > 32) { return; }
if (opts.channel && opts.channel.length > 32 && !opts.revocable) { return; }
// this fetches data from the server
Modal.loadMetadata(Env, data, waitFor);
}).nThen(function () {
@ -105,6 +108,12 @@ define([
}];
var tabs = [];
nThen(function (waitFor) {
var priv = common.getMetadataMgr().getPrivateData();
var hashes = opts.hashes || priv.hashes;
if (hashes && hashes.revocableData) {
opts.revocable = true;
opts.channel = hashes.revocableData.channel;
}
Modal.getPadData(Env, opts, waitFor(function (e, _data) {
if (e) {
blocked = false;

View file

@ -315,6 +315,513 @@ define([
};
};
var errorTab = function (txt) {
return {
content: h('p', txt),
buttons: [{
className: 'cancel',
name: Messages.filePicker_close,
onClick: function () {},
keys: [27]
}]
};
};
var redrawAccessEvt = Util.mkEvent();
var redrawAccess = function (Env, channel) {
var revocation = Env.common.makeUniversal('revocation');
revocation.execCommand('LIST_ACCESS', {
channel: channel
}, function (obj) {
redrawAccessEvt.fire(obj);
});
};
var renderAs = function ($viewAs, obj, onChange) {
$viewAs.empty();
var myKeys = obj.myKeys || [];
var options = myKeys.map(function (obj) {
return {
tag: 'a',
attributes: {
'class': 'cp-share-access-value',
'data-value': obj.key,
'href': '#',
},
content: obj.origin
};
});
var dropdownConfig = {
text: '', // Button initial text
options: options, // Entries displayed in the menu
isSelect: true,
caretDown: true,
buttonCls: 'btn btn-secondary'
};
var select = UIElements.createDropdown(dropdownConfig);
if (myKeys.length) { select.setValue(myKeys[0].key); }
select.onChange.reg(function () {
var v = select.getValue();
var renderedAs = myKeys.find(function (obj) { return obj.key === v});
onChange(renderedAs);
});
$viewAs.append(select);
onChange(myKeys[0]);
};
var getSecurityTab = function (Env, data, opts, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var common = Env.common;
var metadataMgr = common.getMetadataMgr();
var priv = metadataMgr.getPrivateData();
if (priv.offline) {
return void cb(void 0, errorTab(Messages.share_noContactsOffline));
}
var rev = opts.hashes && opts.hashes.revocableData;
if (!rev) {
return void cb(void 0, errorTab('EINVAL')); // XXX
}
var revocation = common.makeUniversal('revocation');
var channel = rev.channel;
var redraw = function () {
redrawAccess(Env, channel);
};
var rotate = h('div');
var $rotate = $(rotate);
var viewAs = h('div.cp-share-access-as');
var $viewAs = $(viewAs);
var content = h('div.cp-share-access-list-container', [viewAs, rotate]);
var $content = $(content);
var drawRotate = function (obj, as) {
var button = h('button.btn.btn-primary', 'ROTATE KEYS'); // XXX
$rotate.append(button);
if (!as.moderator) {
$(button).attr('disabled', 'disabled');
return;
}
var c = UI.confirmButton(button, {
classes: 'btn-primary',
multiple: true // XXX
}, function () {
// Confirmed, start keys rotation
// XXX it may be easier to handle rotation in client?
// XXX because we need access to chainpad (lock + make checkpoint)
revocation.execCommand('ROTATE_KEYS', {
channel: channel,
from: as.key
}, function (obj) {
console.warn(obj);
});
});
};
var drawDestroy = function (obj, as) {
var button = h('button.btn.btn-danger', 'DESTROY'); // XXX
$rotate.append(button);
if (!as.canDestroy) {
$(button).attr('disabled', 'disabled');
return;
}
var c = UI.confirmButton(button, {
classes: 'btn-danger',
multiple: true // XXX
}, function () {
// Confirmed, start keys rotation
// XXX it may be easier to handle rotation in client?
// XXX because we need access to chainpad (lock + make checkpoint)
revocation.execCommand('DESTROY', {
channel: channel,
from: as.key
}, function (obj) {
console.warn(obj);
});
});
};
redrawAccessEvt.reg(function (obj) {
if (obj && obj.error) {
return void UI.warn(Messages.error);
}
if (!obj.myKeys || !obj.myKeys.length) {
console.error('Not a member!');
return void UI.warn(Messages.error);
}
renderAs($viewAs, obj, function (as) {
$rotate.empty();
drawRotate(obj, as);
drawDestroy(obj, as);
});
});
cb(void 0, {
content: [content],
buttons: [{
className: 'cancel',
name: Messages.filePicker_close,
onClick: function () {},
keys: [27]
}]
});
};
var getRevocableTab = function (Env, data, opts, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var common = Env.common;
var hasFriends = opts.hasFriends;
var onFriendShare = Util.mkEvent();
var myName = common.getMetadataMgr().getUserData().name;
var title = opts.title;
var metadataMgr = common.getMetadataMgr();
var priv = metadataMgr.getPrivateData();
if (priv.offline) {
return void cb(void 0, errorTab(Messages.share_noContactsOffline));
}
var rev = opts.hashes && opts.hashes.revocableData;
if (!rev) {
return void cb(void 0, errorTab('EINVAL')); // XXX
}
var viewAs = h('div.cp-share-access-as');
var list = h('div.cp-share-access-list');
var buttonContainer = h('div');
var content = h('div.cp-share-access-list-container', [viewAs, list, buttonContainer]);
var $list = $(list);
var $viewAs = $(viewAs);
var usergrid = h('div.cp-share-access-usergrid');
var $usergrid = $(usergrid);
var $content = $(content);
var channel = rev.channel;
var revocation = common.makeUniversal('revocation');
var updateAccess = function () {};
var addAccess = function () {};
var redraw = function () {
redrawAccess(Env, channel);
};
var TYPES = {
user: { icon: '.fa.fa-user', order: 1 },
team: { icon: '.fa.fa-users', order: 2 },
link: { icon: '.fa.fa-link', order: 3 },
sf: { icon: '.cptools.cptools-shared-folder', order: 4 },
};
var makeDD = function (current, editable, maxR) {
var value;
current = current.replace(/d+$/, '');
if (current.includes('rwm')) { value = 'moderate'; }
else if (current.includes('rw')) { value = 'write'; }
else if (current.includes('r')) { value = 'read'; }
if (!editable) {
return [h('button.btn.btn-secondary', {disabled:'disabled'}, value)]; // XXX
}
var options = ['r', 'rw', 'rwm'].map(function (r, i) {
if (maxR === 'r' && i) { return; }
if (maxR === 'rw' && i > 1) { return; }
return {
tag: 'a',
attributes: {
'class': 'cp-share-access-value',
'data-value': r,
'href': '#',
},
content: r // XXX
};
});
var dropdownConfig = {
text: '', // Button initial text
options: options, // Entries displayed in the menu
isSelect: true,
caretDown: true,
buttonCls: 'btn btn-secondary'
};
var select = UIElements.createDropdown(dropdownConfig);
select.setValue(current);
return select;
};
var getRights = function (dd, $d) {
var r = dd.getValue() || 'r';
var d = Util.isChecked($d);
//var rights = r === 'm' ? 'rwm' : (r === 'w' ? 'rw' : 'r');
if (d) { r += 'd'; }
return r;
};
var renderAccess = function (edPublic, accessData, editable, maxR, renderedAs) {
var type = accessData.notes.type
var icon = TYPES[type].icon;
var rights = accessData.rights;
var note = accessData.notes.note;
var d = rights.includes('d');
var canDestroy = UI.createCheckbox('cp-share-can-destroy', h('i.fa.fa-trash'), d, {});
if (!editable) { $(canDestroy).find('input').attr('disabled', 'disabled'); }
var revoke = h('button.btn.btn-danger', [
h('i.fa.fa-times'),
h('span', 'REVOKE') // XXX
]);
if (!editable) { $(revoke).attr('disabled', 'disabled'); }
else {
$(revoke).click(function () {
UI.confirm(Messages.areYouSure, function (yes) { // XXX REVOCATION message
if (!yes) { return; }
updateAccess(edPublic, false, renderedAs.key);
});
});
}
var $d = $(canDestroy).find('input');
if (editable && maxR === 'm') {
$d.on('change', function () {
updateAccess(edPublic, getRights(dd, $d), renderedAs.key);
});
}
var dd = makeDD(rights, editable, maxR);
if (dd.onChange) {
dd.onChange.reg(function () {
updateAccess(edPublic, getRights(dd, $d), renderedAs.key);
});
}
return h('div.cp-share-access', {
order: TYPES[type].order
}, [
h('i'+icon),
//UI.dialog.selectable(note, {class: 'cp-share-access-id'}),
h('span.cp-share-access-id', note),
dd[0],
canDestroy,
revoke
]);
};
var addAccessForm = function (maxR, renderedAs, userData, onCancel) {
// new form
var input = h('input', {
placeholder: (userData && userData.displayName) || 'Note' // XXX
});
var dd = makeDD('r', true, maxR);
var canDestroy = UI.createCheckbox('cp-share-can-destroy', h('i.fa.fa-trash'), false, {});
var $d = $(canDestroy).find('input');
var saveBtn = h('button.btn.btn-primary', [
h('i.fa.fa-floppy-o'),
h('span', 'SAVE') // XXX
]);
var cancelBtn = h('button.btn.btn-cancel', [
h('i.fa.fa-times')
]);
var temp = h('div.cp-share-access', {order:100}, [
userData ? h('i.fa.fa-user-plus') : h('i.fa.fa-plus'),
input,
dd[0],
canDestroy,
saveBtn,
cancelBtn
]);
var $temp = $(temp);
$(cancelBtn).click(function () {
onCancel();
$temp.remove();
});
$(saveBtn).click(function () {
var access = getRights(dd, $d);
var note = {
type: 'link',
note: $(input).val()
};
if (userData && userData.edPublic) {
// XXX REVOCATION teams and sf?
note.type = 'user';
note.edPublic = userData.edPublic;
}
// XXX REVOCATION add "share with team when I'm a viewer" (send to mailbox)
addAccess(access, note, renderedAs.key, function (obj) {
redraw();
if (obj && obj.error) { return UI.warn(Messages.error); }
var box = Hash.getRevocable(priv.app, obj.seed);
var hash = Hash.getRevocableHashFromKeys(priv.app, box);
var href = Hash.hashToHref(hash, priv.app);
common.mailbox.sendTo("SHARE_PAD", {
href: href,
isStatic: Boolean(opts.static),
password: opts.password,
isTemplate: opts.isTemplate,
name: myName,
isCalendar: Boolean(opts.calendar),
title: title,
revocable: {
channel: opts.channel,
}
}, {
channel: userData.notifications,
curvePublic: userData.curvePublic
});
});
});
return $temp;
};
var refreshUsergrid = function (cb) {
$usergrid.empty().show();
$content.hide();
var friendsList = UIElements.getUserGrid(Messages.share_linkFriends, {
common: common,
data: opts.friends,
noFilter: true, // XXX
noSelect: true,
picker: true,
large: true
}, function ($friend) {
var curve = $friend.data('curve');
var data = opts.friends[curve];
if (!data || !data.notifications || !data.curvePublic) {
// XXX REVOCATION show error? can't share with this contact
return void redraw();
}
$usergrid.hide();
$content.show();
cb(data);
});
$usergrid.append(friendsList.div);
};
var addUserAccessButton = function (maxR, renderedAs) {
var button = h('button.btn.btn-primary', [
h('i.fa.fa-plus'),
h('span', 'ADD USER') // XXX
]);
var $b = $(button);
var onCancel = function () { $b.show(); };
$b.click(function () {
$b.hide();
refreshUsergrid(function (userData) {
var $form = addAccessForm(maxR, renderedAs, userData, onCancel);
$list.append($form);
});
});
$(buttonContainer).append($b);
return button;
};
var addAccessButton = function (maxR, renderedAs) {
// show form btn
var button = h('button.btn.btn-primary', [
h('i.fa.fa-plus'),
h('span', 'ADD') // XXX
]);
var $b = $(button);
var onCancel = function () { $b.show(); };
$b.click(function () {
$b.hide();
var $form = addAccessForm(maxR, renderedAs, null, onCancel);
$list.append($form);
});
$(buttonContainer).append($b);
return button;
};
var addButton, addUserButton;
var renderAll = function (obj, renderedAs) {
$list.empty();
var list = obj.list;
var myAccess = list[renderedAs.key];
var maxRights = myAccess.rights.includes('m') ? 'm' :
(myAccess.rights.includes('w') ? 'w' : 'r');
Object.keys(list || {}).forEach(function (ed) {
var editable = renderedAs.moderator || renderedAs.key === list[ed].from;
var a = renderAccess(ed, list[ed], editable, maxRights, renderedAs);
$list.append(a);
});
if (addButton) { $(addButton).remove(); }
addButton = addAccessButton(maxRights, renderedAs);
if (addUserButton) { $(addUserButton).remove(); }
addUserButton = addUserAccessButton(maxRights, renderedAs);
};
redrawAccessEvt.reg(function (obj) {
if (obj && obj.error) {
return void UI.warn(Messages.error);
}
if (!obj.myKeys || !obj.myKeys.length) {
console.error('Not a member!');
return void UI.warn(Messages.error);
}
renderAs($viewAs, obj, function (as) {
renderAll(obj, as);
});
});
addAccess = function (rights, note, updateAs, cb) {
revocation.execCommand('ADD_ACCESS', {
type: priv.app,
channel: channel,
rights: rights,
note: note,
from: updateAs
}, function (obj) {
if (cb) { return void cb(obj); }
redraw();
if (obj && obj.error) { return UI.warn(Messages.error); }
});
};
updateAccess = function (user, rights, updateAs) {
var access = !rights ? false : {
rights: rights
};
revocation.execCommand('UPDATE_ACCESS', {
channel: channel,
value: {
user: user,
access: access
},
from: updateAs
}, function () {
redraw();
// XXX refresh view
});
};
redraw();
cb(void 0, {
content: [content, usergrid],
buttons: [{
className: 'cancel',
name: Messages.filePicker_close,
onClick: function () {},
keys: [27]
}]
});
};
var getContactsTab = function (Env, data, opts, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var common = Env.common;
@ -535,6 +1042,7 @@ define([
var origin = opts.origin;
var pathname = opts.pathname;
var parsed = Hash.parsePadUrl(pathname);
var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1;
var versionHash = hashes.viewHash && opts.versionHash;
var isForm = parsed.type === "form"; // && opts.auditorHash;
@ -746,8 +1254,12 @@ define([
var parsedHref = Hash.parsePadUrl(href);
opts.hasPassword = parsedHref.hashData.password;
var $rights = opts.$rights = getRightsHeader(common, opts);
var $rights;
if (!hashes.revocableData) {
// XXX this function adds opts.channel which breaks data
// but we may not need it for revocable pads
$rights = opts.$rights = getRightsHeader(common, opts);
}
var resetTab = function () {
if (opts.static) { return; }
$rights.show();
@ -790,7 +1302,24 @@ define([
onHide: resetTab
});
}
console.warn(parsedHref, href, hashes);
if (hashes.revocableData) {
tabs = [{
getTab: getRevocableTab,
title: 'REVOCABLE', // XXX
icon: "fa fa-address-book",
active: true,
}, {
getTab: getSecurityTab,
title: 'SECURITY', // XXX
icon: "fa fa-lock",
active: false,
}];
}
Modal.getModal(common, opts, tabs, function (err, modal) {
console.error(err, modal);
// Hide the burn-after-reading option by default
var $modal = $(modal);
$modal.find('.cp-bar').hide();

View file

@ -122,12 +122,29 @@ define([
}, defaultDismiss(common, data));
return;
}
var href = msg.content.href;
var obj = {
p: msg.content.isTemplate ? ['template'] : undefined,
t: teamNotification || undefined,
pw: msg.content.password || ''
};
common.openURL(Hash.getNewPadURL(msg.content.href, obj));
if (msg.content.revocable) {
// XXX TODO
// We don't want to leak the user personal access for this pad
// The URL should not be visible in the address bar otherwise the user may try
// to copy it and send it to others.
// Option 1: [ ] Ask the user to store in drive before opening
// Option 2: [x] Hide mailbox data in pad options (instantly removed in sco)
var channel = msg.content.revocable.channel;
var parsed = Hash.parsePadUrl(href);
var hash = Hash.getHiddenHashFromKeys(parsed.type, {channel:channel});
href = Hash.hashToHref(hash, parsed.type);
obj.revocable = {
type: 'user',
seed: parsed.hashData && parsed.hashData.key
};
}
common.openURL(Hash.getNewPadURL(href, obj));
defaultDismiss(common, data)();
};
if (!content.archived) {

View file

@ -11,6 +11,7 @@ define([
'/common/common-realtime.js',
'/common/common-messaging.js',
'/common/pinpad.js',
'/common/revocable.js',
'/common/outer/cache-store.js',
'/common/outer/sharedfolder.js',
'/common/outer/cursor.js',
@ -21,6 +22,7 @@ define([
'/common/outer/messenger.js',
'/common/outer/history.js',
'/common/outer/calendar.js',
'/common/outer/revocation.js',
'/common/outer/network-config.js',
'/customize/application_config.js',
@ -32,9 +34,9 @@ define([
'/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js',
], function (ApiConfig, Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging, Pinpad, Cache,
Realtime, Messaging, Pinpad, Revocable, Cache,
SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History,
Calendar, NetConfig, AppConfig,
Calendar, Revocation, NetConfig, AppConfig,
Crypto, ChainPad, CpNetflux, Listmap, Netflux, nThen, Saferphore) {
var onReadyEvt = Util.mkEvent(true);
@ -351,6 +353,23 @@ define([
});
};
var findStoredPad = function (channel) {
var revocable;
var stores = [];
getAllStores().forEach(function (s) {
var res = s.manager.findChannel(channel);
if (!res.length) { return; }
stores.push(s);
if (revocable) { return; }
revocable = res.some(function (obj) {
revocable = obj && obj.data && obj.data.r;
});
});
return {
revocable: revocable,
stores: stores
};
};
var myDeletions = {};
Store.removeOwnedChannel = function (clientId, data, cb) {
// "data" used to be a string (channelID), now it can also be an object
@ -358,6 +377,7 @@ define([
var channel = data;
var force = false;
var teamId;
var revocable = data.revocable;
if (data && typeof(data) === "object") {
channel = data.channel;
force = data.force;
@ -368,6 +388,15 @@ define([
return void cb({error: 'User drive removal blocked!'});
}
var all = findStoredPad(data.channel);
revocable = revocable || all.revocable;
if (revocable) {
var revocation = store.modules['revocation'];
return void revocation.destroy(clientId, channel, cb);
}
var s = getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
@ -706,21 +735,23 @@ define([
});
};
var makePad = function (href, roHref, title) {
var makePad = function (href, roHref, title, accesses) {
var now = +new Date();
return {
href: href,
roHref: roHref,
var obj = {
atime: now,
ctime: now,
title: title || UserObject.getDefaultName(Hash.parsePadUrl(href)),
};
if (href) { obj.href = href; }
if (roHref) { obj.roHref = roHref; }
if (Array.isArray(accesses) && accesses.length) { obj.accesses = accesses; }
return obj;
};
Store.addPad = function (clientId, data, cb) {
if (!data.href && !data.roHref) { return void cb({error:'NO_HREF'}); }
if (!data.href && !data.roHref && !data.accesses) { return void cb({error:'NO_HREF'}); }
var secret;
if (!data.roHref) {
if (!data.roHref && !data.revocable) {
var parsed = Hash.parsePadUrl(data.href);
if (parsed.hashData.type === "pad") {
secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
@ -733,6 +764,8 @@ define([
if (data.password) { pad.password = data.password; }
if (data.channel || secret) { pad.channel = data.channel || secret.channel; }
if (data.readme) { pad.readme = 1; }
if (data.revocable) { pad.r = 1; }
if (Array.isArray(data.accesses)) { pad.accesses = data.accesses; }
var s = getStore(data.teamId);
if (!s || !s.manager) { return void cb({ error: 'ENOTFOUND' }); }
@ -1127,14 +1160,27 @@ define([
});
});
};
Store.deletePadFromStores = function (channel) {
var all = findStoredPad(channel);
if (!all.stores || !all.stores.length) { return; }
all.stores.forEach(function (s) {
s.manager.deleteChannel(channel, function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
});
};
Store.setPadTitle = function (clientId, data, cb) {
onReadyEvt.reg(function () {
var title = data.title;
var href = data.href;
var href = Hash.getRelativeHref(data.href || data.accessHref);
var channel = data.channel;
var p = Hash.parsePadUrl(href);
var h = p.hashData;
var accessType = data.accessType;
var accessHref = data.accessHref;
var revocable = Boolean(accessType);
if (title.trim() === "") { title = UserObject.getDefaultName(p); }
if (AppConfig.disableAnonymousStore && !store.loggedIn) {
@ -1170,7 +1216,7 @@ define([
// team drive. In this case, we just need to check if the pad is already
// stored in this team drive.
// If no team ID is provided, this may be a pad shared with its URL.
// We need to check if the pad is stored in any managers (user or teams).
// We need to check if the pad is stored in any manager (user or teams).
// If it is stored, update its data, otherwise ask the user if they want to store it
var allData = [];
var sendTo = [];
@ -1203,7 +1249,7 @@ define([
} else if (store.offline) {
return void cb();
}
allData.forEach(function (obj) {
allData.forEach(function (obj) { // update
var pad = obj.data;
pad.atime = +new Date();
pad.title = title;
@ -1219,7 +1265,23 @@ define([
Feedback.send('OPEN_README');
}
if (h.mode === 'view') { return; }
// XXX REVOCATION auto-store new accesses to already stored pad
if (h.version !== 3) { // Don't store safe links
if (accessType === 'link') {
if (!Array.isArray(pad.accesses)) {
pad.accesses = [accessHref];
} else if (!pad.accesses.includes(accessHref)) {
pad.accesses.push(accessHref);
}
} else if (accessType) {
if (!pad.href) {
obj.userObject.setHref(channel, null, accessHref);
// XXX SF: roHref with revocable
}
}
}
if (h.mode === 'view' || accessType) { return; }
// If we only have rohref, it means we have a stronger href
if (!pad.href) {
@ -1245,17 +1307,27 @@ define([
roHref = href;
href = undefined;
}
Store.addPad(clientId, {
var padData = {
teamId: data.teamId,
href: href,
roHref: roHref,
channel: channel,
title: title,
owners: owners,
expire: expire,
password: data.password,
path: data.path
}, cb);
path: data.path,
revocable: p.revocable
};
if (revocable) {
if (accessType === 'link') {
padData.accesses = [href];
} else {
padData.href = href;
}
} else {
padData.href = href;
padData.roHref = roHref;
padData.owners = owners;
}
Store.addPad(clientId, padData, cb);
// Let inner know that dropped files shouldn't trigger the popup
postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", {
stored: true,
@ -1353,6 +1425,13 @@ define([
return chans.some(function (pad) {
if (!pad || !pad.data) { return; }
var data = pad.data;
// Revocable pads: the revocation module will try to get the best access later
if (data.r) {
res = data;
return true;
}
// We've found a match: return the value and stop the loops
if ((edit && data.href) || (!edit && data.roHref) || isFile) {
res = data;
@ -1551,7 +1630,7 @@ define([
},
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
unpinPads: function (data, cb) { Store.unpinPads(null, data, cb); },
}, waitFor, function (ev, data, clients) {
}, waitFor, function (ev, data, clients, cb) {
clients.forEach(function (cId) {
postMessage(cId, 'UNIVERSAL_EVENT', {
type: type,
@ -1559,7 +1638,7 @@ define([
ev: ev,
data: data
}
});
}, cb);
});
});
};
@ -1726,9 +1805,21 @@ define([
if (data.versionHash) {
return void getVersionHash(clientId, data);
}
if (!Hash.isValidChannel(data.channel)) {
/*if (!Hash.isValidChannel(data.channel)) { // XXX fix this function
return void postMessage(clientId, "PAD_ERROR", 'INVALID_CHAN');
}*/
var signFunction;
if (data.authentication && data.authentication.edPrivate) {
signFunction = function (msg) {
var sig = Revocable.signLog(msg, data.authentication.edPrivate);
return {
sig: sig,
key: data.authentication.edPublic
};
};
}
var isNew = typeof channels[data.channel] === "undefined";
var channel = channels[data.channel] = channels[data.channel] || {
queue: [],
@ -1782,7 +1873,9 @@ define([
validateKey: channel.data.validateKey
});
});
if (channel.ready) {
postMessage(clientId, "PAD_READY");
}
return;
}
@ -1805,10 +1898,19 @@ define([
var conf = {
Cache: Cache, // ICE pad cache
onCacheStart: function () {
postMessage(clientId, "PAD_CACHE");
channel.bcast("PAD_CACHE");
},
onCacheReady: function () {
postMessage(clientId, "PAD_CACHE_READY");
channel.bcast("PAD_CACHE_READY");
},
onInit: function (obj) {
// We know our netflux ID: use it to prove you know the creator key
if (data.metadata && data.metadata.revocableData) {
var r = data.metadata.revocableData;
var keys = data.creation || {};
var msg = [data.channel, obj.myID];
r.creationProof = Revocable.signLog(msg, keys.creatorEdPrivate);
}
},
onReady: function (pad) {
var padData = pad.metadata || {};
@ -1817,11 +1919,13 @@ define([
store.messenger.storeValidateKey(data.channel, padData.validateKey);
}
if (!store.proxy) {
postMessage(clientId, "PAD_READY", pad.noCache);
channel.ready = true;
channel.bcast("PAD_READY", pad.noCache);
return;
}
onReadyEvt.reg(function () {
postMessage(clientId, "PAD_READY", pad.noCache);
channel.ready = true;
channel.bcast("PAD_READY", pad.noCache);
});
},
onMessage: function (m, user, validateKey, isCp, hash) {
@ -1844,6 +1948,7 @@ define([
onRejected: Store.onRejected,
onConnectionChange: function (info) {
if (!info.state) {
channel.ready = false;
channel.bcast("PAD_DISCONNECT");
}
},
@ -1874,6 +1979,7 @@ define([
noChainPad: true,
channel: data.channel,
metadata: data.metadata,
sign: signFunction,
network: store.network || store.networkPromise,
websocketURL: NetConfig.getWebsocketURL(),
//readOnly: data.readOnly,
@ -2100,7 +2206,7 @@ define([
if (store.offline || !store.anon_rpc) { return void cb({ error: 'OFFLINE' }); }
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (data.channel.length !== 32) { return void cb({ error: 'EINVAL'}); }
//if (data.channel.length !== 32) { return void cb({ error: 'EINVAL'}); }
if (!Hash.isValidChannel(data.channel)) {
Feedback.send('METADATA_INVALID_CHAN');
return void cb({ error: 'EINVAL' });
@ -2216,6 +2322,9 @@ define([
}
};
network.on('message', onMsg);
// XXX REVOCATION SIGNATURE
// XXX netfluxID: store.network.myID
network.sendto(hk, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey]));
};
@ -2290,6 +2399,8 @@ define([
txid: txid,
lastKnownHash: data.lastKnownHash
};
// XXX REVOCATION SIGNATURE
// XXX netfluxID: store.network.myID
var msg = ['GET_HISTORY', data.channel, cfg];
network.sendto(hk, JSON.stringify(msg));
};
@ -2349,6 +2460,9 @@ define([
};
network.on('message', onMsg);
// XXX REVOCATION SIGNATURE
// XXX netfluxID: store.network.myID
network.sendto(hk, JSON.stringify(['GET_HISTORY_RANGE', data.channel, {
from: data.lastKnownHash,
to: data.toHash,
@ -2728,6 +2842,8 @@ define([
};
postMessage(clientId, 'LOADING_DRIVE', data);
}, true);
}).nThen(function (waitFor) {
loadUniversal(Revocation, 'revocation', waitFor, clientId);
}).nThen(function (waitFor) {
loadUniversal(Team, 'team', waitFor, clientId);
}).nThen(function (waitFor) {
@ -3110,6 +3226,7 @@ define([
// initialize the chat (messenger) and the cursor modules.
loadUniversal(Cursor, 'cursor', function () {});
loadUniversal(Messenger, 'messenger', function () {});
loadUniversal(Revocation, 'revocation', function () {});
store.messenger = store.modules['messenger'];
// And now we're ready

View file

@ -126,7 +126,7 @@ proxy.mailboxes = {
crypto = Crypto.Mailbox.createEncryptor(keys);
// Always send your data
if (typeof(msg) === "object" && !msg.user) {
if (typeof(msg) === "object" && !msg.user && !user.noSender) {
var myData = Messaging.createData(ctx.store.proxy, false);
msg.user = myData;
}
@ -152,7 +152,7 @@ proxy.mailboxes = {
anonRpc.send("WRITE_PRIVATE_MESSAGE", [
channel,
ciphertext
ciphertext // XXX REVOCATION CHECK NO NEED TO SIGN MAILBOXES
], function (err /*, response */) {
if (err) {
return void cb({
@ -164,6 +164,15 @@ proxy.mailboxes = {
});
});
};
Mailbox.sendAs = function (ctx, keys, type, msg, user, cb) {
user.noSender = true;
sendTo({
store: {
anon_rpc: ctx.store.anon_rpc,
proxy: keys
}
}, type, msg, user, cb);
};
Mailbox.sendToAnon = function (anonRpc, type, msg, user, cb) {
var Nacl = Crypto.Nacl;
var curveSeed = Nacl.randomBytes(32);
@ -650,6 +659,10 @@ proxy.mailboxes = {
sendTo(ctx, type, msg, user, cb);
};
mailbox.sendAs = function (type, msg, to, as, cb) {
Mailbox.sendAs(ctx, as, type, msg, to, cb);
};
mailbox.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
@ -665,6 +678,9 @@ proxy.mailboxes = {
if (cmd === 'SENDTO') {
return void sendTo(ctx, data.type, data.msg, data.user, cb);
}
if (cmd === 'SENDAS') {
return void Mailbox.sendAs(ctx, data.keys, data.type, data.msg, data.user, cb);
}
if (cmd === 'LOAD_HISTORY') {
return void loadHistory(ctx, clientId, data, cb);
}

File diff suppressed because it is too large Load diff

View file

@ -732,7 +732,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback)
response.expect(id, cb, TIMEOUT_INTERVAL);
anon_rpc.send('WRITE_PRIVATE_MESSAGE', [
channel,
ciphertext
ciphertext,
// XXX REVOCATION SIGN
], function (err) {
if (err) { return response.handle(id, [err.message || err]); }
});

View file

@ -773,6 +773,11 @@ define([
id = Number(id);
var el = fd[id];
if (el.r && !el.href) {
// XXX REVOCATION sanitize/check/fix revocable pads separately
continue;
}
// Clean corrupted data
if (!el || typeof(el) !== "object") {
debug("An element in filesData was not an object.", el);
@ -814,7 +819,8 @@ define([
}
// If we have an edit link, check the view link
if (decryptedHref && parsed.hashData.type === "pad" && parsed.hashData.version) {
if (decryptedHref && parsed.hashData.type === "pad" && parsed.hashData.version
&& !parsed.revocable) { // Don't force roHref for revocable URLs // XXX XXX REVOCATION
if (parsed.hashData.mode === "view") {
el.roHref = decryptedHref;
delete el.href;

View file

@ -864,6 +864,27 @@ define([
cb();
});
};
var _deleteChannel = function (Env, chan, cb) {
var ids = _findChannels(Env, [chan]);
if (!ids.length) { return void cb(); }
var toDelete = {
main: [],
folders: {}
};
ids.forEach(function (id) {
var paths = findFile(Env, id);
var _resolved = _resolvePaths(Env, paths);
Array.prototype.push.apply(toDelete.main, _resolved.main);
Object.keys(_resolved.folders).forEach(function (fId) {
toDelete.folders[fId] = toDelete.folders[fId] || [];
Array.prototype.push.apply(toDelete.folders[fId], _resolved.folders[fId]);
});
});
// Remove deleted pads from the drive
_delete(Env, { resolved: toDelete }, cb);
};
// Delete permanently some pads or folders
var _deleteOwned = function (Env, data, cb) {
data = data || {};
@ -1089,7 +1110,7 @@ define([
}
Env.user.proxy[UserObject.SHARED_FOLDERS][sfId][data.attr] = data.value;
}
var datas = findHref(Env, data.href);
var datas = data.channel ? findChannel(Env, data.channel) : findHref(Env, data.href);
var nt = nThen;
datas.forEach(function (d) {
nt = nt(function (waitFor) {
@ -1106,6 +1127,10 @@ define([
// NOTE: we also return the atime, so that we can also check with each team manager
var getPadAttribute = function (Env, data, cb) {
cb = cb || function () {};
// XXX REVOCATION: get shared folder from their channel ID
var sfId = Env.user.userObject.getSFIdFromHref(data.href);
if (sfId) {
var sfData = getSharedFolderData(Env, sfId);
@ -1118,7 +1143,10 @@ define([
});
return;
}
var datas = findHref(Env, data.href);
// With the revocation system, pads can have multiple URLs (mailboxes).
// We can only search them with channel ID.
var datas = data.channel ? findChannel(Env, data.channel) : findHref(Env, data.href);
var res = {};
datas.forEach(function (d) {
var atime = d.data.atime;
@ -1358,6 +1386,7 @@ define([
getChannelsList: callWithEnv(getChannelsList),
addPad: callWithEnv(addPad),
delete: callWithEnv(_delete),
deleteChannel: callWithEnv(_deleteChannel),
deleteOwned: callWithEnv(_deleteOwned),
// Tools
findChannel: callWithEnv(findChannel),
@ -1493,7 +1522,13 @@ define([
/* Tools */
var findChannels = _findChannels;
var getFileData = _getFileData;
var getFileData = function (Env, id) {
var d = _getFileData(Env, id, false);
if (!d.href && !d.roHref && Array.isArray(d.accesses)) {
d.href = d.accesses[0]; // XXX REVOCATION HACK, could be fixed in drive-ui
}
return d;
};
var getUserObjectPath = _getUserObjectPath;
var find = function (Env, path, fId) {

400
www/common/revocable.js Normal file
View file

@ -0,0 +1,400 @@
(function (window) {
var factory = function (Hash, Nacl) {
var Revocable = window.CryptPad_Revocable = {};
// Access metadata
Revocable.isModerator = function (access) {
if (typeof(access) === "string") {
return access.includes('m');
}
if (!access || !access.rights) { return false; }
return access.rights.includes('m');
};
Revocable.isEditor = function (access) {
if (typeof(access) === "string") {
return access.includes('w');
}
if (!access || !access.rights) { return false; }
return access.rights.includes('w');
};
Revocable.isViewer = function (access) {
if (typeof(access) === "string") {
return access.includes('r');
}
if (!access || !access.rights) { return false; }
return access.rights.includes('r');
};
Revocable.canDestroy = function (access) {
if (typeof(access) === "string") {
return access.includes('d');
}
if (!access || !access.rights) { return false; }
return access.rights.includes('d');
};
Revocable.isValidRights = function (rights) {
if (typeof(rights) !== "string") { return false; }
return /^rw?m?d?$/.test(rights);
};
Revocable.isValidAccessUpdate = function (oldValue, newValue) {
if (oldValue && !newValue) { return true; } // Deletion
if (oldValue) { // Update: I can only change "rights"
return Object.keys(newValue).every(function (key) {
if (key !== "rights") { return false; }
if (!/^rw?m?d?$/.test(newValue.rights)) { return false; }
return true;
});
}
// This is a new entry
return newValue.rights && /^rw?m?d?$/.test(newValue.rights)
&& newValue.notes && newValue.curvePublic && newValue.mailbox;
};
Revocable.isAllowedAccessUpdate = function (all, myKey, oldValue, newValue) {
const myAccess = all[myKey];
// Check integrity
if (!Revocable.isValidAccessUpdate(oldValue, newValue)) { return true; }
// Moderator? always allowed
if (Revocable.isModerator(myAccess)) { return false; }
// Editor: revoke access only for your subtree
if (newValue === false) {
return !Revocable.isInMyAccessTree(oldValue, all, myKey);
}
// Add or update access: delegated access never greater than your access
if (newValue.rights.includes('m') || newValue.rights.includes('d')) { return true; }
if (newValue.rights.includes('w') && !myAccess.rights.includes('w')) { return true; }
// Add access
if (!oldValue) { return false; }
// Update access: forbidden if not in my tree
return !Revocable.isInMyAccessTree(oldValue, all, myKey);
};
Revocable.isInMyAccessTree = function (value, tree, myKey) {
if (value.from === myKey) { return true; }
if (!value.from) { return false; }
if (!tree[value.from]) { return false; }
return Revocable.isInMyAccessTree(tree[value.from], tree, myKey);
};
Revocable.reencryptNotes = function (oldCrypto, newCrypto, md) {
if (!md) { return; }
var result = {};
var reencrypt = function (str) {
// XXX NO NEED TO SIGN HERE
return newCrypto.encrypt(oldCrypto.decrypt(str, true, true));
};
var abort = Object.keys(md.access || {}).some(function (ed) {
var access = md.access[ed];
try {
result[ed] = {
mailbox: reencrypt(access.mailbox),
notes: reencrypt(access.notes),
};
} catch (e) {
return true;
}
});
if (abort) { return; }
return result;
};
// Log
Revocable.firstLog = function (modEdPublic) {
return ['ADD', undefined, modEdPublic];
};
Revocable.addLog = function (modEdPublic, prevHash) {
return ['ADD', prevHash, modEdPublic];
};
Revocable.removeLog = function (modEdPublic, prevHash) {
return ['REMOVE', prevHash, modEdPublic];
};
Revocable.rotateLog = function (keyHash, validateKey, uid, prevHash) {
return ['ROTATE', prevHash, keyHash, validateKey, uid];
};
Revocable.signLog = function (msg, edPrivate) {
try {
var msgBytes = Nacl.util.decodeUTF8(JSON.stringify(msg));
var key = Nacl.util.decodeBase64(edPrivate);
var sig = Nacl.sign.detached(msgBytes, key);
return Nacl.util.encodeBase64(Nacl.sign.detached(msgBytes, key));
} catch(e) {
console.error(e);
return;
}
};
Revocable.addSignatureLog = function (msg, authorEd, signature) {
msg.push(signature);
msg.push(authorEd);
return msg;
};
Revocable.checkLog = function (msg, edPublic, signature) {
try {
var sig = Nacl.util.decodeBase64(signature);
var msgBytes = Nacl.util.decodeUTF8(JSON.stringify(msg));
var key = Nacl.util.decodeBase64(addSlashes(edPublic));
var check = Nacl.sign.detached.verify(msgBytes, sig, key);
return check;
} catch (e) {
console.error(e);
return;
}
};
Revocable.getSanitizedLog = function (md) {
var log = md.moderatorsLog;
var result = [];
if (!Array.isArray(log) || !log.length) { return result; }
var prevHash;
// Read the log and preserve all messages where the "prevHash"
// value matches the hash of the previous valid message
// Also reject messages coming from non-moderators
var moderators = [];
log.forEach(function (msg, i) {
var hash = Revocable.hashMsg(msg);
if (!i) {
prevHash = hash;
result.push(msg);
if (msg[0] === 'ADD') { moderators.push(msg[2]); } // Add moderator key
return;
}
if (msg[1] !== prevHash) { return; } // Invalid: ignore
// Check moderator key and signature
var _msg = msg.slice(0,-2);
var edPublic = msg[msg.length-1];
var signature = msg[msg.length-2];
if (!moderators.includes(edPublic)) { return; }
var check = Revocable.checkLog(_msg, edPublic, signature);
if (!check) { return; }
// Update moderators keys from "ADD" and "REMOVE" log
if (msg[0] === 'ADD' && !moderators.includes(msg[2])) { moderators.push(msg[2]); }
if (msg[0] === 'REMOVE' && moderators.includes(msg[2])) {
moderators.splice(moderators.indexOf(msg[2]), 1);
}
prevHash = hash;
result.push(msg);
});
// XXX make sure "moderators" matches the md.access data
return result;
};
// Mailbox
var RIGHTS = {
r: 'viewer',
w: 'editor',
m: 'moderator'
};
// Send missing keys to the user
Revocable.getUpgradeMessage = function (docKeys, newAccess, oldAccess) {
if (!newAccess) { return; } // Revoke
if (!oldAccess) { return; } // Create
var oldR = oldAccess.rights || 'r';
var newR = newAccess.rights || 'r';
var toSend = {};
newR.split('').forEach(function (key) {
if (oldR.includes(key)) { return; } // they already have this seed
if (!RIGHTS[key]) { return; } // no destroy seed
var type = RIGHTS[key];
if (!docKeys[type]) { return; } // make sure I know this key
toSend[type] = docKeys[type];
});
if (!Object.keys(toSend).length) { return; }
return toSend;
};
Revocable.rotateMailboxes = function (md, crypto, from, docKeys) {
var error = false;
var all = Object.keys(md.access || {}).map(function (ed) {
var access = md.access[ed];
var clone = JSON.parse(JSON.stringify(docKeys));
var rights = access.rights;
if (!Revocable.isModerator(rights)) { delete clone.moderator; }
if (!Revocable.isEditor(rights)) { delete clone.editor; }
try {
var chan = crypto.decrypt(access.mailbox, true, true);
var curve = access.curvePublic;
return {
type: "ROTATE",
msg: clone,
user: {
channel: chan,
curvePublic: curve
}, // send to
keys: from // send from
};
} catch (e) {
error = true;
console.error(e);
return true;
}
});
if (error) { return; }
return all;
};
Revocable.createMailbox = function (type, fromKeys, docKeys, rights) {
if (!Revocable.isValidRights(rights)) { return; }
var mailbox = Hash.getRevocable(type);
var clone = JSON.parse(JSON.stringify(docKeys));
if (!Revocable.isModerator(rights)) { delete clone.moderator; }
if (!Revocable.isEditor(rights)) { delete clone.editor; }
var initMsg = {
type: "INIT",
msg: {
doc: clone,
edPublic: mailbox.keys.edPublic,
edPrivate: mailbox.keys.edPrivate,
},
user: mailbox, // send to
keys: fromKeys // send from
};
return {
rights: rights,
initMsg: initMsg,
mailbox: mailbox,
edPublic: mailbox.keys.edPublic
};
};
Revocable.createAccess = function (type, user, notes, encryptSym, encryptAsym, contact) {
if (!Revocable.isValidRights(user.rights)) { return; }
notes.hash = Hash.getRevocableHashFromKeys(type, user.mailbox);
var access = {
rights: user.rights,
mailbox: encryptSym(user.mailbox.channel),
curvePublic: user.mailbox.curvePublic,
notes: encryptAsym(JSON.stringify(notes))
};
if (contact) { access.contact = encryptSym(contact); }
return access;
};
// Authentication
var addSlashes = function (str) {
return str.replace(/\-/g, '/');
};
var u8_concat = function (A) {
// expect a list of uint8Arrays
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 checkAccess = function (channel, userId, signature, md, type) {
if (!md.access) { return true; }
var edPublic = signature.key;
var access = md.access[edPublic];
// Reject if key is not allowed
if (!access || !access.rights.includes(type)) {
return false;
}
// If allowed, check signature
return Revocable.checkLog([channel, userId], signature.key, signature.sig);
};
Revocable.checkRead = function (channel, userId, signature, md) {
return checkAccess(channel, userId, signature, md, 'r');
};
Revocable.checkWrite = function (channel, userId, signature, md) {
return checkAccess(channel, userId, signature, md, 'w');
};
Revocable.creatorAuth = function (channel, userId, edPrivate) { // XXX DEPRECATED
var msg = channel + userId;
// Get encrypted content
var msgBytes = Nacl.util.decodeUTF8(msg);
var myKey = Nacl.util.decodeBase64(edPrivate);
var cipher = Nacl.box(msgBytes, nonce, theirKey, myKey);
// Bundle with nonce
var bundle = u8_concat([nonce, cipher]);
// Return results as base64
return Nacl.util.encodeBase64(bundle);
};
Revocable.creatorCheck = function (channel, userId, bundle, myPrivate, theirPublic) { // XXX DEPRECATED
var expected = channel + userId;
// Get encrypted content
var bundleBytes = Nacl.util.decodeBase64(bundle);
var nonce = bundleBytes.subarray(0, 24);
var cipher = bundleBytes.subarray(24, bundle.length);
var myKey = myPrivate;
var theirKey = Nacl.util.decodeBase64(addSlashes(theirPublic)); // theirPublic = chanId
var content = Nacl.box.open(cipher, nonce, theirKey, myKey);
// Compare with expected result
console.error(channel, Nacl.util.encodeUTF8(content) === expected);
return Nacl.util.encodeUTF8(content) === expected;
};
// Util
Revocable.hashBytes = function (bytes) {
try {
var hash = Nacl.hash(bytes);
return Nacl.util.encodeBase64(hash);
} catch (e) {
return;
}
};
Revocable.hashMsg = function (msg) {
try {
return Revocable.hashBytes(Nacl.util.decodeUTF8(JSON.stringify(msg)));
} catch (e) {
return;
}
};
Revocable.getCpId = function (msg) {
try {
return Nacl.util.encodeBase64(Nacl.hash(Nacl.util.decodeUTF8(msg)).slice(0, 8));
} catch (e) {
return;
}
};
return Revocable;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require("../../www/common/common-hash"),
require("tweetnacl/nacl-fast")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-hash.js',
'/bower_components/tweetnacl/nacl-fast.min.js'
], function (Hash) {
return factory(Hash, window.nacl);
});
} else {
// unsupported initialization
}
}(typeof(window) !== 'undefined'? window : {}));

View file

@ -55,7 +55,7 @@ define([
INFINITE_SPINNER: 'INFINITE_SPINNER',
ERROR: 'ERROR',
INITIALIZING: 'INITIALIZING',
READY: 'READY'
READY: 'READY',
});
var badStateTimeout = typeof(AppConfig.badStateTimeout) === 'number' ?
@ -81,6 +81,7 @@ define([
var toolbar;
var state = STATE.DISCONNECTED;
var firstConnection = true;
var restricted = false;
var toolbarContainer = options.toolbarContainer ||
(function () { throw new Error("toolbarContainer must be specified"); }());
@ -200,6 +201,7 @@ define([
break;
}
case STATE.ERROR: {
if (text === 'ERESTRICTED') { restricted = true; }
evStart.reg(function () {
if (text === 'ERESTRICTED') {
toolbar.failed(true);
@ -253,6 +255,7 @@ define([
var newContent = JSON.parse(newContentStr);
var meta = extractMetadata(newContent);
cpNfInner.metadataMgr.updateMetadata(meta);
newContent = normalize(newContent);
@ -481,6 +484,7 @@ define([
sframeChan.event("EV_CORRUPTED_CACHE");
};
var onCacheReady = function () {
if (state === STATE.ERROR && restricted) { return; }
stateChange(STATE.INITIALIZING);
toolbar.offline(true);
var newContentStr = cpNfInner.chainpad.getUserDoc();
@ -932,6 +936,28 @@ define([
var $store = common.createButton('storeindrive', true);
toolbar.$drawer.append($store);
var revocation = common.makeUniversal('revocation', {
onEvent: function (data, cb) {
if (data.ev === 'ASK_CHECKPOINT') {
console.warn(ChainPad, cpNfInner.chainpad);
var chainpad = cpNfInner.chainpad;
// Make patch
var content = chainpad.getUserDoc();
var authPatchHash = chainpad._.best.content.mut.inverseOf.parentHash
var cp = ChainPad.Patch.createCheckpoint(content, content, authPatchHash);
// Make message
var h = chainpad.getAuthBlock().hashOf;
var msg = ChainPad.Message.create(ChainPad.Message.CHECKPOINT, cp, h);
var str = ChainPad.Message.toStr(msg);
cb({msg: str});
return;
}
}
});
if (!cpNfInner.metadataMgr.getPrivateData().isTemplate) {
var templateObj = {
rt: cpNfInner.chainpad,

View file

@ -66,6 +66,17 @@ define([
sframeChan.query('Q_RT_MESSAGE', message, function (_err, obj) {
var err = _err || (obj && obj.error);
if (!err) { evPatchSent.fire(); }
if (err === 'EFORBIDDEN') {
// XXX REVOCATION
// XXX try other keys instead?
// Stop chainpad and force a page reload to try other keys?
// XXX We need to update outer/revocation to revoke this mailbox
// XXX Maybe switch to read-only instead of errorLoadingScreen?
isReady = false;
chainpad.abort();
onError(err);
return;
}
cb(err);
}, { timeout: -1 });
});

View file

@ -30,6 +30,8 @@ define([], function () {
var metadata= conf.metadata || {};
var versionHash = conf.versionHash;
var validateKey = metadata.validateKey;
var creation = conf.creation;
var authentication = conf.authentication;
var onConnect = conf.onConnect || function () { };
var lastTime; // Time of last patch (if versioned link);
conf = undefined;
@ -52,8 +54,9 @@ define([], function () {
var decryptedMsg = Crypto.decrypt(msg, key, isHk);
return decryptedMsg;
} catch (err) {
console.error(err);
console.warn(peer, msg);
// XXX REVOCATION: it's normal to have the wrong decryption key
//console.error(err);
//console.warn(peer, msg.slice(0,30));
return false;
}
};
@ -151,7 +154,9 @@ define([], function () {
channel: channel || null,
readOnly: readOnly,
versionHash: versionHash,
metadata: metadata
metadata: metadata,
creation: creation,
authentication: authentication
});
};

View file

@ -98,6 +98,7 @@ define([
var realtime = !cfg.noRealtime;
var secret;
var hashes;
var padOpts = {};
var isNewFile;
var CpNfOuter;
var Cryptpad;
@ -110,6 +111,7 @@ define([
var OOIframe;
var Messaging;
var Notifier;
var Revocable;
var Utils = {
nThen: nThen
};
@ -140,6 +142,7 @@ define([
'/common/common-notifier.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/revocable.js',
'/common/common-realtime.js',
'/common/notify.js',
'/common/common-constants.js',
@ -150,7 +153,8 @@ define([
//'/common/test.js',
'/common/userObject.js',
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel,
_SecureIframe, _UnsafeIframe, _OOIframe, _Messaging, _Notifier, _Hash, _Util, _Realtime, _Notify,
_SecureIframe, _UnsafeIframe, _OOIframe, _Messaging, _Notifier, _Hash, _Util,
_Revocable, _Realtime, _Notify,
_Constants, _Feedback, _LocalStore, _Cache, _AppConfig, /* _Test,*/ _UserObject) {
CpNfOuter = _CpNfOuter;
Cryptpad = _Cryptpad;
@ -162,6 +166,7 @@ define([
OOIframe = _OOIframe;
Messaging = _Messaging;
Notifier = _Notifier;
Revocable = _Revocable;
Utils.Hash = _Hash;
Utils.Util = _Util;
Utils.Realtime = _Realtime;
@ -355,10 +360,12 @@ define([
// New pad options
var options = parsed.getOptions();
if (options.newPadOpts) {
var rev = false;
try {
var newPad = Utils.Hash.decodeDataOptions(options.newPadOpts);
Cryptpad.initialTeam = newPad.t;
Cryptpad.initialPath = newPad.p;
padOpts.readOnly = newPad.mode === "view";
if (newPad.pw) {
try {
var uHash = Utils.LocalStore.getUserHash();
@ -375,15 +382,26 @@ define([
delete Cryptpad.fromFileData;
}
}
if (newPad.revocable) {
// XXX REVOCATION fake safe link user access, might change
var seed = newPad.revocable.seed;
var box = Utils.Hash.getRevocable(parsed.type, seed);
currentPad.hash = Utils.Hash.getRevocableHashFromKeys(parsed.type, box);
currentPad.type = newPad.revocable.type;
currentPad.href = Utils.Hash.hashToHref(currentPad.hash, parsed.type);
rev = true;
Cryptpad.setTabHash(parsed.hashData.getHash({}));
}
} catch (e) {
console.error(e, parsed.hashData.newPadOpts);
}
delete options.newPadOpts;
if (!rev) {
currentPad.href = parsed.getUrl(options);
currentPad.hash = parsed.hashData.getHash ? parsed.hashData.getHash(options)
: '';
}
var version = parsed.hashData.version;
parsed = Utils.Hash.parsePadUrl(currentPad.href);
Cryptpad.setTabHash(currentPad.hash);
@ -473,6 +491,75 @@ define([
value: ''
};
// Handle pads with revocable access (safe & unsafe)
var getRevocable = function (w) {
var correctPassword = waitFor();
var revCommand = function (cmd, data, cb) {
Cryptpad.universalCommand('revocation', cmd, data, cb);
};
// Receive password requests from worker and relay them to inner
var lastUid;
sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) {
password = data;
revCommand('PASSWORD', {
uid: lastUid,
pw: password
});
});
var onEvent = function (obj) {
if (obj.type !== 'revocation') { return; }
var q = obj.data;
if (q.ev !== 'ASK_PASSWORD') { return; }
lastUid = q.data && q.data.uid;
var oldPw = q.data && q.data.pw;
if (oldPw) { passwordCfg.value = oldPw; }
sframeChan.event("EV_PAD_PASSWORD", passwordCfg);
};
Cryptpad.universal.onEvent.reg(onEvent);
var seed = parsed.revocable && parsed.hashData.key;
var chan = parsed.hashData && parsed.hashData.channel;
revCommand('LOAD_PAD', {
seed: seed,
chan: chan
}, w(function (obj) {
// XXX fix with password workflow
if (obj && obj.error) {
if (obj.error === 'EFORBIDDEN') {
sframeChan.event("EV_RESTRICTED_ERROR");
}
console.error(obj.error);
return;
}
Cryptpad.universal.onEvent.unreg(onEvent);
correctPassword();
// XXX get access type (sf/team/user/link)
if (!currentPad.type) { currentPad.type = 'link'; }
secret = Utils.secret = Utils.Hash.getRevocableSecret({
channel: obj.channel,
viewerSeedStr: obj.doc.viewer,
editorSeedStr: obj.doc.editor,
moderatorSeedStr: obj.doc.moderator
}, password);
secret.revocation = {
edPrivate: obj.edPrivate,
edPublic: obj.edPublic
};
hashes = {
editHash: currentPad.hash,
revocableData: {
channel: secret.channel,
}
};
}));
};
// Hidden hash: can't find the channel in our drives: abort
var noPadData = function (err) {
sframeChan.event("EV_PAD_NODATA", err);
@ -480,6 +567,7 @@ define([
var newHref;
var expire;
var revocable;
nThen(function (w) {
// If we're using an unsafe link, get pad attribute
if (parsed.hashData.key || !parsed.hashData.channel) {
@ -496,6 +584,7 @@ define([
edit: edit,
file: parsed.hashData.type === 'file'
}, w(function (err, res) {
console.error(res);
// Error while getting data? abort
if (err || !res || res.error) {
w.abort();
@ -508,6 +597,9 @@ define([
}
// Data found but weaker? warn
expire = res.expire;
if (res.r) { revocable = true; return; } // Revocable
if (edit && !res.href) {
newHref = res.roHref;
return;
@ -524,13 +616,21 @@ define([
currentPad.href = parsed.getUrl(opts);
currentPad.hash = parsed.hashData && parsed.hashData.getHash(opts);
}
var chan = parsed.hashData.version === 3 && parsed.hashData.channel;
var url = revocable ? null : parsed.getUrl();
Cryptpad.getPadAttribute('channel', w(function (err, data) {
stored = (!err && typeof (data) === "string");
}));
}), null, chan);
Cryptpad.getPadAttribute('password', w(function (err, val) {
password = val;
}), parsed.getUrl());
}), url, chan);
}).nThen(function (w) {
// Revocable pad, safe (revocable) or unsafe (parsed.revocable) link
if (parsed.revocable || revocable) {
return void getRevocable(w);
}
// If we've already tested this password and this is a redirect, force
if (typeof(newPadPassword) !== "undefined" && newPadPasswordForce) {
password = newPadPassword;
@ -604,7 +704,7 @@ define([
// Check if the pad exists on server
if (!currentPad.hash) { isNewFile = true; return; }
if (realtime) {
if (realtime && !currentPad.type) {
// TODO we probably don't need to check again for password-protected pads
Cryptpad.hasChannelHistory(currentPad.href, password, waitFor(function (e, isNew) {
if (e) { return console.error(e); }
@ -612,7 +712,8 @@ define([
}));
}
}).nThen(function () {
var readOnly = secret.keys && !secret.keys.editKeyStr;
//var readOnly = secret.keys && !secret.keys.editKeyStr; // XXX XXX
var readOnly = padOpts.readOnly || (secret.keys && !secret.keys.signKey);
var isNewHash = true;
if (!secret.keys) {
isNewHash = false;
@ -775,8 +876,10 @@ define([
_sframeChan.event('EV_ALERTIFY_WARN', msg);
});
Cryptpad.universal.onEvent.reg(function (data) {
sframeChan.event('EV_UNIVERSAL_EVENT', data);
Cryptpad.universal.onEvent.reg(function (data, cb) {
sframeChan.query('EV_UNIVERSAL_EVENT', data, function (err, obj) {
cb(obj);
});
});
sframeChan.on('Q_UNIVERSAL_COMMAND', function (data, cb) {
Cryptpad.universal.execCommand(data, cb);
@ -929,7 +1032,7 @@ define([
error: e,
data: data
});
}, href);
}, href, data.channel);
});
sframeChan.on('Q_SET_PAD_ATTRIBUTE', function (data, cb) {
var href;
@ -940,7 +1043,7 @@ define([
if (data.href) { href = data.href; }
Cryptpad.setPadAttribute(data.key, data.value, function (e) {
cb({error:e});
}, href);
}, href, data.channel);
});
// Add or remove our mailbox from the list if we're an owner
@ -1178,6 +1281,10 @@ define([
};
var setPadTitle = function (data, cb) {
if (!data.href && currentPad.type) {
data.accessType = currentPad.type;
data.accessHref = Utils.Hash.getRelativeHref(currentPad.href);
}
Cryptpad.setPadTitle(data, function (err, obj) {
if (!err && !(obj && obj.notStored)) {
// No error and the pad was correctly stored
@ -1187,9 +1294,17 @@ define([
var useUnsafe = Utils.Util.find(settings, ['security', 'unsafeLinks']);
if (useUnsafe !== true && window.history && window.history.replaceState) {
if (!/^#/.test(hash)) { hash = '#' + hash; }
window.history.replaceState({}, window.document.title, hash);
//window.history.replaceState({}, window.document.title, hash);
// XXX REVOCATION USE SAFE HERE
}
}
// If we just created a pad, also store the moderator access
if (currentPad.revocationModerator) {
var data2 = Utils.Util.clone(data);
data2.accessHref = currentPad.revocationModerator.href;
data2.accessType = currentPad.revocationModerator.type;
Cryptpad.setPadTitle(data2, function () {});
}
cb({error: err});
});
};
@ -1570,7 +1685,6 @@ define([
UnsafeObject.modal = UnsafeIframe.create(config);
}
UnsafeObject.modal.refresh(cfg, function (data) {
console.error(data);
cb(data);
});
};
@ -1958,6 +2072,15 @@ define([
// Join the netflux channel
var rtStarted = false;
var startRealtime = function (rtConfig) {
if (!rtConfig && secret.revocation) {
rtConfig = {
authentication: {
edPublic: secret.revocation.edPublic,
edPrivate: secret.revocation.edPrivate
}
};
}
rtConfig = rtConfig || {};
rtStarted = true;
@ -1981,7 +2104,7 @@ define([
window.location.hash = hash;
};
if (burnAfterReading) {
if (burnAfterReading) { // XXX REVOCABLE
Cryptpad.padRpc.onReadyEvent.reg(function () {
Cryptpad.burnPad({
password: password,
@ -1991,6 +2114,13 @@ define([
});
});
}
// XXX cpNfCfg should use mailbox credentials?
// XXX WE NEED TO AUTHENTICATE WIHT THE SERVER TO HAVE OUR CORRECT ACCESS RIGHTS
// XXX get doc keys in mailbox directly
// XXX ["JOIN", "channelId", sign(channel+netfluxId, my_priv, serv_pub)] in netflux websocket
console.error(secret);
var cpNfCfg = {
sframeChan: sframeChan,
channel: secret.channel,
@ -2009,7 +2139,7 @@ define([
return;
}
if (readOnly || cfg.noHash) { return; }
replaceHash(Utils.Hash.getEditHashFromKeys(secret));
//replaceHash(Utils.Hash.getEditHashFromKeys(secret)); // XXX REVOCABLE
}
};
@ -2029,6 +2159,7 @@ define([
Object.keys(rtConfig).forEach(function (k) {
cpNfCfg[k] = rtConfig[k];
});
console.error(cpNfCfg);
CpNfOuter.start(cpNfCfg);
});
};
@ -2037,32 +2168,72 @@ define([
Cryptpad.onCorruptedCache(secret.channel);
});
var sendMailboxMsg = function (data, cb) {
Cryptpad.mailbox.execCommand({
cmd: 'SENDAS',
data: data
}, cb);
};
sframeChan.on('Q_CREATE_PAD', function (data, cb) {
if (!isNewFile || rtStarted) { return; }
// Create a new hash
password = data.password;
var newHash = Utils.Hash.createRandomHash(parsed.type, password);
secret = Utils.secret = Utils.Hash.getSecrets(parsed.type, newHash, password);
Utils.crypto = Utils.Crypto.createEncryptor(Utils.secret.keys);
Cryptpad.universalCommand('revocation', 'CREATE_PAD', {
type: parsed.type,
password: password,
edPublic: edPublic
}, function (data) {
var docKeys = data.docKeys;
secret = Utils.secret = Utils.Hash.getRevocableSecret({
channel: docKeys.channel,
viewerSeedStr: docKeys.viewer,
editorSeedStr: docKeys.editor,
}, password);
var crypto = Utils.crypto = Crypto.createEncryptor(secret.keys);
var onCreated = function () {
Cryptpad.universalCommand('revocation', 'JOIN_CREATED_PAD', data.seeds);
setTimeout(function () {
Cryptpad.padRpc.onReadyEvent.unreg(onCreated);
});
};
Cryptpad.padRpc.onReadyEvent.reg(onCreated);
currentPad.revocationModerator = {
type: edPublic ? 'user' : 'link',
href: Utils.Hash.hashToHref(data.modHash, parsed.type)
};
// Update the hash in the address bar
currentPad.hash = newHash;
currentPad.href = '/' + parsed.type + '/#' + newHash;
Cryptpad.setTabHash(newHash);
currentPad.hash = data.newHash;
currentPad.href = '/' + parsed.type + '/#' + data.newHash;
currentPad.type = 'link'; // XXX editor seed
Cryptpad.setTabHash(data.newHash);
hashes = {
editHash: currentPad.hash,
revocableData: {
channel: secret.channel,
}
};
// Update metadata values and send new metadata inside
parsed = Utils.Hash.parsePadUrl(currentPad.href);
defaultTitle = Utils.UserObject.getDefaultName(parsed);
hashes = Utils.Hash.getHashes(secret);
//hashes = Utils.Hash.getHashes(secret);
readOnly = false;
updateMeta();
var rtConfig = {
metadata: {}
};
if (data.team) {
Cryptpad.initialTeam = data.team.id;
}
if (data.team) { Cryptpad.initialTeam = data.team.id; }
var rtConfig = data.rtConfig;
if (data.expire) { rtConfig.metadata.expire = data.expire; }
/*
if (data.owned && data.team && data.team.edPublic) {
rtConfig.metadata.owners = [data.team.edPublic];
} else if (data.owned) {
@ -2073,14 +2244,20 @@ define([
curvePublic: curvePublic
}));
}
if (data.expire) {
rtConfig.metadata.expire = data.expire;
}
rtConfig.metadata.validateKey = (secret.keys && secret.keys.validateKey) || undefined;
*/
startRealtime(rtConfig); // XXX
cb(); // XXX
});
return;
Utils.rtConfig = rtConfig;
var templatePw;
nThen(function(waitFor) {
return;
// XXX CRYPTGET USE MAILBOX
if (data.templateContent) { return; }
if (data.templateId) {
if (data.templateId === -1) {
@ -2094,6 +2271,9 @@ define([
}));
}
}).nThen(function () {
return;
// XXX CRYPTGET USE MAILBOX
var cryptputCfg = $.extend(true, {}, rtConfig, {password: password});
if (data.templateContent) {
Cryptget.put(currentPad.hash, JSON.stringify(data.templateContent), function () {

View file

@ -523,19 +523,21 @@ define([
};
// href is optional here: if not provided, we use the href of the current tab
funcs.getPadAttribute = function (key, cb, href) {
funcs.getPadAttribute = function (key, cb, href, channel) {
ctx.sframeChan.query('Q_GET_PAD_ATTRIBUTE', {
key: key,
channel: channel,
href: href
}, function (err, res) {
cb(err || res.error, res && res.data);
});
};
funcs.setPadAttribute = function (key, value, cb, href) {
funcs.setPadAttribute = function (key, value, cb, href, channel) {
cb = cb || $.noop;
ctx.sframeChan.query('Q_SET_PAD_ATTRIBUTE', {
key: key,
href: href,
channel: channel,
value: value
}, cb);
};
@ -868,10 +870,10 @@ define([
});
});
ctx.sframeChan.on('EV_UNIVERSAL_EVENT', function (obj) {
ctx.sframeChan.on('EV_UNIVERSAL_EVENT', function (obj, cb) {
var type = obj.type;
if (!type || !modules[type]) { return; }
modules[type].fire(obj.data);
modules[type].fire(obj.data, cb);
});
ctx.cache = Cache.create(ctx.sframeChan);

View file

@ -234,6 +234,7 @@ define([
if (!isFile(element)) { return false; }
var data = exp.getFileData(element);
// undefined means this pad doesn't support read-only
if (data.r) { return false; } // XXX REVOCATION, we're not sure so consider edit
if (!data.roHref) { return; }
return Boolean(data.roHref && !data.href);
};
@ -373,7 +374,7 @@ define([
}
// handle links
if (data.static) { return data.name; }
if (!file || !(data.href || data.roHref)) {
if (!file || !(data.href || data.roHref || data.accesses)) {
error("getTitle called with a non-existing file id: ", file, data);
return;
}

View file

@ -388,7 +388,8 @@ define([
var hash = ciphertext.slice(0,64);
Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [
box.channel,
ciphertext
ciphertext,
// XXX REVOCATION SIGN ? PROBABLY NOT FOR A MAILBOX
], function (err, response) {
Cryptpad.storeFormAnswer({
uid: uid,

View file

@ -44,6 +44,7 @@ define([
// Share modal
create['share'] = function (data) {
console.error(data);
var priv = metadataMgr.getPrivateData();
var friends = common.getFriends();