Compare commits
17 commits
main
...
revocation
Author | SHA1 | Date | |
---|---|---|---|
|
3b5d3e9907 | ||
|
06c172d9dc | ||
|
388729e376 | ||
|
3c6819ec4a | ||
|
b0d65139ce | ||
|
de448f0fab | ||
|
9156096629 | ||
|
1bf606cf49 | ||
|
c0af9cb74d | ||
|
519a8861f3 | ||
|
21397022be | ||
|
aa9d4e950b | ||
|
935cb34f4b | ||
|
95e8133905 | ||
|
d6a9210190 | ||
|
bb59b2a6d0 | ||
|
36b46e7fc9 |
34 changed files with 3101 additions and 140 deletions
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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);
|
||||
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(metadata)]);
|
||||
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;
|
||||
}
|
||||
|
||||
// End of history message:
|
||||
let parsedMsg = {state: 1, channel: channelName, txid: txid};
|
||||
endHistory();
|
||||
|
||||
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg)]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -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; }
|
||||
|
|
142
lib/metadata.js
142
lib/metadata.js
|
@ -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 {
|
||||
handleCommand(ref.meta, line);
|
||||
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
1
lib/revocable.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require("../www/common/revocable");
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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})
|
||||
});
|
||||
}
|
||||
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; }
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
postMessage(clientId, "PAD_READY");
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
1127
www/common/outer/revocation.js
Normal file
1127
www/common/outer/revocation.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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]); }
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
400
www/common/revocable.js
Normal 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 : {}));
|
|
@ -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,
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
currentPad.href = parsed.getUrl(options);
|
||||
currentPad.hash = parsed.hashData.getHash ? parsed.hashData.getHash(options)
|
||||
: '';
|
||||
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);
|
||||
|
||||
// Update the hash in the address bar
|
||||
currentPad.hash = newHash;
|
||||
currentPad.href = '/' + parsed.type + '/#' + newHash;
|
||||
Cryptpad.setTabHash(newHash);
|
||||
|
||||
// Update metadata values and send new metadata inside
|
||||
parsed = Utils.Hash.parsePadUrl(currentPad.href);
|
||||
defaultTitle = Utils.UserObject.getDefaultName(parsed);
|
||||
hashes = Utils.Hash.getHashes(secret);
|
||||
readOnly = false;
|
||||
updateMeta();
|
||||
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 rtConfig = {
|
||||
metadata: {}
|
||||
};
|
||||
if (data.team) {
|
||||
Cryptpad.initialTeam = data.team.id;
|
||||
}
|
||||
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 = 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,
|
||||
}
|
||||
};
|
||||
|
||||
parsed = Utils.Hash.parsePadUrl(currentPad.href);
|
||||
defaultTitle = Utils.UserObject.getDefaultName(parsed);
|
||||
//hashes = Utils.Hash.getHashes(secret);
|
||||
readOnly = false;
|
||||
updateMeta();
|
||||
|
||||
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 () {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -44,6 +44,7 @@ define([
|
|||
|
||||
// Share modal
|
||||
create['share'] = function (data) {
|
||||
console.error(data);
|
||||
var priv = metadataMgr.getPrivateData();
|
||||
var friends = common.getFriends();
|
||||
|
||||
|
|
Loading…
Reference in a new issue