define([ '/bower_components/tweetnacl/nacl-fast.min.js', ], function () { var Nacl = window.nacl; var PARANOIA = true; var plainChunkLength = 128 * 1024; var cypherChunkLength = 131088; var computeEncryptedSize = function (bytes, meta) { var metasize = Nacl.util.decodeUTF8(JSON.stringify(meta)).length; var chunks = Math.ceil(bytes / plainChunkLength); return metasize + 18 + (chunks * 16) + bytes; }; var encodePrefix = function (p) { return [ 65280, // 255 << 8 255, ].map(function (n, i) { return (p & n) >> ((1 - i) * 8); }); }; var decodePrefix = function (A) { return (A[0] << 8) | A[1]; }; 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 (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) { throw new Error('E_NONCE_TOO_LARGE'); } } }; var joinChunks = function (chunks) { return new Blob(chunks); }; var concatBuffer = function (a, b) { // TODO make this not so ugly return new Uint8Array(slice(a).concat(slice(b))); }; var fetchMetadata = function (src, cb) { var done = false; var CB = function (err, res) { if (done) { return; } done = true; cb(err, res); }; var xhr = new XMLHttpRequest(); xhr.open("GET", src, true); xhr.setRequestHeader('Range', 'bytes=0-1'); xhr.responseType = 'arraybuffer'; xhr.onload = function () { if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); } var res = new Uint8Array(xhr.response); var size = decodePrefix(res); var xhr2 = new XMLHttpRequest(); xhr2.open("GET", src, true); xhr2.setRequestHeader('Range', 'bytes=2-' + (size + 2)); xhr2.responseType = 'arraybuffer'; xhr2.onload = function () { if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); } var res2 = new Uint8Array(xhr2.response); var all = concatBuffer(res, res2); CB(void 0, all); }; xhr2.send(null); }; xhr.send(null); }; var decryptMetadata = function (u8, key) { var prefix = u8.subarray(0, 2); var metadataLength = decodePrefix(prefix); var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength)); var metaChunk = Nacl.secretbox.open(metaBox, createNonce(), key); try { return JSON.parse(Nacl.util.encodeUTF8(metaChunk)); } catch (e) { return null; } }; var fetchDecryptedMetadata = function (src, key, cb) { if (typeof(src) !== 'string') { return window.setTimeout(function () { cb('NO_SOURCE'); }); } fetchMetadata(src, function (e, buffer) { if (e) { return cb(e); } cb(void 0, decryptMetadata(buffer, key)); }); }; var decrypt = function (u8, key, done, progress) { var MAX = u8.length; var _progress = function (offset) { if (typeof(progress) !== 'function') { return; } progress(Math.min(1, offset / MAX)); }; var nonce = createNonce(); var i = 0; var prefix = u8.subarray(0, 2); var metadataLength = decodePrefix(prefix); var res = { metadata: undefined, }; var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength)); var metaChunk = Nacl.secretbox.open(metaBox, nonce, key); increment(nonce); try { res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk)); } catch (e) { return window.setTimeout(function () { done('E_METADATA_DECRYPTION'); }); } if (!res.metadata) { return void setTimeout(function () { done('NO_METADATA'); }); } var takeChunk = function (cb) { var start = i * cypherChunkLength + 2 + metadataLength; var end = start + cypherChunkLength; i++; var box = new Uint8Array(u8.subarray(start, end)); // decrypt the chunk var plaintext = Nacl.secretbox.open(box, nonce, key); increment(nonce); if (!plaintext) { return cb('DECRYPTION_ERROR'); } _progress(end); cb(void 0, plaintext); }; var chunks = []; var again = function () { takeChunk(function (e, plaintext) { if (e) { return setTimeout(function () { done(e); }); } if (plaintext) { if ((2 + metadataLength + i * cypherChunkLength) < u8.length) { // not done chunks.push(plaintext); return setTimeout(again); } chunks.push(plaintext); res.content = joinChunks(chunks); return done(void 0, res); } done('UNEXPECTED_ENDING'); }); }; again(); }; // metadata /* { filename: 'raccoon.jpg', type: 'image/jpeg' } */ var encrypt = function (u8, metadata, key) { var nonce = createNonce(); // encode metadata var plaintext = Nacl.util.decodeUTF8(JSON.stringify(metadata)); // if metadata is too large, drop the thumbnail. if (plaintext.length > 65535) { var temp = JSON.parse(JSON.stringify(metadata)); delete metadata.thumbnail; plaintext = Nacl.util.decodeUTF8(JSON.stringify(temp)); } var i = 0; var state = 0; var next = function (cb) { if (state === 2) { return void cb(); } var start; var end; var part; var box; if (state === 0) { // metadata... part = new Uint8Array(plaintext); box = Nacl.secretbox(part, nonce, key); increment(nonce); if (box.length > 65535) { return void cb('METADATA_TOO_LARGE'); } var prefixed = new Uint8Array(encodePrefix(box.length) .concat(slice(box))); state++; return void cb(void 0, prefixed); } // encrypt the rest of the file... start = i * plainChunkLength; end = start + plainChunkLength; part = u8.subarray(start, end); box = Nacl.secretbox(part, nonce, key); increment(nonce); i++; // regular data is done if (i * plainChunkLength >= u8.length) { state = 2; } return void cb(void 0, box); }; return next; }; return { decrypt: decrypt, encrypt: encrypt, joinChunks: joinChunks, computeEncryptedSize: computeEncryptedSize, decryptMetadata: decryptMetadata, fetchMetadata: fetchMetadata, fetchDecryptedMetadata: fetchDecryptedMetadata, }; });