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) {