From a97e7223f1ac52b0589edc2ca127bd6fc1341175 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 Apr 2017 12:47:21 +0200 Subject: [PATCH 1/4] implement getBlobPathFromHex --- www/common/common-hash.js | 4 ++++ www/common/cryptpad-common.js | 1 + 2 files changed, 5 insertions(+) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 620acd319..b282a1b94 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -266,5 +266,9 @@ Version 2 return hex; }; + var getBlobPath = Hash.getBlobPathFromHex = function (id) { + return '/blob/' + id.slice(0,2) + '/' + id; + }; + return Hash; }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 5d8ab7804..f4834b285 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -73,6 +73,7 @@ define([ var hrefToHexChannelId = common.hrefToHexChannelId = Hash.hrefToHexChannelId; var parseHash = common.parseHash = Hash.parseHash; var getRelativeHref = common.getRelativeHref = Hash.getRelativeHref; + common.getBlobPathFromHex = Hash.getBlobPathFromHex; common.getEditHashFromKeys = Hash.getEditHashFromKeys; common.getViewHashFromKeys = Hash.getViewHashFromKeys; From e2942f959b549d72faeffbe6cf833e9cf4a93779 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 Apr 2017 12:56:42 +0200 Subject: [PATCH 2/4] add crypto for decrypting a chunked file --- www/file/file-crypto.js | 88 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 www/file/file-crypto.js diff --git a/www/file/file-crypto.js b/www/file/file-crypto.js new file mode 100644 index 000000000..8c520dd59 --- /dev/null +++ b/www/file/file-crypto.js @@ -0,0 +1,88 @@ +define([ + '/bower_components/tweetnacl/nacl-fast.min.js', +], function () { + var Nacl = window.nacl; + + var chunkLength = 131088; + + var slice = function (A) { + return Array.prototype.slice.call(A); + }; + + var increment = function (N) { + var l = N.length; + while (l-- > 1) { + if (N[l] !== 255) { return void N[l]++; } + N[l] = 0; + if (l === 0) { return true; } + } + }; + + var joinChunks = function (B) { + return new Uint8Array(chunks.reduce(function (A, B) { + return slice(A).concat(slice(B)); + }, [])); + }; + + var decrypt = function (u8, key, cb) { + var nonce = new Uint8Array(new Array(24).fill(0)); + var i = 0; + var takeChunk = function () { + let start = i * chunkLength; + let end = start + chunkLength; + i++; + let box = new Uint8Array(u8.subarray(start, end)); + + // decrypt the chunk + let plaintext = Nacl.secretbox.open(box, nonce, key); + increment(nonce); + return plaintext; + }; + + var buffer = ''; + + var res = { + metadata: undefined, + }; + + // decrypt metadata + for (; !res.metadata && i * chunkLength < u8.length;) { + var chunk = takeChunk(); + buffer += Nacl.util.encodeUTF8(chunk); + try { + res.metadata = JSON.parse(buffer); + //console.log(res.metadata); + } catch (e) { + console.log('buffering another chunk for metadata'); + } + } + + if (!res.metadata) { + return void setTimeout(function () { + cb('NO_METADATA'); + }); + } + + var chunks = []; + // decrypt file contents + for (;i * chunkLength < u8.length;) { + let chunk = takeChunk(); + if (!chunk) { + return void window.setTimeout(function () { + cb('DECRYPTION_ERROR'); + }); + //throw new Error('failed to parse'); + } + chunks.push(chunk); + } + + // send chunks + res.content = joinChunks(chunks); + + cb(void 0, res); + }; + + return { + decrypt: decrypt, + }; +}); From e132ccf94ac7d381c0a30289855cba67e62a9dfd Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 28 Apr 2017 11:45:53 +0200 Subject: [PATCH 3/4] prepare for upload --- .jshintignore | 1 + www/file/file-crypto.js | 126 ++++++++++++++++++++++++++++++++----- www/file/inner.html | 36 ++++++++++- www/file/main.js | 135 ++++++++++++++++++++++++++++------------ 4 files changed, 239 insertions(+), 59 deletions(-) diff --git a/.jshintignore b/.jshintignore index 51636ed77..93e467aef 100644 --- a/.jshintignore +++ b/.jshintignore @@ -10,3 +10,4 @@ NetFluxWebsocketSrv.js NetFluxWebsocketServer.js WebRTCSrv.js www/common/media-tag.js +www/scratch diff --git a/www/file/file-crypto.js b/www/file/file-crypto.js index 8c520dd59..924cbe334 100644 --- a/www/file/file-crypto.js +++ b/www/file/file-crypto.js @@ -2,39 +2,75 @@ define([ '/bower_components/tweetnacl/nacl-fast.min.js', ], function () { var Nacl = window.nacl; + var PARANOIA = true; - var chunkLength = 131088; + var plainChunkLength = 128 * 1024; + var cypherChunkLength = 131088; var slice = function (A) { return Array.prototype.slice.call(A); }; + var createNonce = function () { + return new Uint8Array(new Array(24).fill(0)); + }; + var increment = function (N) { var l = N.length; while (l-- > 1) { - if (N[l] !== 255) { return void N[l]++; } + if (PARANOIA) { + if (typeof(N[l]) !== 'number') { + throw new Error('E_UNSAFE_TYPE'); + } + if (N[l] > 255) { + throw new Error('E_OUT_OF_BOUNDS'); + } + } + /* jshint probably suspects this is unsafe because we lack types + but as long as this is only used on nonces, it should be safe */ + if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line N[l] = 0; + + // you don't need to worry about this running out. + // you'd need a REAAAALLY big file if (l === 0) { return true; } } }; - var joinChunks = function (B) { + var joinChunks = function (chunks) { return new Uint8Array(chunks.reduce(function (A, B) { return slice(A).concat(slice(B)); }, [])); }; + var padChunk = function (A) { + var padding; + if (A.length === plainChunkLength) { return A; } + if (A.length < plainChunkLength) { + padding = new Array(plainChunkLength - A.length).fill(32); + return A.concat(padding); + } + if (A.length > plainChunkLength) { + // how many times larger is it? + var chunks = Math.ceil(A.length / plainChunkLength); + padding = new Array((plainChunkLength * chunks) - A.length).fill(32); + return A.concat(padding); + } + }; + var decrypt = function (u8, key, cb) { - var nonce = new Uint8Array(new Array(24).fill(0)); + var nonce = createNonce(); var i = 0; + var takeChunk = function () { - let start = i * chunkLength; - let end = start + chunkLength; + var start = i * cypherChunkLength; + var end = start + cypherChunkLength; i++; - let box = new Uint8Array(u8.subarray(start, end)); + var box = new Uint8Array(u8.subarray(start, end)); // decrypt the chunk - let plaintext = Nacl.secretbox.open(box, nonce, key); + var plaintext = Nacl.secretbox.open(box, nonce, key); + // TODO handle nonce-too-large-error increment(nonce); return plaintext; }; @@ -46,8 +82,9 @@ define([ }; // decrypt metadata - for (; !res.metadata && i * chunkLength < u8.length;) { - var chunk = takeChunk(); + var chunk; + for (; !res.metadata && i * cypherChunkLength < u8.length;) { + chunk = takeChunk(); buffer += Nacl.util.encodeUTF8(chunk); try { res.metadata = JSON.parse(buffer); @@ -63,15 +100,16 @@ define([ }); } + var fail = function () { + cb("DECRYPTION_ERROR"); + }; + var chunks = []; // decrypt file contents - for (;i * chunkLength < u8.length;) { - let chunk = takeChunk(); + for (;i * cypherChunkLength < u8.length;) { + chunk = takeChunk(); if (!chunk) { - return void window.setTimeout(function () { - cb('DECRYPTION_ERROR'); - }); - //throw new Error('failed to parse'); + return window.setTimeout(fail); } chunks.push(chunk); } @@ -82,7 +120,63 @@ define([ cb(void 0, res); }; + // 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 nonce = createNonce(); + + // encode metadata + var metaBuffer = Array.prototype.slice + .call(Nacl.util.decodeUTF8(JSON.stringify(metadata))); + + var plaintext = new Uint8Array(padChunk(metaBuffer)); + + var chunks = []; + var j = 0; + + var start; + var end; + + var part; + var box; + + // prepend some metadata + for (;j * plainChunkLength < plaintext.length; j++) { + start = j * plainChunkLength; + end = start + plainChunkLength; + + part = plaintext.subarray(start, end); + box = Nacl.secretbox(part, nonce, key); + chunks.push(box); + increment(nonce); + } + + // append the encrypted file chunks + var i = 0; + for (;i * plainChunkLength < u8.length; i++) { + start = i * plainChunkLength; + end = start + plainChunkLength; + + part = new Uint8Array(u8.subarray(start, end)); + box = Nacl.secretbox(part, nonce, key); + chunks.push(box); + increment(nonce); + } + + + // TODO do something with the chunks... + }; + return { decrypt: decrypt, + encrypt: encrypt, }; }); diff --git a/www/file/inner.html b/www/file/inner.html index 7f315cef2..09f627842 100644 --- a/www/file/inner.html +++ b/www/file/inner.html @@ -14,14 +14,44 @@ padding: 0px; display: inline-block; } - media-tag * { - max-width: 100%; + #file { + display: block; + height: 300px; + width: 300px; + border: 2px solid black; + margin: 50px; } + + .inputfile { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; + } + .inputfile + label { + border: 2px solid black; + display: block; + height: 500px; + width: 500px; + background-color: rgba(50, 50, 50, .10); + margin: 50px; + } + + .inputfile:focus + label, + .inputfile + label:hover { + background-color: rgba(50, 50, 50, 0.30); + } +
- + diff --git a/www/file/main.js b/www/file/main.js index bf7cb9e00..e237d7a63 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -6,12 +6,14 @@ define([ '/common/cryptpad-common.js', '/common/visible.js', '/common/notify.js', + '/file/file-crypto.js', '/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/file-saver/FileSaver.min.js', -], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify) { +], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify, FileCrypto) { var Messages = Cryptpad.Messages; var saveAs = window.saveAs; - //window.Nacl = window.nacl; + var Nacl = window.nacl; + $(function () { var ifrw = $('#pad-iframe')[0].contentWindow; @@ -19,18 +21,40 @@ define([ Cryptpad.addLoadingScreen(); + var fetch = function (src, cb) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", src, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function (e) { + return void cb(void 0, new Uint8Array(xhr.response)); + }; + xhr.send(null); + }; + + var upload = function (blob, id, key) { + Cryptpad.alert("UPLOAD IS NOT IMPLEMENTED YET"); + }; + + var myFile; + var myDataType; + var uploadMode = false; + var andThen = function () { var $bar = $iframe.find('.toolbar-container'); - var secret = Cryptpad.getSecrets(); - if (!secret.keys) { throw new Error("You need a hash"); } // TODO - - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var fileId = secret.channel; - var hexFileName = Cryptpad.base64ToHex(fileId); - var type = "image/png"; // Test hash: // #/2/K6xWU-LT9BJHCQcDCT-DcQ/TBo77200c0e-FdldQFcnQx4Y/ + var secret; + var hexFileName; + if (window.location.hash) { + secret = Cryptpad.getSecrets(); + if (!secret.keys) { throw new Error("You need a hash"); } // TODO + hexFileName = Cryptpad.base64ToHex(secret.channel); + } else { + uploadMode = true; + } + + //window.location.hash = '/2/K6xWU-LT9BJHCQcDCT-DcQ/VLIgpQOgmSaW3AQcUCCoJnYvCbMSO0MKBqaICSly9fo='; var parsed = Cryptpad.parsePadUrl(window.location.href); var defaultName = Cryptpad.getDefaultName(parsed); @@ -67,45 +91,76 @@ define([ var exportFile = function () { var suggestion = document.title; Cryptpad.prompt(Messages.exportPrompt, - Cryptpad.fixFileName(suggestion) + '.html', function (filename) { + Cryptpad.fixFileName(suggestion), function (filename) { if (!(typeof(filename) === 'string' && filename)) { return; } - //var blob = new Blob([html], {type: "text/html;charset=utf-8"}); + var blob = new Blob([myFile], {type: myDataType}); saveAs(blob, filename); }); }; - var $mt = $iframe.find('#encryptedFile'); - $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); - $mt.attr('data-crypto-key', cryptKey); - $mt.attr('data-type', type); - - require(['/common/media-tag.js'], function (MediaTag) { - var configTb = { - displayed: ['useradmin', 'share', 'newpad'], - ifrw: ifrw, - common: Cryptpad, - title: { - onRename: renameCb, - defaultName: defaultName, - suggestName: suggestName - }, - share: { - secret: secret, - channel: hexFileName - } - }; - Toolbar.create($bar, null, null, null, null, configTb); - var $rightside = $bar.find('.' + Toolbar.constants.rightside); - - var $export = Cryptpad.createButton('export', true, {}, exportFile); - $rightside.append($export); - - updateTitle(Cryptpad.initialName || getTitle() || defaultName); + var displayed = ['useradmin', 'newpad', 'limit']; + if (secret && hexFileName) { + displayed.push('share'); + } + + var configTb = { + displayed: displayed, + ifrw: ifrw, + common: Cryptpad, + title: { + onRename: renameCb, + defaultName: defaultName, + suggestName: suggestName + }, + share: { + secret: secret, + channel: hexFileName + } + }; + Toolbar.create($bar, null, null, null, null, configTb); + var $rightside = $bar.find('.' + Toolbar.constants.rightside); + + var $export = Cryptpad.createButton('export', true, {}, exportFile); + $rightside.append($export); + + updateTitle(Cryptpad.initialName || getTitle() || defaultName); + + if (!uploadMode) { + var src = Cryptpad.getBlobPathFromHex(hexFileName); + return fetch(src, function (e, u8) { + // now decrypt the u8 + if (e) { return window.alert('error'); } + var cryptKey = secret.keys && secret.keys.fileKeyStr; + var key = Nacl.util.decodeBase64(cryptKey); + + FileCrypto.decrypt(u8, key, function (e, data) { + console.log(data); + var title = document.title = data.metadata.filename; + myFile = data.content; + myDataType = data.metadata.type; + updateTitle(title || defaultName); + + Cryptpad.removeLoadingScreen(); + }); + }); + } - var mt = MediaTag($mt[0]); + var $form = $iframe.find('#upload-form'); + $form.css({ + display: 'block', + }); - Cryptpad.removeLoadingScreen(); + 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.readAsText(file); }); + + // we're in upload mode + Cryptpad.removeLoadingScreen(); }; Cryptpad.ready(function (err, anv) { From fe93da8817901ecb495dda23b1702f6af2099dc0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 28 Apr 2017 11:46:13 +0200 Subject: [PATCH 4/4] get ready to implement blob storage --- rpc.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/rpc.js b/rpc.js index 808815d84..5670f1c7a 100644 --- a/rpc.js +++ b/rpc.js @@ -376,6 +376,23 @@ var resetUserPins = function (store, Sessions, publicKey, channelList, cb) { }); }; +var getLimit = function (cb) { + +}; + +var createBlobStaging = function (cb) { + +}; + +var createBlobStore = function (cb) { +}; + +var upload = function (store, Sessions, publicKey, cb) { + +}; + + + /*::const ConfigType = require('./config.example.js');*/ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) { // load pin-store... @@ -428,7 +445,6 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY'); } - if (checkSignature(serialized, signature, publicKey) !== true) { return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY"); } @@ -459,7 +475,8 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return resetUserPins(store, Sessions, safeKey, msg[1], function (e, hash) { return void Respond(e, hash); }); - case 'PIN': + case 'PIN': // TODO don't pin if over the limit + // if over, send error E_OVER_LIMIT return pinChannel(store, Sessions, safeKey, msg[1], function (e, hash) { Respond(e, hash); }); @@ -471,13 +488,17 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return void getHash(store, Sessions, safeKey, function (e, hash) { Respond(e, hash); }); - case 'GET_TOTAL_SIZE': + case 'GET_TOTAL_SIZE': // TODO cache this, since it will get called quite a bit return getTotalSize(store, ctx.store, Sessions, safeKey, function (e, size) { if (e) { return void Respond(e); } Respond(e, size); }); case 'GET_FILE_SIZE': return void getFileSize(ctx.store, msg[1], Respond); + case 'GET_LIMIT': // TODO implement this and cache it per-user + return void getLimit(function (e, limit) { + Respond('NOT_IMPLEMENTED'); + }); case 'GET_MULTIPLE_FILE_SIZE': return void getMultipleFileSize(ctx.store, msg[1], function (e, dict) { if (e) { return void Respond(e); }