mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-13 18:45:43 +05:00
446 lines
14 KiB
JavaScript
446 lines
14 KiB
JavaScript
// SPDX-FileCopyrightText: 2026 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
const Linked = module.exports;
|
|
|
|
const nThen = require("nthen");
|
|
//const Core = require("./core");
|
|
//const CPCrypto = require('../crypto');
|
|
const Util = require("../common-util");
|
|
const MetaRPC = require("./metadata");
|
|
const HK = require("../hk-util");
|
|
|
|
const getMetadata = (Env, channel, _cb) => {
|
|
const cb = Util.once(Util.mkAsync(_cb));
|
|
|
|
const metadata = Env.metadata_cache[channel];
|
|
if (metadata && typeof(metadata) === 'object') {
|
|
return void cb(undefined, metadata);
|
|
}
|
|
|
|
MetaRPC.getMetadataRaw(Env, channel, (err, metadata) => {
|
|
if (err) { return void cb(err); }
|
|
if (metadata?.channel !== channel && channel.length !== HK.BLOB_ID_LENGTH) {
|
|
return cb();
|
|
}
|
|
|
|
// cache it
|
|
if (channel.length !== HK.BLOB_ID_LENGTH) {
|
|
Env.metadata_cache[channel] = metadata;
|
|
}
|
|
cb(undefined, metadata);
|
|
});
|
|
};
|
|
|
|
const allowedTypes = ['checkpoints', 'media', 'channels'];
|
|
|
|
// XXX add logs
|
|
|
|
Linked.getLinkedDocuments = (Env, data, cb) => {
|
|
Env.store.getLinkedDocuments(data.channel, (err, json) => {
|
|
if (err && err !== 'ENOENT') { return void cb(err?.message); }
|
|
cb(void 0, json || {});
|
|
});
|
|
};
|
|
|
|
Linked.listLinkedDocuments = (Env, channel, _cb) => {
|
|
const cb = Util.mkAsync(_cb);
|
|
if (channel.length !== HK.STANDARD_CHANNEL_LENGTH) {
|
|
return void cb(void 0, []);
|
|
}
|
|
|
|
const list = new Set();
|
|
Linked.getLinkedDocuments(Env, { channel }, (err, json) => {
|
|
if (err) { return void cb(err); }
|
|
// For each type, add the channels and/or blobs
|
|
allowedTypes.forEach(type => {
|
|
const data = json[type];
|
|
if (!Array.isArray(data)) { return; }
|
|
// Media or channel:
|
|
if (type !== 'checkpoints') {
|
|
data.forEach(id => { list.add(id); });
|
|
return;
|
|
}
|
|
// Checkpoint:
|
|
data.forEach(obj => {
|
|
if (obj?.rtChannel) { list.add(obj.rtChannel); }
|
|
if (obj?.blob) { list.add(obj.blob); }
|
|
});
|
|
});
|
|
cb(void 0, Array.from(list));
|
|
});
|
|
};
|
|
|
|
Linked.listOldCheckpoints = (Env, channel, cb) => {
|
|
const list = new Set();
|
|
Linked.getLinkedDocuments(Env, { channel }, (err, json) => {
|
|
if (err) { return void cb(err); }
|
|
|
|
const cps = json.checkpoints || [];
|
|
cps.pop(); // preserve last cp
|
|
|
|
cps.forEach(obj => {
|
|
if (obj?.rtChannel) { list.add(obj.rtChannel); }
|
|
if (obj?.blob) { list.add(obj.blob); }
|
|
});
|
|
|
|
cb(void 0, Array.from(list));
|
|
});
|
|
};
|
|
|
|
|
|
const checkContent = (content, user) => {
|
|
const { type, data } = content;
|
|
|
|
if (type === 'checkpoints' && data) {
|
|
const { rtChannel, blob } = data;
|
|
if (rtChannel?.length !== 32 || (blob && blob?.length !== 48)) {
|
|
return false;
|
|
}
|
|
return {
|
|
rtChannel, blob, user,
|
|
time: Date.now()
|
|
};
|
|
}
|
|
|
|
if (type === 'media') {
|
|
return data?.length === 48 ? data : false;
|
|
}
|
|
|
|
if (type === 'channels') {
|
|
return data?.length === 32 ? data : false;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
Linked.addLinkedDocument = (Env, data, cb, _S, userId) => {
|
|
// data.user
|
|
// data.channel
|
|
// data.content
|
|
// type, data (channelId or blobId or checkpoint {blob, rtChannel}})
|
|
// data.proof
|
|
// (sign "{ user, channel, content }" with pad signing key
|
|
|
|
const { user, channel, content, netfluxId, proof } = data;
|
|
if (userId !== netfluxId) { return void cb('EFORBIDDEN'); }
|
|
|
|
const msg = Util.clone(data);
|
|
delete msg.proof;
|
|
const signedMsg = JSON.stringify(msg);
|
|
|
|
const type = content?.type;
|
|
|
|
if (!allowedTypes.includes(type)) {
|
|
return void cb('INVALID_TYPE');
|
|
}
|
|
|
|
const value = checkContent(content, user);
|
|
if (!value) { return void cb('INVALID_CONTENT'); }
|
|
|
|
let validateKey;
|
|
|
|
nThen(waitFor => {
|
|
getMetadata(Env, channel, waitFor((err, metadata) => {
|
|
if (!metadata?.validateKey) {
|
|
waitFor.abort();
|
|
return void cb(err || 'METADATA_ERROR');
|
|
}
|
|
validateKey = metadata.validateKey;
|
|
}));
|
|
}).nThen(waitFor => {
|
|
Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb('INVALID_PROOF');
|
|
}
|
|
}));
|
|
}).nThen(() => {
|
|
Env.store.addLinkedDocument(channel, type, value, cb);
|
|
});
|
|
};
|
|
|
|
Linked.resetLinkedDocuments = (Env, data, cb, _S, userId) => {
|
|
// data.user
|
|
// data.channel
|
|
// data.content
|
|
// data.proof
|
|
// (sign "{ user, channel, content }" with pad signing key
|
|
|
|
const { user, channel, content, netfluxId, proof } = data;
|
|
|
|
if (userId !== netfluxId) { return void cb('EFORBIDDEN'); }
|
|
|
|
const msg = Util.clone(data);
|
|
delete msg.proof;
|
|
const signedMsg = JSON.stringify(msg);
|
|
|
|
let validateKey;
|
|
const newContent = {};
|
|
allowedTypes.forEach(type => { newContent[type] = []; });
|
|
|
|
nThen(waitFor => {
|
|
getMetadata(Env, channel, waitFor((err, metadata) => {
|
|
if (!metadata?.validateKey) {
|
|
waitFor.abort();
|
|
return void cb(err || 'METADATA_ERROR');
|
|
}
|
|
validateKey = metadata.validateKey;
|
|
}));
|
|
}).nThen(waitFor => {
|
|
Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb('INVALID_PROOF');
|
|
}
|
|
}));
|
|
}).nThen(waitFor => {
|
|
Linked.getLinkedDocuments(Env, { channel }, waitFor((err, json = {}) => {
|
|
// checkpoints
|
|
if (Array.isArray(content?.checkpoints)) {
|
|
const old = json?.checkpoints || [];
|
|
// add last 10 valid checkpoints
|
|
let i = 0;
|
|
content.checkpoints.reverse().some(data => {
|
|
// If cp already exists, recover user and time
|
|
// Otherwise, check integrity of new value and add them now
|
|
const oldValue = old.find(obj => {
|
|
return obj.blob === data.blob &&
|
|
obj.rtChannel === data.rtChannel;
|
|
});
|
|
const toAdd = oldValue || checkContent({
|
|
type: 'checkpoints',
|
|
data
|
|
}, user);
|
|
if (!toAdd) { return false; }
|
|
newContent.checkpoints.unshift(toAdd);
|
|
|
|
// Abort after 10 cps
|
|
if (++i >= 10) { return true; }
|
|
});
|
|
}
|
|
|
|
// channels and media
|
|
['channels', 'media'].forEach(type => {
|
|
if (!Array.isArray(content?.[type])) { return; }
|
|
content[type].forEach(data => {
|
|
const toAdd = checkContent({type, data}, user);
|
|
if (!toAdd) { return false; }
|
|
newContent[type].push(toAdd);
|
|
});
|
|
});
|
|
}));
|
|
}).nThen(() => {
|
|
Env.store.resetLinkedDocuments(channel, newContent, (err, data) => {
|
|
const { oldContent } = data;
|
|
Env.Log.info('RESET_LINKED_DOCUMENTS', {user, channel, oldContent, content});
|
|
cb();
|
|
});
|
|
});
|
|
};
|
|
|
|
Linked.removeLinkedDocument = (Env, allData, cb, _S, userId) => {
|
|
// data.user
|
|
// data.channel
|
|
// data.content
|
|
// type, channelId or blobId
|
|
// data.proof
|
|
// (sign "{ user, channel, content }" with pad signing key
|
|
|
|
const { channel, content, netfluxId, proof } = allData;
|
|
|
|
if (userId !== netfluxId) { return void cb('EFORBIDDEN'); }
|
|
|
|
const { type, data } = content;
|
|
|
|
const msg = Util.clone(data);
|
|
delete msg.proof;
|
|
const signedMsg = JSON.stringify(msg);
|
|
|
|
if (!allowedTypes.includes(type)) {
|
|
return void cb('INVALID_TYPE');
|
|
}
|
|
|
|
if (typeof(data) !== "string" || ![32,48].includes(data.length)) {
|
|
return void cb('INVALID_CONTENT');
|
|
}
|
|
|
|
let validateKey;
|
|
nThen(waitFor => {
|
|
getMetadata(Env, channel, waitFor((err, metadata) => {
|
|
if (!metadata?.validateKey) {
|
|
waitFor.abort();
|
|
return void cb(err || 'METADATA_ERROR');
|
|
}
|
|
validateKey = metadata.validateKey;
|
|
}));
|
|
}).nThen(waitFor => {
|
|
Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb('INVALID_PROOF');
|
|
}
|
|
}));
|
|
}).nThen(() => {
|
|
Env.store.removeLinkedDocument(channel, type, data, cb);
|
|
});
|
|
};
|
|
|
|
Linked.getFileSize = (Env, data, _cb) => {
|
|
const cb = Util.once(_cb);
|
|
const channel = data.channel;
|
|
let linked;
|
|
nThen(waitFor => {
|
|
Linked.listLinkedDocuments(Env, channel, waitFor((err, channels) => {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb(err);
|
|
}
|
|
linked = channels || [];
|
|
}));
|
|
}).nThen(() => {
|
|
linked.push(channel);
|
|
Env.getTotalSize(linked, cb);
|
|
});
|
|
};
|
|
|
|
Linked.getHistorySize = (Env, data, _cb) => {
|
|
const cb = Util.once(_cb);
|
|
const channel = data.channel;
|
|
let linked;
|
|
let channelTotalSize = 0;
|
|
let size = 0;
|
|
let start = 0;
|
|
let hash;
|
|
|
|
nThen(waitFor => {
|
|
Linked.getLinkedDocuments(Env, data, waitFor((err, json) => {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb(err);
|
|
}
|
|
linked = Util.clone(json);
|
|
}));
|
|
}).nThen(waitFor => {
|
|
// Get main channel size (chainpad)
|
|
Env.getFileSize(channel, waitFor((err, _size) => {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb(err);
|
|
}
|
|
channelTotalSize = _size;
|
|
}), true);
|
|
}).nThen(waitFor => {
|
|
// Get history offset to compute non-history size
|
|
HK.getHistoryOffset(Env, channel, null, waitFor((err, offset) => {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb(err);
|
|
}
|
|
start = offset;
|
|
const chanSize = channelTotalSize - offset;
|
|
size += chanSize;
|
|
}));
|
|
}).nThen(waitFor => {
|
|
// Get oldest hash of non-history data
|
|
Env.store.readMessagesBin(channel, start, (msgObj, readMore, abort) => {
|
|
const parsed = Util.tryParse(msgObj.buff.toString('utf8'));
|
|
if (!parsed) { return void readMore(); }
|
|
hash = HK.getHash(parsed[4]);
|
|
abort();
|
|
}, waitFor());
|
|
|
|
}).nThen(waitFor => {
|
|
// Get last checkpoint size (blob + rtChannel)
|
|
// Note: blob may be falsy if no checkpoint
|
|
|
|
const lastCp = (linked?.checkpoints || []).pop();
|
|
if (!lastCp) { return; }
|
|
const { blob, rtChannel } = lastCp;
|
|
|
|
if (blob) {
|
|
Env.getFileSize(blob, waitFor((err, _size) => {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb(err);
|
|
}
|
|
size += _size;
|
|
}), true);
|
|
}
|
|
|
|
Env.getFileSize(rtChannel, waitFor((err, _size) => {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb(err);
|
|
}
|
|
size += _size;
|
|
}), true);
|
|
}).nThen(() => {
|
|
cb(void 0, {
|
|
size, hash
|
|
});
|
|
});
|
|
};
|
|
|
|
Linked.trimHistory = (Env, data, cb) => {
|
|
const channel = data.channel;
|
|
let linked;
|
|
// if we reach this step, it means this user is an owner of "channel"
|
|
// so we can also delete any document linked to "channel" (from metadata)
|
|
nThen(waitFor => {
|
|
// List all but the current checkpoints
|
|
Linked.listOldCheckpoints(Env, channel, waitFor((err, channels) => {
|
|
if (err) {
|
|
waitFor.abort();
|
|
return void cb(err);
|
|
}
|
|
linked = channels || [];
|
|
}));
|
|
}).nThen(() => {
|
|
let n = nThen;
|
|
linked.forEach(chan => {
|
|
n = n(w => {
|
|
// If channel is "linked", we can archive all but last cp
|
|
getMetadata(Env, chan, w((err, md) => {
|
|
if (md?.linked !== channel) { return; }
|
|
// This is an old checkpoint linked to our document,
|
|
// we can archive it
|
|
const reason = "TRIM_HISTORY";
|
|
if (chan.length === HK.BLOB_ID_LENGTH) {
|
|
return Env.blobStore.archive.blob(chan, reason, w());
|
|
}
|
|
Env.store.archiveChannel(chan, reason, w());
|
|
}));
|
|
}).nThen;
|
|
});
|
|
n(() => {
|
|
cb();
|
|
});
|
|
});
|
|
};
|
|
|
|
// Archive all linked documents that inherit metadata from their
|
|
// parent. We consider ownership has already been checked when
|
|
// this function is called.
|
|
Linked.archiveLinkedData = (Env, channel, reason, channels, _cb) => {
|
|
const cb = Util.once(_cb);
|
|
let n = nThen;
|
|
channels.forEach(chan => {
|
|
n = n(w => {
|
|
// For each linked document, check if they inherit properties
|
|
getMetadata(Env, chan, w((err, md) => {
|
|
if (md?.linked !== channel) { return; }
|
|
// If they do, archive the document
|
|
if (chan.length === HK.BLOB_ID_LENGTH) {
|
|
return Env.blobStore.archive.blob(chan, reason, w());
|
|
}
|
|
Env.store.archiveChannel(chan, reason, w());
|
|
}));
|
|
}).nThen;
|
|
});
|
|
n(() => {
|
|
cb();
|
|
});
|
|
};
|