initial implementation of serverside password change RPCs

This commit is contained in:
ansuz 2018-06-19 11:31:15 +02:00
parent 2463dd0c9e
commit 9d3d8c3ba2
3 changed files with 179 additions and 0 deletions

View File

@ -211,6 +211,11 @@ module.exports = {
*/
taskPath: './tasks',
/* if you would like users' authenticated blocks to be stored in
a custom location, change the path below:
*/
blockPath: './block',
/*
* By default, CryptPad also contacts our accounts server once a day to check for changes in
* the people who have accounts. This check-in will also send the version of your CryptPad

171
rpc.js
View File

@ -1297,6 +1297,164 @@ var upload_status = function (Env, publicKey, filesize, 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) {
// 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) {
console.error(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) {
// TODO print to console
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);
// signature 64 bytes
// sign.detached(hash(decodeBase64_content(base64_content)), decodeBase64(publicKey))
// 1 byte version
// base64_content
};
var createLoginBlockPath = function (Env, publicKey) {
// 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) {
console.log(msg); // XXX
var publicKey = msg[0];
var signature = msg[1];
var block = msg[2];
validateLoginBlock(Env, publicKey, signature, block, function (e, verified_block) {
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');
}
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
Mkdirp(parsed.dir, w(function (e) {
if (e) {
w.abort();
cb(e);
}
}));
}).nThen(function () {
// actually write the block
Fs.writeFile(path, new Buffer(verified_block), { 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) {
console.log(msg); // XXX
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) {
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');
}
Fs.unlink(path, function (err) {
if (err) { return void cb(err); }
cb();
});
});
};
var isNewChannel = function (Env, channel, cb) {
if (!isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length !== 32) { return void cb('INVALID_CHAN'); }
@ -1353,6 +1511,8 @@ var isAuthenticatedCall = function (call) {
'CLEAR_OWNED_CHANNEL',
'REMOVE_OWNED_CHANNEL',
'REMOVE_PINS',
'WRITE_LOGIN_BLOCK',
'REMOVE_LOGIN_BLOCK',
].indexOf(call) !== -1;
};
@ -1423,6 +1583,7 @@ RPC.create = function (
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob');
var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
paths.block = keyOrDefaultString('blockPath', './block');
var isUnauthenticateMessage = function (msg) {
return msg && msg.length === 2 && isUnauthenticatedCall(msg[0]);
@ -1692,6 +1853,16 @@ RPC.create = function (
WARN(e, 'UPLOAD_CANCEL');
Respond(e);
});
case 'WRITE_LOGIN_BLOCK':
return void writeLoginBlock(Env, msg, function (e) {
// TODO handle response
e = e;
});
case 'REMOVE_LOGIN_BLOCK':
return void removeLoginBlock(Env, msg, function (e) {
// TODO handle response
e = e;
});
default:
return void Respond('UNSUPPORTED_RPC_CALL', msg);
}

View File

@ -126,6 +126,9 @@ app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob
app.use("/datastore", Express.static(Path.join(__dirname, (config.filePath || './datastore')), {
maxAge: "0d"
}));
app.use("/block", Express.static(Path.join(__dirname, (config.blockPath || '/block')), {
maxAge: "0d",
}));
app.use("/customize", Express.static(__dirname + '/customize'));
app.use("/customize", Express.static(__dirname + '/customize.dist'));