mirror of https://github.com/xwiki-labs/cryptpad
move login block functionality into its own rpc module
This commit is contained in:
parent
c765362744
commit
bb7e8e4512
|
@ -0,0 +1,172 @@
|
||||||
|
/*jshint esversion: 6 */
|
||||||
|
/* globals Buffer*/
|
||||||
|
var Block = module.exports;
|
||||||
|
|
||||||
|
const Fs = require("fs");
|
||||||
|
const Fse = require("fs-extra");
|
||||||
|
const Path = require("path");
|
||||||
|
const Nacl = require("tweetnacl/nacl-fast");
|
||||||
|
const nThen = require("nthen");
|
||||||
|
|
||||||
|
const Util = require("../common-util");
|
||||||
|
|
||||||
|
/*
|
||||||
|
We assume that the server is secured against MitM attacks
|
||||||
|
via HTTPS, and that malicious actors do not have code execution
|
||||||
|
capabilities. If they do, we have much more serious problems.
|
||||||
|
|
||||||
|
The capability to replay a block write or remove results in either
|
||||||
|
a denial of service for the user whose block was removed, or in the
|
||||||
|
case of a write, a rollback to an earlier password.
|
||||||
|
|
||||||
|
Since block modification is destructive, this can result in loss
|
||||||
|
of access to the user's drive.
|
||||||
|
|
||||||
|
So long as the detached signature is never observed by a malicious
|
||||||
|
party, and the server discards it after proof of knowledge, replays
|
||||||
|
are not possible. However, this precludes verification of the signature
|
||||||
|
at a later time.
|
||||||
|
|
||||||
|
Despite this, an integrity check is still possible by the original
|
||||||
|
author of the block, since we assume that the block will have been
|
||||||
|
encrypted with xsalsa20-poly1305 which is authenticated.
|
||||||
|
*/
|
||||||
|
Block.validateLoginBlock = function (Env, publicKey, signature, block, cb) { // FIXME BLOCKS
|
||||||
|
// convert the public key to a Uint8Array and validate it
|
||||||
|
if (typeof(publicKey) !== 'string') { return void cb('E_INVALID_KEY'); }
|
||||||
|
|
||||||
|
var u8_public_key;
|
||||||
|
try {
|
||||||
|
u8_public_key = Nacl.util.decodeBase64(publicKey);
|
||||||
|
} catch (e) {
|
||||||
|
return void cb('E_INVALID_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
var u8_signature;
|
||||||
|
try {
|
||||||
|
u8_signature = Nacl.util.decodeBase64(signature);
|
||||||
|
} catch (e) {
|
||||||
|
Env.Log.error('INVALID_BLOCK_SIGNATURE', e);
|
||||||
|
return void cb('E_INVALID_SIGNATURE');
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert the block to a Uint8Array
|
||||||
|
var u8_block;
|
||||||
|
try {
|
||||||
|
u8_block = Nacl.util.decodeBase64(block);
|
||||||
|
} catch (e) {
|
||||||
|
return void cb('E_INVALID_BLOCK');
|
||||||
|
}
|
||||||
|
|
||||||
|
// take its hash
|
||||||
|
var hash = Nacl.hash(u8_block);
|
||||||
|
|
||||||
|
// validate the signature against the hash of the content
|
||||||
|
var verified = Nacl.sign.detached.verify(hash, u8_signature, u8_public_key);
|
||||||
|
|
||||||
|
// existing authentication ensures that users cannot replay old blocks
|
||||||
|
|
||||||
|
// call back with (err) if unsuccessful
|
||||||
|
if (!verified) { return void cb("E_COULD_NOT_VERIFY"); }
|
||||||
|
|
||||||
|
return void cb(null, u8_block);
|
||||||
|
};
|
||||||
|
|
||||||
|
var createLoginBlockPath = function (Env, publicKey) { // FIXME BLOCKS
|
||||||
|
// prepare publicKey to be used as a file name
|
||||||
|
var safeKey = Util.escapeKeyCharacters(publicKey);
|
||||||
|
|
||||||
|
// validate safeKey
|
||||||
|
if (typeof(safeKey) !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// derive the full path
|
||||||
|
// /home/cryptpad/cryptpad/block/fg/fg32kefksjdgjkewrjksdfksjdfsdfskdjfsfd
|
||||||
|
return Path.join(Env.paths.block, safeKey.slice(0, 2), safeKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
Block.writeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS
|
||||||
|
//console.log(msg);
|
||||||
|
var publicKey = msg[0];
|
||||||
|
var signature = msg[1];
|
||||||
|
var block = msg[2];
|
||||||
|
|
||||||
|
Block.validateLoginBlock(Env, publicKey, signature, block, function (e, validatedBlock) {
|
||||||
|
if (e) { return void cb(e); }
|
||||||
|
if (!(validatedBlock instanceof Uint8Array)) { return void cb('E_INVALID_BLOCK'); }
|
||||||
|
|
||||||
|
// derive the filepath
|
||||||
|
var path = createLoginBlockPath(Env, publicKey);
|
||||||
|
|
||||||
|
// make sure the path is valid
|
||||||
|
if (typeof(path) !== 'string') {
|
||||||
|
return void cb('E_INVALID_BLOCK_PATH');
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed = Path.parse(path);
|
||||||
|
if (!parsed || typeof(parsed.dir) !== 'string') {
|
||||||
|
return void cb("E_INVALID_BLOCK_PATH_2");
|
||||||
|
}
|
||||||
|
|
||||||
|
nThen(function (w) {
|
||||||
|
// make sure the path to the file exists
|
||||||
|
Fse.mkdirp(parsed.dir, w(function (e) {
|
||||||
|
if (e) {
|
||||||
|
w.abort();
|
||||||
|
cb(e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}).nThen(function () {
|
||||||
|
// actually write the block
|
||||||
|
|
||||||
|
// flow is dumb and I need to guard against this which will never happen
|
||||||
|
/*:: if (typeof(validatedBlock) === 'undefined') { throw new Error('should never happen'); } */
|
||||||
|
/*:: if (typeof(path) === 'undefined') { throw new Error('should never happen'); } */
|
||||||
|
Fs.writeFile(path, Buffer.from(validatedBlock), { encoding: "binary", }, function (err) {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
When users write a block, they upload the block, and provide
|
||||||
|
a signature proving that they deserve to be able to write to
|
||||||
|
the location determined by the public key.
|
||||||
|
|
||||||
|
When removing a block, there is nothing to upload, but we need
|
||||||
|
to sign something. Since the signature is considered sensitive
|
||||||
|
information, we can just sign some constant and use that as proof.
|
||||||
|
|
||||||
|
*/
|
||||||
|
Block.removeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS
|
||||||
|
var publicKey = msg[0];
|
||||||
|
var signature = msg[1];
|
||||||
|
var block = Nacl.util.decodeUTF8('DELETE_BLOCK'); // clients and the server will have to agree on this constant
|
||||||
|
|
||||||
|
Block.validateLoginBlock(Env, publicKey, signature, block, function (e /*::, validatedBlock */) {
|
||||||
|
if (e) { return void cb(e); }
|
||||||
|
// derive the filepath
|
||||||
|
var path = createLoginBlockPath(Env, publicKey);
|
||||||
|
|
||||||
|
// make sure the path is valid
|
||||||
|
if (typeof(path) !== 'string') {
|
||||||
|
return void cb('E_INVALID_BLOCK_PATH');
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME COLDSTORAGE
|
||||||
|
Fs.unlink(path, function (err) {
|
||||||
|
Env.Log.info('DELETION_BLOCK_BY_OWNER_RPC', {
|
||||||
|
publicKey: publicKey,
|
||||||
|
path: path,
|
||||||
|
status: err? String(err): 'SUCCESS',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
175
lib/rpc.js
175
lib/rpc.js
|
@ -1,13 +1,4 @@
|
||||||
/*jshint esversion: 6 */
|
/*jshint esversion: 6 */
|
||||||
/* Use Nacl for checking signatures of messages */
|
|
||||||
var Nacl = require("tweetnacl/nacl-fast");
|
|
||||||
|
|
||||||
/* globals Buffer*/
|
|
||||||
|
|
||||||
var Fs = require("fs");
|
|
||||||
|
|
||||||
var Fse = require("fs-extra");
|
|
||||||
var Path = require("path");
|
|
||||||
const nThen = require("nthen");
|
const nThen = require("nthen");
|
||||||
const Meta = require("./metadata");
|
const Meta = require("./metadata");
|
||||||
const WriteQueue = require("./write-queue");
|
const WriteQueue = require("./write-queue");
|
||||||
|
@ -21,6 +12,7 @@ const Core = require("./commands/core");
|
||||||
const Admin = require("./commands/admin-rpc");
|
const Admin = require("./commands/admin-rpc");
|
||||||
const Pinning = require("./commands/pin-rpc");
|
const Pinning = require("./commands/pin-rpc");
|
||||||
const Quota = require("./commands/quota");
|
const Quota = require("./commands/quota");
|
||||||
|
const Block = require("./commands/block");
|
||||||
|
|
||||||
var RPC = module.exports;
|
var RPC = module.exports;
|
||||||
|
|
||||||
|
@ -249,167 +241,6 @@ var removeOwnedChannelHistory = function (Env, channelId, unsafeKey, hash, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
We assume that the server is secured against MitM attacks
|
|
||||||
via HTTPS, and that malicious actors do not have code execution
|
|
||||||
capabilities. If they do, we have much more serious problems.
|
|
||||||
|
|
||||||
The capability to replay a block write or remove results in either
|
|
||||||
a denial of service for the user whose block was removed, or in the
|
|
||||||
case of a write, a rollback to an earlier password.
|
|
||||||
|
|
||||||
Since block modification is destructive, this can result in loss
|
|
||||||
of access to the user's drive.
|
|
||||||
|
|
||||||
So long as the detached signature is never observed by a malicious
|
|
||||||
party, and the server discards it after proof of knowledge, replays
|
|
||||||
are not possible. However, this precludes verification of the signature
|
|
||||||
at a later time.
|
|
||||||
|
|
||||||
Despite this, an integrity check is still possible by the original
|
|
||||||
author of the block, since we assume that the block will have been
|
|
||||||
encrypted with xsalsa20-poly1305 which is authenticated.
|
|
||||||
*/
|
|
||||||
var validateLoginBlock = function (Env, publicKey, signature, block, cb) { // FIXME BLOCKS
|
|
||||||
// convert the public key to a Uint8Array and validate it
|
|
||||||
if (typeof(publicKey) !== 'string') { return void cb('E_INVALID_KEY'); }
|
|
||||||
|
|
||||||
var u8_public_key;
|
|
||||||
try {
|
|
||||||
u8_public_key = Nacl.util.decodeBase64(publicKey);
|
|
||||||
} catch (e) {
|
|
||||||
return void cb('E_INVALID_KEY');
|
|
||||||
}
|
|
||||||
|
|
||||||
var u8_signature;
|
|
||||||
try {
|
|
||||||
u8_signature = Nacl.util.decodeBase64(signature);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('INVALID_BLOCK_SIGNATURE', e);
|
|
||||||
return void cb('E_INVALID_SIGNATURE');
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert the block to a Uint8Array
|
|
||||||
var u8_block;
|
|
||||||
try {
|
|
||||||
u8_block = Nacl.util.decodeBase64(block);
|
|
||||||
} catch (e) {
|
|
||||||
return void cb('E_INVALID_BLOCK');
|
|
||||||
}
|
|
||||||
|
|
||||||
// take its hash
|
|
||||||
var hash = Nacl.hash(u8_block);
|
|
||||||
|
|
||||||
// validate the signature against the hash of the content
|
|
||||||
var verified = Nacl.sign.detached.verify(hash, u8_signature, u8_public_key);
|
|
||||||
|
|
||||||
// existing authentication ensures that users cannot replay old blocks
|
|
||||||
|
|
||||||
// call back with (err) if unsuccessful
|
|
||||||
if (!verified) { return void cb("E_COULD_NOT_VERIFY"); }
|
|
||||||
|
|
||||||
return void cb(null, u8_block);
|
|
||||||
};
|
|
||||||
|
|
||||||
var createLoginBlockPath = function (Env, publicKey) { // FIXME BLOCKS
|
|
||||||
// prepare publicKey to be used as a file name
|
|
||||||
var safeKey = escapeKeyCharacters(publicKey);
|
|
||||||
|
|
||||||
// validate safeKey
|
|
||||||
if (typeof(safeKey) !== 'string') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// derive the full path
|
|
||||||
// /home/cryptpad/cryptpad/block/fg/fg32kefksjdgjkewrjksdfksjdfsdfskdjfsfd
|
|
||||||
return Path.join(Env.paths.block, safeKey.slice(0, 2), safeKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
var writeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS
|
|
||||||
//console.log(msg);
|
|
||||||
var publicKey = msg[0];
|
|
||||||
var signature = msg[1];
|
|
||||||
var block = msg[2];
|
|
||||||
|
|
||||||
validateLoginBlock(Env, publicKey, signature, block, function (e, validatedBlock) {
|
|
||||||
if (e) { return void cb(e); }
|
|
||||||
if (!(validatedBlock instanceof Uint8Array)) { return void cb('E_INVALID_BLOCK'); }
|
|
||||||
|
|
||||||
// derive the filepath
|
|
||||||
var path = createLoginBlockPath(Env, publicKey);
|
|
||||||
|
|
||||||
// make sure the path is valid
|
|
||||||
if (typeof(path) !== 'string') {
|
|
||||||
return void cb('E_INVALID_BLOCK_PATH');
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsed = Path.parse(path);
|
|
||||||
if (!parsed || typeof(parsed.dir) !== 'string') {
|
|
||||||
return void cb("E_INVALID_BLOCK_PATH_2");
|
|
||||||
}
|
|
||||||
|
|
||||||
nThen(function (w) {
|
|
||||||
// make sure the path to the file exists
|
|
||||||
Fse.mkdirp(parsed.dir, w(function (e) {
|
|
||||||
if (e) {
|
|
||||||
w.abort();
|
|
||||||
cb(e);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}).nThen(function () {
|
|
||||||
// actually write the block
|
|
||||||
|
|
||||||
// flow is dumb and I need to guard against this which will never happen
|
|
||||||
/*:: if (typeof(validatedBlock) === 'undefined') { throw new Error('should never happen'); } */
|
|
||||||
/*:: if (typeof(path) === 'undefined') { throw new Error('should never happen'); } */
|
|
||||||
Fs.writeFile(path, Buffer.from(validatedBlock), { encoding: "binary", }, function (err) {
|
|
||||||
if (err) { return void cb(err); }
|
|
||||||
cb();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
When users write a block, they upload the block, and provide
|
|
||||||
a signature proving that they deserve to be able to write to
|
|
||||||
the location determined by the public key.
|
|
||||||
|
|
||||||
When removing a block, there is nothing to upload, but we need
|
|
||||||
to sign something. Since the signature is considered sensitive
|
|
||||||
information, we can just sign some constant and use that as proof.
|
|
||||||
|
|
||||||
*/
|
|
||||||
var removeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS
|
|
||||||
var publicKey = msg[0];
|
|
||||||
var signature = msg[1];
|
|
||||||
var block = Nacl.util.decodeUTF8('DELETE_BLOCK'); // clients and the server will have to agree on this constant
|
|
||||||
|
|
||||||
validateLoginBlock(Env, publicKey, signature, block, function (e /*::, validatedBlock */) {
|
|
||||||
if (e) { return void cb(e); }
|
|
||||||
// derive the filepath
|
|
||||||
var path = createLoginBlockPath(Env, publicKey);
|
|
||||||
|
|
||||||
// make sure the path is valid
|
|
||||||
if (typeof(path) !== 'string') {
|
|
||||||
return void cb('E_INVALID_BLOCK_PATH');
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME COLDSTORAGE
|
|
||||||
Fs.unlink(path, function (err) {
|
|
||||||
Log.info('DELETION_BLOCK_BY_OWNER_RPC', {
|
|
||||||
publicKey: publicKey,
|
|
||||||
path: path,
|
|
||||||
status: err? String(err): 'SUCCESS',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (err) { return void cb(err); }
|
|
||||||
cb();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var ARRAY_LINE = /^\[/;
|
var ARRAY_LINE = /^\[/;
|
||||||
|
|
||||||
/* Files can contain metadata but not content
|
/* Files can contain metadata but not content
|
||||||
|
@ -863,7 +694,7 @@ RPC.create = function (config, cb) {
|
||||||
Respond(e);
|
Respond(e);
|
||||||
});
|
});
|
||||||
case 'WRITE_LOGIN_BLOCK':
|
case 'WRITE_LOGIN_BLOCK':
|
||||||
return void writeLoginBlock(Env, msg[1], function (e) {
|
return void Block.writeLoginBlock(Env, msg[1], function (e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
WARN(e, 'WRITE_LOGIN_BLOCK');
|
WARN(e, 'WRITE_LOGIN_BLOCK');
|
||||||
return void Respond(e);
|
return void Respond(e);
|
||||||
|
@ -871,7 +702,7 @@ RPC.create = function (config, cb) {
|
||||||
Respond(e);
|
Respond(e);
|
||||||
});
|
});
|
||||||
case 'REMOVE_LOGIN_BLOCK':
|
case 'REMOVE_LOGIN_BLOCK':
|
||||||
return void removeLoginBlock(Env, msg[1], function (e) {
|
return void Block.removeLoginBlock(Env, msg[1], function (e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
WARN(e, 'REMOVE_LOGIN_BLOCK');
|
WARN(e, 'REMOVE_LOGIN_BLOCK');
|
||||||
return void Respond(e);
|
return void Respond(e);
|
||||||
|
|
Loading…
Reference in New Issue