From 9d3d8c3ba206b08ab4f2f42193e2f32dfb630a06 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 19 Jun 2018 11:31:15 +0200 Subject: [PATCH 01/18] initial implementation of serverside password change RPCs --- config.example.js | 5 ++ rpc.js | 171 ++++++++++++++++++++++++++++++++++++++++++++++ server.js | 3 + 3 files changed, 179 insertions(+) diff --git a/config.example.js b/config.example.js index 166fd74a8..1d85d6c96 100644 --- a/config.example.js +++ b/config.example.js @@ -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 diff --git a/rpc.js b/rpc.js index 69d84b6dd..6970c7736 100644 --- a/rpc.js +++ b/rpc.js @@ -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); } diff --git a/server.js b/server.js index aac8d7513..02cb54029 100644 --- a/server.js +++ b/server.js @@ -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')); From 577dea4c75ab49ef09df3bbdb63ea7c06e3036fe Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 19 Jun 2018 16:38:49 +0200 Subject: [PATCH 02/18] clientside implementation of block signing and encryption --- .gitignore | 1 + customize.dist/login.js | 2 +- rpc.js | 12 ++--- www/common/common-util.js | 15 +++++++ www/common/outer/login-block.js | 79 +++++++++++++++++++++++++++++++++ www/common/pinpad.js | 14 +++++- 6 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 www/common/outer/login-block.js diff --git a/.gitignore b/.gitignore index 741aedaf7..656e1f378 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,6 @@ data npm-debug.log pins/ blob/ +block/ blobstage/ privileged.conf diff --git a/customize.dist/login.js b/customize.dist/login.js index 4d2855b16..17e3daab1 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -105,7 +105,7 @@ define([ return void cb('PASS_TOO_SHORT'); } - Cred.deriveFromPassphrase(uname, passwd, 128, function (bytes) { + Cred.deriveFromPassphrase(uname, passwd, 192, function (bytes) { // results... var res = { register: isRegister, diff --git a/rpc.js b/rpc.js index 6970c7736..f07af7c4c 100644 --- a/rpc.js +++ b/rpc.js @@ -1342,7 +1342,6 @@ var validateLoginBlock = function (Env, publicKey, signature, block, cb) { try { u8_block = Nacl.util.decodeBase64(block); } catch (e) { - // TODO print to console return void cb('E_INVALID_BLOCK'); } @@ -1432,8 +1431,6 @@ var writeLoginBlock = function (Env, msg, cb) { */ 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 @@ -1854,9 +1851,12 @@ RPC.create = function ( Respond(e); }); case 'WRITE_LOGIN_BLOCK': - return void writeLoginBlock(Env, msg, function (e) { - // TODO handle response - e = e; + return void writeLoginBlock(Env, msg[1], function (e) { + if (e) { + WARN(e, 'WRITE_LOGIN_BLOCK'); + return void Respond(e); + } + Respond(e); }); case 'REMOVE_LOGIN_BLOCK': return void removeLoginBlock(Env, msg, function (e) { diff --git a/www/common/common-util.js b/www/common/common-util.js index a67ba6673..2a30133e9 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -83,6 +83,21 @@ define([], function () { }).join(''); }; + // given an array of Uint8Arrays, return a new Array with all their values + Util.uint8ArrayJoin = function (AA) { + var l = 0; + var i = 0; + for (; i < AA.length; i++) { l += AA[i].length; } + var C = new Uint8Array(l); + + i = 0; + for (var offset = 0; i < AA.length; i++) { + C.set(AA[i], offset); + offset += AA[i].length; + } + return C; + }; + Util.deduplicateString = function (array) { var a = array.slice(); for(var i=0; i + + // signature + + // block + + // [b64_public, b64_sig, b64_block [version, nonce, content]] + + Block.seed = function () { + return Nacl.hash(Nacl.util.decodeUTF8('pewpewpew')); + }; + + // should be deterministic from a seed... + Block.genkeys = function (seed) { + if (!seed || typeof(seed.length) !== 'number' || seed.length < 64) { + throw new Error('INVALID_SEED_LENGTH'); + } + + var signSeed = seed.subarray(0, Nacl.sign.seedLength); + var symmetric = seed.subarray(Nacl.sign.seedLength, + Nacl.sign.seedLength + Nacl.secretbox.keyLength); + + return { + sign: Nacl.sign.keyPair.fromSeed(signSeed), // 32 bytes + symmetric: symmetric, + }; + }; + + // (UTF8 content, keys object) => Uint8Array block + Block.encrypt = function (version, content, keys) { + var u8 = Nacl.util.decodeUTF8(content); + var nonce = Nacl.randomBytes(Nacl.secretbox.nonceLength); + return Block.join([ + [0], + nonce, + Nacl.secretbox(u8, nonce, keys.symmetric) + ]); + }; + + // (uint8Array block) => payload object + Block.decrypt = function (u8_content, keys) { + // version is currently ignored since there is only one + var nonce = u8_content.subarray(1, 1 + Nacl.secretbox.nonceLength); + var box = content.subarray(1 + Nacl.secretbox.nonceLength); + return Nacl.secretbox.open(box, nonce, keys.symmetric); + }; + + // (Uint8Array block) => signature + Block.sign = function (ciphertext, keys) { + return Nacl.sign.detached(Nacl.hash(ciphertext), keys.sign.secretKey); + }; + + Block.serialize = function (content, keys) { + // encrypt the content + var ciphertext = Block.encrypt(0, content, keys); + + // generate a detached signature + var sig = Block.sign(ciphertext, keys); + + // serialize {publickey, sig, ciphertext} + return { + publicKey: Nacl.util.encodeBase64(keys.sign.publicKey), + signature: Nacl.util.encodeBase64(sig), + ciphertext: Nacl.util.encodeBase64(ciphertext), + }; + }; + + return Block; +}); diff --git a/www/common/pinpad.js b/www/common/pinpad.js index 1e4dd6046..da1e51d28 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -222,7 +222,19 @@ define([ }; exp.writeLoginBlock = function (data, cb) { - cb(); + if (!data) { return void cb('NO_DATA'); } + if (!data.publicKey || !data.signature || !data.ciphertext) { + console.log(data); + return void cb("MISSING_PARAMETERS"); + } + + rpc.send('WRITE_LOGIN_BLOCK', [ + data.publicKey, + data.signature, + data.ciphertext + ], function (e) { + cb(e); + }); }; cb(e, exp); From cb5fa72dbad2b4420a80c301e1b7bda4705f283d Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 19 Jun 2018 17:17:56 +0200 Subject: [PATCH 03/18] implement removeLoginBlock rpc --- rpc.js | 9 ++++++--- www/common/cryptpad-common.js | 6 ++++++ www/common/outer/async-store.js | 11 ++++++++++- www/common/outer/login-block.js | 11 +++++++++++ www/common/outer/store-rpc.js | 1 + www/common/pinpad.js | 15 +++++++++++++++ www/common/sframe-common-outer.js | 4 ++++ www/common/sframe-protocol.js | 3 +++ 8 files changed, 56 insertions(+), 4 deletions(-) diff --git a/rpc.js b/rpc.js index f07af7c4c..d10fe6149 100644 --- a/rpc.js +++ b/rpc.js @@ -1859,9 +1859,12 @@ RPC.create = function ( Respond(e); }); case 'REMOVE_LOGIN_BLOCK': - return void removeLoginBlock(Env, msg, function (e) { - // TODO handle response - e = e; + return void removeLoginBlock(Env, msg[1], function (e) { + if (e) { + WARN(e, 'REMOVE_LOGIN_BLOCK'); + return void Respond(e); + } + Respond(e); }); default: return void Respond('UNSUPPORTED_RPC_CALL', msg); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index b3f3edb63..d095d2b3c 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -240,6 +240,12 @@ define([ }); }; + common.removeLoginBlock = function (data, cb) { + postMessage('REMOVE_LOGIN_BLOCK', data, function (obj) { + cb(obj); + }); + }; + // ANON RPC // SFRAME: talk to anon_rpc from the iframe diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e703b6a5b..e22e2d574 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -13,7 +13,7 @@ define([ '/common/outer/network-config.js', '/customize/application_config.js', - '/bower_components/chainpad-crypto/crypto.js?v=0.1.5', + '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad/chainpad.dist.js', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/nthen/index.js', @@ -285,6 +285,15 @@ define([ }); }; + Store.removeLoginBlock = function (clientId, data, cb) { + store.rpc.removeLoginBlock(data, function (e, res) { + cb({ + error: e, + data: res + }); + }); + }; + Store.initRpc = function (clientId, data, cb) { if (store.rpc) { return void cb(account); } require(['/common/pinpad.js'], function (Pinpad) { diff --git a/www/common/outer/login-block.js b/www/common/outer/login-block.js index bac63f2f8..026fa289f 100644 --- a/www/common/outer/login-block.js +++ b/www/common/outer/login-block.js @@ -75,5 +75,16 @@ define([ }; }; + Block.remove = function (keys) { + // sign the hash of the text 'DELETE_BLOCK' + var sig = Nacl.sign.detached(Nacl.hash( + Nacl.util.decodeUTF8('DELETE_BLOCK')), keys.sign.secretKey); + + return { + publicKey: Nacl.util.encodeBase64(keys.sign.publicKey), + signature: Nacl.util.encodeBase64(sig), + }; + }; + return Block; }); diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index a2b7d2404..e1a941040 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -24,6 +24,7 @@ define([ UPLOAD_STATUS: Store.uploadStatus, UPLOAD_CANCEL: Store.uploadCancel, WRITE_LOGIN_BLOCK: Store.writeLoginBlock, + REMOVE_LOGIN_BLOCK: Store.removeLoginBlock, PIN_PADS: Store.pinPads, UNPIN_PADS: Store.unpinPads, GET_DELETED_PADS: Store.getDeletedPads, diff --git a/www/common/pinpad.js b/www/common/pinpad.js index da1e51d28..721f8a94b 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -237,6 +237,21 @@ define([ }); }; + exp.removeLoginBlock = function (data, cb) { + if (!data) { return void cb('NO_DATA'); } + if (!data.publicKey || !data.signature) { + console.log(data); + return void cb("MISSING_PARAMETERS"); + } + + rpc.send('REMOVE_LOGIN_BLOCK', [ + data.publicKey, // publicKey + data.signature, // signature + ], function (e) { + cb(e); + }); + }; + cb(e, exp); }); }; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 79f9c45a7..c40bff5e3 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -647,6 +647,10 @@ define([ Cryptpad.writeLoginBlock(data, cb); }); + sframeChan.on('Q_REMOVE_LOGIN_BLOCK', function (data, cb) { + Cryptpad.removeLoginBlock(data, cb); + }); + if (cfg.addRpc) { cfg.addRpc(sframeChan, Cryptpad, Utils); } diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index a75936af5..8bb68aeef 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -77,6 +77,9 @@ define({ // Write/update the login block when the account password is changed 'Q_WRITE_LOGIN_BLOCK': true, + // Remove login blocks + 'Q_REMOVE_LOGIN_BLOCK': true, + // Check the pin limit to determine if we can store the pad in the drive or if we should. // display a warning 'Q_GET_PIN_LIMIT_STATUS': true, From 3ba0ad3cf1c42f7714d008e419f7a7fd2bf9b79c Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 19 Jun 2018 18:08:14 +0200 Subject: [PATCH 04/18] get, set, and clear blocks from localStorage --- www/common/common-constants.js | 1 + www/common/outer/local-store.js | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/www/common/common-constants.js b/www/common/common-constants.js index 494267ed3..ce96bb66b 100644 --- a/www/common/common-constants.js +++ b/www/common/common-constants.js @@ -3,6 +3,7 @@ define(function () { // localStorage userHashKey: 'User_hash', userNameKey: 'User_name', + blockHashKey: 'Block_hash', fileHashKey: 'FS_hash', // sessionStorage newPadPathKey: "newPadPath", diff --git a/www/common/outer/local-store.js b/www/common/outer/local-store.js index a40a3e6f5..28f94b62b 100644 --- a/www/common/outer/local-store.js +++ b/www/common/outer/local-store.js @@ -58,6 +58,14 @@ define([ localStorage[Constants.userHashKey] = sHash; }; + LocalStore.getBlockHash = function () { + return localStorage[Constants.blockHashKey]; + }; + + LocalStore.setBlockHash = function (hash) { + LocalStorage[Constants.blockHashKey] = hash; + }; + LocalStore.getAccountName = function () { return localStorage[Constants.userNameKey]; }; @@ -96,6 +104,7 @@ define([ [ Constants.userNameKey, Constants.userHashKey, + Constants.blockHashKey, 'loginToken', 'plan', ].forEach(function (k) { From d03339f20bb651c682047e29e3f8eaaba279b4f1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 20 Jun 2018 14:27:44 +0200 Subject: [PATCH 05/18] check for the presence of a blockHash in localStorage when logging in --- rpc.js | 3 +-- www/assert/main.js | 15 ++++++++++++ www/common/common-hash.js | 37 +++++++++++++++++++++++++++++ www/common/common-util.js | 8 +++---- www/common/cryptpad-common.js | 42 ++++++++++++++++++++++++++++++++- www/common/outer/login-block.js | 31 ++++++++++++++++++++---- www/settings/inner.js | 35 +++++++++++++++++++++++++-- 7 files changed, 157 insertions(+), 14 deletions(-) diff --git a/rpc.js b/rpc.js index d10fe6149..5f6288805 100644 --- a/rpc.js +++ b/rpc.js @@ -1380,8 +1380,7 @@ var createLoginBlockPath = function (Env, publicKey) { }; var writeLoginBlock = function (Env, msg, cb) { - console.log(msg); // XXX - + //console.log(msg); var publicKey = msg[0]; var signature = msg[1]; var block = msg[2]; diff --git a/www/assert/main.js b/www/assert/main.js index 4ff862511..a6a82e421 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -10,9 +10,12 @@ define([ '/common/wire.js', '/common/flat-dom.js', '/common/media-tag.js', + + '/bower_components/tweetnacl/nacl-fast.min.js', ], function ($, Hyperjson, Sortify, Drive, Test, Hash, Util, Thumb, Wire, Flat, MediaTag) { window.Hyperjson = Hyperjson; window.Sortify = Sortify; + var Nacl = window.nacl; var assertions = 0; var failed = false; @@ -296,6 +299,18 @@ define([ !secret.hashData.present); }, "test support for ugly tracking query paramaters in url"); + assert(function (cb) { + var href = 'https://cryptpad.fr/block/pe/pewpewpewpewpew'; + var key = Nacl.randomBytes(32); + + var hash = Hash.createBlockHash(href, key); + + var parsed = Hash.parseBlockHash(hash); + + cb(parsed && href === parsed.href && + parsed.keys.symmetric.length === key.length); + }, 'parse a block hash'); + assert(function (cb) { try { MediaTag(void 0).on('progress').on('decryption'); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 4b0c2c607..e283d2525 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -474,6 +474,43 @@ Version 1 '/' + curvePublic.replace(/\//g, '-') + '/'; }; + // XXX consider putting Block functions in /common/outer/login-block.js + Hash.createBlockHash = function (href, key) { + if (typeof(href) !== 'string') { return; } + if (!key instanceof Uint8Array) { return; } + + // TODO verify inputs + try { return href + '#' + Nacl.util.encodeBase64(key); } + catch (e) { return; } + }; + + var decodeSafeB64 = function (b64) { + try { + return Nacl.util.decodeBase64(b64.replace(/\-/g, '/')); + } catch (e) { + console.error(e); + return; + } + }; + + Hash.parseBlockHash = function (hash) { + if (typeof(hash) !== 'string') { return; } + var parts = hash.split('#'); + if (parts.length !== 2) { return; } + + try { + return { + href: parts[0], + keys: { + symmetric: decodeSafeB64(parts[1]), + } + }; + } catch (e) { + console.error(e); + return; + } + }; + // Create untitled documents when no name is given var getLocaleDate = function () { if (window.Intl && window.Intl.DateTimeFormat) { diff --git a/www/common/common-util.js b/www/common/common-util.js index 2a30133e9..cc56cd0d5 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -137,17 +137,15 @@ define([], function () { else if (bytes >= oneMegabyte) { return 'MB'; } }; + // given a path, asynchronously return an arraybuffer Util.fetch = function (src, cb) { var done = false; - var CB = function (err, res) { - if (done) { return; } - done = true; - cb(err, res); - }; + var CB = Util.once(cb); var xhr = new XMLHttpRequest(); xhr.open("GET", src, true); xhr.responseType = "arraybuffer"; + xhr.onerror = function (err) { CB(err); }; xhr.onload = function () { if (/^4/.test(''+this.status)) { return CB('XHR_ERROR'); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index d095d2b3c..dd7a46d25 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -8,11 +8,12 @@ define([ '/common/common-feedback.js', '/common/outer/local-store.js', '/common/outer/worker-channel.js', + '/common/outer/login-block.js', '/customize/application_config.js', '/bower_components/nthen/index.js', ], function (Config, Messages, Util, Hash, - Messaging, Constants, Feedback, LocalStore, Channel, + Messaging, Constants, Feedback, LocalStore, Channel, Block, AppConfig, Nthen) { /* This file exposes functionality which is specific to Cryptpad, but not to @@ -883,7 +884,46 @@ define([ if (AppConfig.beforeLogin) { AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor()); } + }).nThen(function (waitFor) { + var blockHash = LocalStore.getBlockHash(); + + if (blockHash) { + console.log(blockHash); + var parsed = Hash.parseBlockHash(blockHash); + + if (typeof(parsed) !== 'object') { + console.error("Failed to parse blockHash"); + console.log(parsed); + return; + } else { + console.log(parsed); + } + Util.fetch(parsed.href, waitFor(function (err, arraybuffer) { + if (err) { return void console.log(err); } + + // use the results to load your user hash and + // put your userhash into localStorage + try { + var block_info = Block.decrypt(arraybuffer, parsed.keys); + if (block_info[Constants.userHashKey]) { LocalStore.setUserHash(block_info[Constants.userHashKey]); } + } catch (e) { + console.error(e); + return void console.error("failed to decrypt or decode block content"); + } + })); + } else { + // XXX debugging + console.error("NO BLOCK HASH"); + } + }).nThen(function (waitFor) { + // XXX debugging + if (LocalStore.getUserHash()) { + console.log('User_hash detected'); + } else { + console.log("User_hash not detected"); + } + var cfg = { init: true, //query: onMessage, // TODO temporary, will be replaced by a webworker channel diff --git a/www/common/outer/login-block.js b/www/common/outer/login-block.js index 026fa289f..3e931488b 100644 --- a/www/common/outer/login-block.js +++ b/www/common/outer/login-block.js @@ -1,7 +1,8 @@ define([ '/common/common-util.js', + '/api/config', '/bower_components/tweetnacl/nacl-fast.min.js', -], function (Util) { +], function (Util, ApiConfig) { var Nacl = window.nacl; var Block = {}; @@ -30,9 +31,11 @@ define([ var symmetric = seed.subarray(Nacl.sign.seedLength, Nacl.sign.seedLength + Nacl.secretbox.keyLength); + console.log("symmetric key: ", Nacl.util.encodeBase64(symmetric)); + return { sign: Nacl.sign.keyPair.fromSeed(signSeed), // 32 bytes - symmetric: symmetric, + symmetric: symmetric, // 32 bytes ... }; }; @@ -51,8 +54,15 @@ define([ Block.decrypt = function (u8_content, keys) { // version is currently ignored since there is only one var nonce = u8_content.subarray(1, 1 + Nacl.secretbox.nonceLength); - var box = content.subarray(1 + Nacl.secretbox.nonceLength); - return Nacl.secretbox.open(box, nonce, keys.symmetric); + var box = u8_content.subarray(1 + Nacl.secretbox.nonceLength); + + var plaintext = Nacl.secretbox.open(box, nonce, keys.symmetric); + try { + return JSON.parse(Nacl.util.encodeUTF8(plaintext)); + } catch (e) { + console.error(e); + return; + } }; // (Uint8Array block) => signature @@ -86,5 +96,18 @@ define([ }; }; + // FIXME don't spread the functions below across this file and common-hash + // find a permanent home for these hacks + var urlSafeB64 = function (u8) { + return Nacl.util.encodeBase64(u8).replace(/\//g, '-'); + }; + + Block.getBlockHash = function (keys) { + var publicKey = urlSafeB64(keys.sign.publicKey); + var relative = 'block/' + publicKey.slice(0, 2) + '/' + publicKey; // XXX FIXME use configurable path from /api/config + var symmetric = urlSafeB64(keys.symmetric); + return ApiConfig.httpUnsafeOrigin + relative + '#' + symmetric; + }; + return Block; }); diff --git a/www/settings/inner.js b/www/settings/inner.js index acfa8bafd..3d59acb9c 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -11,6 +11,7 @@ define([ '/common/hyperscript.js', '/customize/application_config.js', '/api/config', + '/common/outer/login-block.js', // XXX HACK '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', @@ -28,7 +29,8 @@ define([ Messages, h, AppConfig, - ApiConfig + ApiConfig, + Block // XXX HACK ) { var saveAs = window.saveAs; @@ -389,7 +391,36 @@ define([ }); }; - updateBlock = updateBlock; // jshint.. + var removeBlock = function (data, cb) { + sframeChan.query('Q_REMOVE_LOGIN_BLOCK', data, function (err, obj) { + if (err || obj.error) { return void cb ({error: err || obj.error}); } + cb (obj); + }); + }; + + + // XXX + if (false) { // STUBBED, just for development purposes + console.error("TRYING TO WRITE A BLOCK"); + + var keys = Block.genkeys(Block.seed()); + var data = Block.serialize(JSON.stringify({ + a: 5, + b: 6, + User_hash: "XXX", /// TODO encode newly derived User_hash here + }), keys); + + updateBlock(data, function (err, thing) { + console.log(err, thing); + + console.log(Block.getBlockHash(keys)); + + return; + removeBlock(Block.remove(keys), function (err, obj) { + console.log(err, obj); + }); + }); + } return $div; }; From 71b085ba23ee781abd01e37f297527a81bbfa9b0 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 20 Jun 2018 16:39:01 +0200 Subject: [PATCH 06/18] Owned drive migration UI --- customize.dist/translations/messages.js | 5 ++ www/common/cryptpad-common.js | 66 +++++++++++++++++++++++++ www/common/sframe-common-outer.js | 4 ++ www/common/sframe-protocol.js | 3 ++ www/settings/inner.js | 44 +++++++++++++++++ 5 files changed, 122 insertions(+) diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 16e837a35..e69835c36 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -602,6 +602,11 @@ define(function () { out.settings_templateSkip = "Skip the template selection modal"; out.settings_templateSkipHint = "When you create a new empty pad, if you have stored templates for this type of pad, a modal appears to ask if you want to use a template. Here you can choose to never show this modal and so to never use a template."; + out.settings_ownDriveTitle = "Drive migration"; // XXX + out.settings_ownDriveHint = "Migrating your drive to the new version will give you access to new features..."; // XXX + out.settings_ownDriveButton = "Migrate"; // XXX + out.settings_ownDriveConfirm = "Are you sure?"; // XXX + out.upload_title = "File upload"; out.upload_modal_title = "File upload options"; out.upload_modal_filename = "File name (extension {0} added automatically)"; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index d095d2b3c..f89c27143 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -691,6 +691,72 @@ define([ }); }; + common.ownUserDrive = function (Crypt, edPublic, cb) { + var hash = LocalStore.getUserHash(); + //var href = '/drive/#' + hash; + var secret = Hash.getSecrets('drive', hash); + var newHash, newHref, newSecret; + Nthen(function (waitFor) { + // Check if our drive is already owned + common.anonRpcMsg('GET_METADATA', secret.channel, waitFor(function (err, obj) { + if (err || obj.error) { return; } + if (obj.owners && Array.isArray(obj.owners) && + obj.owners.indexOf(edPublic) !== -1) { + waitFor.abort(); + cb({ + error: 'ALREADY_OWNED' + }); + } + })); + }).nThen(function (waitFor) { + waitFor.abort(); // TODO remove this line + // Create a new user hash + // Get the current content, store it in the new user file + // and make sure the new user drive is owned + newHash = Hash.createRandomHash('drive'); + newHref = '/drive/#' + newHash; + newSecret = Hash.getSecrets('drive', newHash); + + var optsPut = { + owners: [edPublic] + }; + + Crypt.get(hash, waitFor(function (err, val) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + Crypt.put(newHash, val, waitFor(function (err) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + }), optsPut); + })); + }).nThen(function (waitFor) { + // Migration success + // TODO: Replace user hash in login block + }).nThen(function (waitFor) { + // New drive hash is in login block, unpin the old one and pin the new one + common.unpinPads([secret.channel], waitFor()); + common.pinPads([newSecret.channel], waitFor()); + }).nThen(function (waitFor) { + // Login block updated + // TODO: logout everywhere + // * It should wipe localStorage.User_hash, ... + // * login will get the new value from loginBlock and store it in localStorage + // * SharedWorker will reconnect with the new value in other locations + // TODO: then DISCONNECT here + common.logoutFromAll(waitFor(function () { + postMessage("DISCONNECT"); + })); + }).nThen(function () { + // We have the new drive, with the new login block + // TODO: maybe reload automatically? + cb({ state: true }); + }); + }; + // Loading events common.loading = {}; common.loading.onDriveEvent = Util.mkEvent(); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index c40bff5e3..f82293e8a 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -643,6 +643,10 @@ define([ Cryptpad.changePadPassword(Cryptget, href, data.password, edPublic, cb); }); + sframeChan.on('Q_OWN_USER_DRIVE', function (data, cb) { + Cryptpad.ownUserDrive(Cryptget, edPublic, cb); + }); + sframeChan.on('Q_WRITE_LOGIN_BLOCK', function (data, cb) { Cryptpad.writeLoginBlock(data, cb); }); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 8bb68aeef..15ef32e86 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -237,6 +237,9 @@ define({ // Change pad password 'Q_PAD_PASSWORD_CHANGE': true, + // Migrate drive to owned drive + 'Q_OWN_USER_DRIVE': true, + // Loading events to display in the loading screen 'EV_LOADING_INFO': true, // Critical error outside the iframe during loading screen diff --git a/www/settings/inner.js b/www/settings/inner.js index acfa8bafd..bf2fe3bc0 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -49,6 +49,7 @@ define([ 'cp-settings-thumbnails', 'cp-settings-userfeedback', 'cp-settings-change-password', + 'cp-settings-migrate', 'cp-settings-delete' ], 'creation': [ @@ -394,6 +395,49 @@ define([ return $div; }; + create['migrate'] = function () { + // TODO + // if (!loginBlock) { return; } + // if (alreadyMigrated) { return; } + if (!common.isLoggedIn()) { return; } + + var $div = $('
', { 'class': 'cp-settings-migrate cp-sidebarlayout-element'}); + + $('', {'class': 'label'}).text(Messages.settings_ownDriveTitle).appendTo($div); + + $('', {'class': 'cp-sidebarlayout-description'}) + .append(Messages.settings_ownDriveHint).appendTo($div); + + var $ok = $('', {'class': 'fa fa-check', title: Messages.saved}); + var $spinner = $('', {'class': 'fa fa-spinner fa-pulse'}); + + var $button = $('