From 410a9dfb17ca9ddabe1fcff0dabd0f1a7c5bd01e Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 4 May 2017 11:20:52 +0200 Subject: [PATCH 1/6] temporary solution for testing pin limits --- customize.dist/application_config.js | 3 +++ www/common/cryptpad-common.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index a91267132..05d4356bb 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -37,5 +37,8 @@ define(function() { config.enableHistory = true; + //config.enablePinLimit = true; + //config.pinLimit = 1000; + return config; }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 4c1e5c970..e55cfa6ba 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -721,7 +721,7 @@ define([ }; var getPinLimit = common.getPinLimit = function (cb) { - cb(void 0, 1000); + cb(void 0, typeof(AppConfig.pinLimit) === 'number'? AppConfig.pinLimit: 1000); }; var isOverPinLimit = common.isOverPinLimit = function (cb) { From 7573b86946aedd5f4d86bc6b5280f1e61850ffb9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 4 May 2017 11:36:24 +0200 Subject: [PATCH 2/6] call back with error if an RPC is made while disconnected --- www/common/rpc.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/www/common/rpc.js b/www/common/rpc.js index b69a5ce5a..2e1114729 100644 --- a/www/common/rpc.js +++ b/www/common/rpc.js @@ -102,9 +102,16 @@ types of messages: timeouts: {}, // timeouts pending: {}, // callbacks cookie: null, + connected: true, }; var send = function (type, msg, cb) { + if (!ctx.connected && type !== 'COOKIE') { + return void window.setTimeout(function () { + cb('DISCONNECTED'); + }); + } + // construct a signed message... var data = [type, msg]; @@ -127,6 +134,17 @@ types of messages: onMsg(ctx, msg); }); + network.on('disconnect', function (reason) { + ctx.connected = false; + }); + + network.on('reconnect', function (uid) { + send('COOKIE', "", function (e, msg) { + if (e) { return void cb(e); } + ctx.connected = true; + }); + }); + send('COOKIE', "", function (e, msg) { if (e) { return void cb(e); } // callback to provide 'send' method to whatever needs it From ca7b76a812b71271f36facbd0f7b741e0df04bee Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 4 May 2017 11:36:56 +0200 Subject: [PATCH 3/6] prototype of encypted file uploads --- rpc.js | 192 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 165 insertions(+), 27 deletions(-) diff --git a/rpc.js b/rpc.js index 4c4afb10d..98e896d5d 100644 --- a/rpc.js +++ b/rpc.js @@ -3,6 +3,7 @@ var Nacl = require("tweetnacl"); var Fs = require("fs"); +var Path = require("path"); var RPC = module.exports; @@ -12,6 +13,31 @@ var isValidChannel = function (chan) { return /^[a-fA-F0-9]/.test(chan); }; +var uint8ArrayToHex = function (a) { + // call slice so Uint8Arrays work as expected + return Array.prototype.slice.call(a).map(function (e, i) { + var n = Number(e & 0xff).toString(16); + if (n === 'NaN') { + throw new Error('invalid input resulted in NaN'); + } + + switch (n.length) { + case 0: return '00'; // just being careful, shouldn't happen + case 1: return '0' + n; + case 2: return n; + default: throw new Error('unexpected value'); + } + }).join(''); +}; + +var createChannelId = function () { + var id = uint8ArrayToHex(Nacl.randomBytes(16)); + if (id.length !== 32 || /[^a-f0-9]/.test(id)) { + throw new Error('channel ids must consist of 32 hex characters'); + } + return id; +}; + var makeToken = function () { return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)) .toString(16); @@ -351,8 +377,7 @@ var unpinChannel = function (store, Sessions, publicKey, channels, cb) { function (e) { if (e) { return void cb(e); } toStore.forEach(function (channel) { - // TODO actually delete - session.channels[channel] = false; + delete session.channels[channel]; // = false; }); getHash(store, Sessions, publicKey, cb); @@ -389,31 +414,144 @@ var safeMkdir = function (path, cb) { }); }; -var upload = function (store, Sessions, publicKey, cb) { -/* - 1. check if there is an upload in progress - * if yes, return error - 2. +var makeFilePath = function (root, id) { + if (typeof(id) !== 'string' || id.length <= 2) { return null; } + return Path.join(root, id.slice(0, 2), id); +}; + +var makeFileStream = function (root, id, cb) { + var stub = id.slice(0, 2); + var full = makeFilePath(root, id); + safeMkdir(Path.join(root, stub), function (e) { + if (e) { return void cb(e); } + + try { + var stream = Fs.createWriteStream(full, { + flags: 'a', + encoding: 'binary', + }); + stream.on('open', function () { + cb(void 0, stream); + }); + } catch (err) { + cb('BAD_STREAM'); + } + }); +}; + +var upload = function (stagingPath, Sessions, publicKey, content, cb) { + var dec = new Buffer(Nacl.util.decodeBase64(content)); -*/ + var session = Sessions[publicKey]; + if (!session.blobstage) { + makeFileStream(stagingPath, publicKey, function (e, stream) { + if (e) { return void cb(e); } - console.log('UPLOAD_NOT_IMPLEMENTED'); - cb('NOT_IMPLEMENTED'); + var blobstage = session.blobstage = stream; + blobstage.write(dec); + cb(void 0, dec.length); + }); + } else { + session.blobstage.write(dec); + cb(void 0, dec.length); + } +}; + +var upload_cancel = function (stagingPath, Sessions, publicKey, cb) { + var path = makeFilePath(stagingPath, publicKey); + if (!path) { + console.log(stagingPath, publicKey); + console.log(path); + return void cb('NO_FILE'); + } + + Fs.unlink(path, function (e) { + if (e) { return void cb('E_UNLINK'); } + cb(void 0); + }); }; -var cancelUpload = function (store, Sessions, publicKey, cb) { - console.log('CANCEL_UPLOAD_NOT_IMPLEMENTED'); - cb('NOT_IMPLEMENTED'); +var isFile = function (filePath, cb) { + Fs.stat(filePath, function (e, stats) { + if (e) { + if (e.code === 'ENOENT') { return void cb(void 0, false); } + return void cb(e.message); + } + return void cb(void 0, stats.isFile()); + }); +}; + +var upload_complete = function (stagingPath, storePath, Sessions, publicKey, cb) { + var session = Sessions[publicKey]; + + if (session.blobstage && session.blobstage.close) { + session.blobstage.close(); + delete session.blobstage; + } + + var oldPath = makeFilePath(stagingPath, publicKey); + + var tryRandomLocation = function (cb) { + var id = createChannelId(); + var prefix = id.slice(0, 2); + var newPath = makeFilePath(storePath, id); + //publicKey); + + safeMkdir(Path.join(storePath, prefix), function (e) { + if (e) { + console.error(e); + return void cb('RENAME_ERR'); + } + isFile(newPath, function (e, yes) { + if (e) { + console.error(e); + return void cb(e); + } + if (yes) { + return void tryRandomLocation(cb); + } + + cb(void 0, newPath, id); + }); + }); + }; + + tryRandomLocation(function (e, newPath, id) { + console.log(newPath, id); + Fs.rename(oldPath, newPath, function (e) { + if (e) { + console.error(e); + return cb(e); + } + + cb(void 0, id); + }); + }); +}; + +var upload_status = function (stagingPath, Sessions, publicKey, cb) { + var filePath = makeFilePath(stagingPath, publicKey); + if (!filePath) { return void cb('E_INVALID_PATH'); } + isFile(filePath, function (e, yes) { + cb(e, yes); + }); }; /*::const ConfigType = require('./config.example.js');*/ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) { // load pin-store... - console.log('loading rpc module...'); var Sessions = {}; + var keyOrDefaultString = function (key, def) { + return typeof(config[key]) === 'string'? config[key]: def; + }; + + var pinPath = keyOrDefaultString('pinPath', './pins'); + var blobPath = keyOrDefaultString('blobPath', './blob'); + var blobStagingPath = keyOrDefaultString('blobStagingPath', './blobstage'); + var store; var rpc = function ( @@ -475,7 +613,7 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) var Respond = function (e, msg) { var token = Sessions[publicKey].tokens.slice(-1)[0]; var cookie = makeCookie(token).join('|'); - respond(e, [cookie].concat(msg||[])); + respond(e, [cookie].concat(typeof(msg) !== 'undefined' ?msg: [])); }; if (typeof(msg) !== 'object' || !msg.length) { @@ -519,11 +657,19 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) }); case 'UPLOAD': - return void upload(null, null, null, function (e) { - Respond(e); + return void upload(blobStagingPath, Sessions, safeKey, msg[1], function (e, len) { + Respond(e, len); + }); + case 'UPLOAD_STATUS': + return void upload_status(blobStagingPath, Sessions, safeKey, function (e, stat) { + Respond(e, stat); + }); + case 'UPLOAD_COMPLETE': + return void upload_complete(blobStagingPath, blobPath, Sessions, safeKey, function (e, hash) { + Respond(e, hash); }); - case 'CANCEL_UPLOAD': - return void cancelUpload(null, null, null, function (e) { + case 'UPLOAD_CANCEL': + return void upload_cancel(blobStagingPath, Sessions, safeKey, function (e) { Respond(e); }); default: @@ -531,14 +677,6 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) } }; - var keyOrDefaultString = function (key, def) { - return typeof(config[key]) === 'string'? config[key]: def; - }; - - var pinPath = keyOrDefaultString('pinPath', './pins'); - var blobPath = keyOrDefaultString('blobPath', './blob'); - var blobStagingPath = keyOrDefaultString('blobStagingPath', './blobstage'); - Store.create({ filePath: pinPath, }, function (s) { From f644dc6c0b03991f68fb76eb296f023c4527aea0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 4 May 2017 11:37:46 +0200 Subject: [PATCH 4/6] WIP support encrypted file upload via base64 chunks --- www/file/file-crypto.js | 70 ++++++++++++++----------- www/file/main.js | 110 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 139 insertions(+), 41 deletions(-) diff --git a/www/file/file-crypto.js b/www/file/file-crypto.js index 924cbe334..4af79aa04 100644 --- a/www/file/file-crypto.js +++ b/www/file/file-crypto.js @@ -122,15 +122,7 @@ define([ // metadata /* { filename: 'raccoon.jpg', type: 'image/jpeg' } */ - - - /* TODO - in your callback, return an object which you can iterate... - - - */ - - var encrypt = function (u8, metadata, key, cb) { + var encrypt = function (u8, metadata, key) { var nonce = createNonce(); // encode metadata @@ -139,44 +131,62 @@ define([ var plaintext = new Uint8Array(padChunk(metaBuffer)); - var chunks = []; var j = 0; + var i = 0; - var start; - var end; + /* + 0: metadata + 1: u8 + 2: done + */ - var part; - var box; + var state = 0; - // prepend some metadata - for (;j * plainChunkLength < plaintext.length; j++) { - start = j * plainChunkLength; - end = start + plainChunkLength; + var next = function (cb) { + var start; + var end; + var part; + var box; - part = plaintext.subarray(start, end); - box = Nacl.secretbox(part, nonce, key); - chunks.push(box); - increment(nonce); - } + if (state === 0) { // metadata... + start = j * plainChunkLength; + end = start + plainChunkLength; - // append the encrypted file chunks - var i = 0; - for (;i * plainChunkLength < u8.length; i++) { + part = plaintext.subarray(start, end); + box = Nacl.secretbox(part, nonce, key); + increment(nonce); + + j++; + + // metadata is done + if (j * plainChunkLength >= plaintext.length) { + return void cb(state++, box); + } + + return void cb(state, box); + } + + // encrypt the rest of the file... start = i * plainChunkLength; end = start + plainChunkLength; - part = new Uint8Array(u8.subarray(start, end)); + part = u8.subarray(start, end); box = Nacl.secretbox(part, nonce, key); - chunks.push(box); increment(nonce); - } + i++; + // regular data is done + if (i * plainChunkLength >= u8.length) { state = 2; } + + return void cb(state, box); + }; - // TODO do something with the chunks... + return next; }; return { decrypt: decrypt, encrypt: encrypt, + joinChunks: joinChunks, }; }); diff --git a/www/file/main.js b/www/file/main.js index 24ef72f2b..004a665e0 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -14,6 +14,8 @@ define([ var saveAs = window.saveAs; var Nacl = window.nacl; + var APP = {}; + $(function () { var ifrw = $('#pad-iframe')[0].contentWindow; @@ -31,12 +33,93 @@ define([ xhr.send(null); }; - var upload = function (blob, id, key) { - Cryptpad.alert("UPLOAD IS NOT IMPLEMENTED YET"); - }; - var myFile; var myDataType; + + var upload = function (blob, metadata) { + console.log(metadata); + var u8 = new Uint8Array(blob); + + var key = Nacl.randomBytes(32); + var next = FileCrypto.encrypt(u8, metadata, key); + + var chunks = []; + + var sendChunk = function (box, cb) { + var enc = Nacl.util.encodeBase64(box); + + chunks.push(box); + Cryptpad.rpc.send('UPLOAD', enc, function (e, msg) { + cb(e, msg); + }); + }; + + var again = function (state, box) { + switch (state) { + case 0: + sendChunk(box, function (e, msg) { + if (e) { return console.error(e); } + next(again); + }); + break; + case 1: + sendChunk(box, function (e, msg) { + if (e) { return console.error(e); } + next(again); + }); + break; + case 2: + sendChunk(box, function (e, msg) { + if (e) { return console.error(e); } + Cryptpad.rpc.send('UPLOAD_COMPLETE', '', function (e, res) { + if (e) { return void console.error(e); } + var id = res[0]; + var uri = ['', 'blob', id.slice(0,2), id].join('/'); + console.log("encrypted blob is now available as %s", uri); + + window.location.hash = [ + '', + 2, + Cryptpad.hexToBase64(id).replace(/\//g, '-'), + Nacl.util.encodeBase64(key).replace(/\//g, '-'), + '' + ].join('/'); + + APP.$form.hide(); + + var newU8 = FileCrypto.joinChunks(chunks); + FileCrypto.decrypt(newU8, key, function (e, res) { + var title = document.title = res.metadata.filename; + myFile = res.content; + myDataType = res.metadata.type; + }); + }); + }); + break; + default: + throw new Error("E_INVAL_STATE"); + } + }; + + Cryptpad.rpc.send('UPLOAD_STATUS', '', function (e, pending) { + if (e) { + console.error(e); + return void Cryptpad.alert("something went wrong"); + } + + if (pending[0]) { + return void Cryptpad.confirm('upload pending, abort?', function (yes) { + if (!yes) { return; } + Cryptpad.rpc.send('UPLOAD_CANCEL', '', function (e, res) { + if (e) { return void console.error(e); } + console.log(res); + }); + }); + } + next(again); + }); + }; + var uploadMode = false; var andThen = function () { @@ -54,8 +137,6 @@ define([ uploadMode = true; } - //window.location.hash = '/2/K6xWU-LT9BJHCQcDCT-DcQ/VLIgpQOgmSaW3AQcUCCoJnYvCbMSO0MKBqaICSly9fo='; - var parsed = Cryptpad.parsePadUrl(window.location.href); var defaultName = Cryptpad.getDefaultName(parsed); @@ -136,7 +217,7 @@ define([ FileCrypto.decrypt(u8, key, function (e, data) { console.log(data); - var title = document.title = data.metadata.filename; + var title = document.title = data.metadata.name; myFile = data.content; myDataType = data.metadata.type; updateTitle(title || defaultName); @@ -146,7 +227,11 @@ define([ }); } - var $form = $iframe.find('#upload-form'); + if (!Cryptpad.isLoggedIn()) { + return Cryptpad.alert("You must be logged in to upload files"); + } + + var $form = APP.$form = $iframe.find('#upload-form'); $form.css({ display: 'block', }); @@ -154,10 +239,13 @@ define([ var $file = $form.find("#file").on('change', function (e) { var file = e.target.files[0]; var reader = new FileReader(); - reader.onload = function (e) { - upload(e.target.result); + reader.onloadend = function (e) { + upload(this.result, { + name: file.name, + type: file.type, + }); }; - reader.readAsText(file); + reader.readAsArrayBuffer(file); }); // we're in upload mode From 5f700c7451aa569c7d0fb0b794bd2cde6a9b691a Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 4 May 2017 11:46:11 +0200 Subject: [PATCH 5/6] cleanup and jshint compliance --- rpc.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rpc.js b/rpc.js index 98e896d5d..3f705fab0 100644 --- a/rpc.js +++ b/rpc.js @@ -2,6 +2,9 @@ /* Use Nacl for checking signatures of messages */ var Nacl = require("tweetnacl"); +/* globals Buffer*/ +/* globals process */ + var Fs = require("fs"); var Path = require("path"); @@ -49,7 +52,7 @@ var makeCookie = function (token) { return [ time, - process.pid, // jshint ignore:line + process.pid, token ]; }; @@ -114,7 +117,7 @@ var isValidCookie = function (Sessions, publicKey, cookie) { } // different process. try harder - if (process.pid !== parsed.pid) { // jshint ignore:line + if (process.pid !== parsed.pid) { return false; } @@ -440,7 +443,7 @@ var makeFileStream = function (root, id, cb) { }; var upload = function (stagingPath, Sessions, publicKey, content, cb) { - var dec = new Buffer(Nacl.util.decodeBase64(content)); + var dec = new Buffer(Nacl.util.decodeBase64(content)); // jshint ignore:line var session = Sessions[publicKey]; if (!session.blobstage) { @@ -495,7 +498,6 @@ var upload_complete = function (stagingPath, storePath, Sessions, publicKey, cb) var id = createChannelId(); var prefix = id.slice(0, 2); var newPath = makeFilePath(storePath, id); - //publicKey); safeMkdir(Path.join(storePath, prefix), function (e) { if (e) { @@ -517,7 +519,6 @@ var upload_complete = function (stagingPath, storePath, Sessions, publicKey, cb) }; tryRandomLocation(function (e, newPath, id) { - console.log(newPath, id); Fs.rename(oldPath, newPath, function (e) { if (e) { console.error(e); From 2232518c646b968f80917295b2ccfb993865004b Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 4 May 2017 12:01:37 +0200 Subject: [PATCH 6/6] set title after uploading --- www/file/main.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/www/file/main.js b/www/file/main.js index 004a665e0..c0970c24f 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -92,6 +92,9 @@ define([ var title = document.title = res.metadata.filename; myFile = res.content; myDataType = res.metadata.type; + + var defaultName = Cryptpad.getDefaultName(Cryptpad.parsePadUrl(window.location.href)); + APP.updateTitle(title || defaultName); }); }); }); @@ -147,7 +150,7 @@ define([ return data ? data.title : undefined; }; - var updateTitle = function (newTitle) { + var updateTitle = APP.updateTitle = function (newTitle) { Cryptpad.renamePad(newTitle, function (err, data) { if (err) { console.log("Couldn't set pad title");