diff --git a/frontend/src/fileSender.js b/frontend/src/fileSender.js index 449e5f18..deee1b09 100644 --- a/frontend/src/fileSender.js +++ b/frontend/src/fileSender.js @@ -1,8 +1,6 @@ const EventEmitter = require('events'); const { arrayToHex } = require('./utils'); -const Raven = window.Raven; - class FileSender extends EventEmitter { constructor(file) { super(); @@ -38,15 +36,14 @@ class FileSender extends EventEmitter { const self = this; self.emit('loading', true); return Promise.all([ - window.crypto.subtle - .generateKey( - { - name: 'AES-GCM', - length: 128 - }, - true, - ['encrypt', 'decrypt'] - ), + window.crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: 128 + }, + true, + ['encrypt', 'decrypt'] + ), new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsArrayBuffer(this.file); diff --git a/frontend/src/upload.js b/frontend/src/upload.js index f3ab980f..cbcb6db2 100644 --- a/frontend/src/upload.js +++ b/frontend/src/upload.js @@ -1,12 +1,7 @@ /* global MAXFILESIZE EXPIRE_SECONDS */ require('./common'); const FileSender = require('./fileSender'); -const { - notify, - findMetric, - sendEvent, - ONE_DAY_IN_MS -} = require('./utils'); +const { notify, findMetric, sendEvent, ONE_DAY_IN_MS } = require('./utils'); const bytes = require('bytes'); const Storage = require('./storage'); const storage = new Storage(localStorage); diff --git a/public/main.css b/public/main.css index ccd598b5..00e3ab17 100644 --- a/public/main.css +++ b/public/main.css @@ -1,12 +1,8 @@ /*** index.html ***/ html { background: url('resources/send_bg.svg'); - font-family: -apple-system, - BlinkMacSystemFont, - 'SF Pro Text', - Helvetica, - Arial, - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', Helvetica, + Arial, sans-serif; font-weight: 200; background-size: 110%; background-repeat: no-repeat; diff --git a/scripts/version.js b/scripts/version.js index de92da98..0a68ff7f 100755 --- a/scripts/version.js +++ b/scripts/version.js @@ -14,7 +14,7 @@ const filename = path.join(__dirname, '..', 'public', 'version.json'); const filedata = { commit, source: pkg.homepage, - version: process.env.CIRCLE_TAG || `v${ pkg.version }` + version: process.env.CIRCLE_TAG || `v${pkg.version}` }; fs.writeFileSync(filename, JSON.stringify(filedata, null, 2) + '\n'); diff --git a/test/frontend/frontend.bundle.js b/test/frontend/frontend.bundle.js index e3882cbb..af584d1b 100644 --- a/test/frontend/frontend.bundle.js +++ b/test/frontend/frontend.bundle.js @@ -9,7 +9,7 @@ window.Raven = { captureException: function(err) { console.error(err, err.stack); } -} +}; window.FakeFile = FakeFile; window.FileSender = require('../../frontend/src/fileSender'); diff --git a/test/frontend/frontend.test.js b/test/frontend/frontend.test.js index 1e286008..56fd222f 100644 --- a/test/frontend/frontend.test.js +++ b/test/frontend/frontend.test.js @@ -15,83 +15,84 @@ let originalBlob; describe('File Sender', function() { before(function() { - server.respondImmediately = true; - server.respondWith( - 'POST', - '/upload', - function(request) { - const reader = new FileReader(); - reader.readAsArrayBuffer(request.requestBody.get('data')); + server.respondImmediately = true; + server.respondWith('POST', '/upload', function(request) { + const reader = new FileReader(); + reader.readAsArrayBuffer(request.requestBody.get('data')); - reader.onload = function(event) { - file = this.result; - } + reader.onload = function(event) { + file = this.result; + }; - const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']); - request.respond( - 200, - {'Content-Type': 'application/json'}, - JSON.stringify({url: 'some url', - id: responseObj.id, - delete: responseObj.delete}) - ) - } - ) - }) + const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']); + request.respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + url: 'some url', + id: responseObj.id, + delete: responseObj.delete + }) + ); + }); + }); it('Should get a loading event emission', function() { - const file = new FakeFile('hello_world.txt', ['This is some data.']) - const fs = new FileSender(file); - let testLoading = true; + const file = new FakeFile('hello_world.txt', ['This is some data.']); + const fs = new FileSender(file); + let testLoading = true; - fs.on('loading', isStillLoading => { - assert(!(!testLoading && isStillLoading)); - testLoading = isStillLoading; + fs.on('loading', isStillLoading => { + assert(!(!testLoading && isStillLoading)); + testLoading = isStillLoading; + }); + + return fs + .upload() + .then(info => { + assert(info); + assert(!testLoading); }) - - return fs.upload() - .then(info => { - assert(info); - assert(!testLoading); - }) - .catch(err => { - console.log(err, err.stack); - assert.fail(); - }); - }) + .catch(err => { + console.log(err, err.stack); + assert.fail(); + }); + }); it('Should get a hashing event emission', function() { - const file = new FakeFile('hello_world.txt', ['This is some data.']) + const file = new FakeFile('hello_world.txt', ['This is some data.']); const fs = new FileSender(file); let testHashing = true; fs.on('hashing', isStillHashing => { - assert(!(!testHashing && isStillHashing)); - testHashing = isStillHashing; - }) + assert(!(!testHashing && isStillHashing)); + testHashing = isStillHashing; + }); - return fs.upload() - .then(info => { - assert(info); - assert(!testHashing); - }) - .catch(err => { - console.log(err, err.stack); - assert.fail(); - }); - }) + return fs + .upload() + .then(info => { + assert(info); + assert(!testHashing); + }) + .catch(err => { + console.log(err, err.stack); + assert.fail(); + }); + }); it('Should get a encrypting event emission', function() { - const file = new FakeFile('hello_world.txt', ['This is some data.']) + const file = new FakeFile('hello_world.txt', ['This is some data.']); const fs = new FileSender(file); let testEncrypting = true; fs.on('encrypting', isStillEncrypting => { assert(!(!testEncrypting && isStillEncrypting)); testEncrypting = isStillEncrypting; - }) + }); - return fs.upload() + return fs + .upload() .then(info => { assert(info); assert(!testEncrypting); @@ -100,67 +101,68 @@ describe('File Sender', function() { console.log(err, err.stack); assert.fail(); }); - }) + }); it('Should encrypt a file properly', function(done) { - const newFile = new FakeFile('hello_world.txt', ['This is some data.']) + const newFile = new FakeFile('hello_world.txt', ['This is some data.']); const fs = new FileSender(newFile); fs.upload().then(info => { const key = info.secretKey; secretKey = info.secretKey; const IV = info.fileId; encryptedIV = info.fileId; - - const readRaw = new FileReader; + + const readRaw = new FileReader(); readRaw.onload = function(event) { const rawArray = new Uint8Array(this.result); originalBlob = rawArray; window.crypto.subtle.digest('SHA-256', rawArray).then(hash => { fileHash = hash; - window.crypto.subtle.importKey( - 'jwk', - { - kty: 'oct', - k: key, - alg: 'A128GCM', - ext: true, - }, - { - name: 'AES-GCM' - }, - true, - ['encrypt', 'decrypt'] - ) - .then(cryptoKey => { - window.crypto.subtle.encrypt( + window.crypto.subtle + .importKey( + 'jwk', { - name: 'AES-GCM', - iv: hexToArray(IV), - additionalData: hash, - tagLength: 128 + kty: 'oct', + k: key, + alg: 'A128GCM', + ext: true }, - cryptoKey, - rawArray + { + name: 'AES-GCM' + }, + true, + ['encrypt', 'decrypt'] ) - .then(encrypted => { - assert(new Uint8Array(encrypted).toString() === - new Uint8Array(file).toString()); - done(); - }) - }) - }) - - } + .then(cryptoKey => { + window.crypto.subtle + .encrypt( + { + name: 'AES-GCM', + iv: hexToArray(IV), + additionalData: hash, + tagLength: 128 + }, + cryptoKey, + rawArray + ) + .then(encrypted => { + assert( + new Uint8Array(encrypted).toString() === + new Uint8Array(file).toString() + ); + done(); + }); + }); + }); + }; readRaw.readAsArrayBuffer(newFile); - }) - }) - + }); + }); }); describe('File Receiver', function() { - class FakeXHR { constructor() { this.response = file; @@ -169,19 +171,19 @@ describe('File Receiver', function() { static setup() { FakeXHR.prototype.open = sinon.spy(); - FakeXHR.prototype.send = function () { + FakeXHR.prototype.send = function() { this.onload(); - } + }; FakeXHR.prototype.originalXHR = window.XMLHttpRequest; - FakeXHR.prototype.getResponseHeader = function () { + FakeXHR.prototype.getResponseHeader = function() { return JSON.stringify({ aad: arrayToHex(new Uint8Array(fileHash)), filename: 'hello_world.txt', id: encryptedIV - }) - } + }); + }; window.XMLHttpRequest = FakeXHR; } @@ -191,38 +193,47 @@ describe('File Receiver', function() { window.XMLHttpRequest.prototype.originalXHR.restore(); } } - + const cb = function(done) { - if (file === undefined || - encryptedIV === undefined || - fileHash === undefined || - secretKey === undefined) { - assert.fail('Please run file sending tests before trying to receive the files.'); + if ( + file === undefined || + encryptedIV === undefined || + fileHash === undefined || + secretKey === undefined + ) { + assert.fail( + 'Please run file sending tests before trying to receive the files.' + ); done(); } FakeXHR.setup(); done(); - } + }; - before(cb) + before(cb); after(function() { FakeXHR.restore(); - }) + }); it('Should decrypt properly', function() { const fr = new FileReceiver(); location.hash = secretKey; - return fr.download().then(([decrypted, name]) => { - assert(name); - assert(new Uint8Array(decrypted).toString() === - new Uint8Array(originalBlob).toString()) - }).catch(err => { - console.log(err, err.stack); - assert.fail(); - }) - }) + return fr + .download() + .then(([decrypted, name]) => { + assert(name); + assert( + new Uint8Array(decrypted).toString() === + new Uint8Array(originalBlob).toString() + ); + }) + .catch(err => { + console.log(err, err.stack); + assert.fail(); + }); + }); it('Should emit decrypting events', function() { const fr = new FileReceiver(); @@ -237,17 +248,20 @@ describe('File Receiver', function() { fr.on('safe', isSafe => { assert(isSafe); - }) + }); - return fr.download().then(([decrypted, name]) => { - assert(decrypted); - assert(name); - assert(!testDecrypting); - }).catch(err => { - console.log(err, err.stack); - assert.fail(); - }) - }) + return fr + .download() + .then(([decrypted, name]) => { + assert(decrypted); + assert(name); + assert(!testDecrypting); + }) + .catch(err => { + console.log(err, err.stack); + assert.fail(); + }); + }); it('Should emit hashing events', function() { const fr = new FileReceiver(); @@ -262,99 +276,109 @@ describe('File Receiver', function() { fr.on('safe', isSafe => { assert(isSafe); - }) + }); - return fr.download().then(([decrypted, name]) => { - assert(decrypted); - assert(name); - assert(!testHashing); - }).catch(err => { - assert.fail(); - }) - }) + return fr + .download() + .then(([decrypted, name]) => { + assert(decrypted); + assert(name); + assert(!testHashing); + }) + .catch(err => { + assert.fail(); + }); + }); it('Should catch fraudulent checksums', function(done) { // Use the secret key and file hash of the previous file to encrypt, // which has a different hash than this one (different strings). - const newFile = new FakeFile('hello_world.txt', - ['This is some data, with a changed hash.']) + const newFile = new FakeFile('hello_world.txt', [ + 'This is some data, with a changed hash.' + ]); const readRaw = new FileReader(); - + readRaw.onload = function(event) { const plaintext = new Uint8Array(this.result); - window.crypto.subtle.importKey( - 'jwk', - { - kty: 'oct', - k: secretKey, - alg: 'A128GCM', - ext: true - }, - { - name: 'AES-GCM' - }, - true, - ['encrypt', 'decrypt'] - ) - .then(key => { - // The file hash used here is the hash of the fake - // file from the previous test; it's a phony checksum. - return window.crypto.subtle.encrypt( + window.crypto.subtle + .importKey( + 'jwk', { - name: 'AES-GCM', - iv: hexToArray(encryptedIV), - additionalData: fileHash, - tagLength: 128 + kty: 'oct', + k: secretKey, + alg: 'A128GCM', + ext: true }, - key, - plaintext + { + name: 'AES-GCM' + }, + true, + ['encrypt', 'decrypt'] ) - }) - .then(encrypted => { - file = encrypted; - const fr = new FileReceiver(); - location.hash = secretKey; - - fr.on('unsafe', isUnsafe => { - assert(isUnsafe) + .then(key => { + // The file hash used here is the hash of the fake + // file from the previous test; it's a phony checksum. + return window.crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: hexToArray(encryptedIV), + additionalData: fileHash, + tagLength: 128 + }, + key, + plaintext + ); }) + .then(encrypted => { + file = encrypted; + const fr = new FileReceiver(); + location.hash = secretKey; - fr.on('safe', () => { - // This event should not be emitted. - assert.fail(); - }) + fr.on('unsafe', isUnsafe => { + assert(isUnsafe); + }); - fr.download().then(() => { - assert.fail(); - done(); - }).catch(err => { - assert(1); - done(); - }) - }) - } + fr.on('safe', () => { + // This event should not be emitted. + assert.fail(); + }); + + fr + .download() + .then(() => { + assert.fail(); + done(); + }) + .catch(err => { + assert(1); + done(); + }); + }); + }; readRaw.readAsArrayBuffer(newFile); - }) + }); it('Should not decrypt with an incorrect checksum', function() { - FakeXHR.prototype.getResponseHeader = function () { + FakeXHR.prototype.getResponseHeader = function() { return JSON.stringify({ aad: 'some_bad_hashz', filename: 'hello_world.txt', id: encryptedIV - }) - } + }); + }; const fr = new FileReceiver(); location.hash = secretKey; - return fr.download().then(([decrypted, name]) => { - assert(decrypted); - assert(name); - assert.fail(); - }).catch(err => { - assert(1); - }) - }) - -}) + return fr + .download() + .then(([decrypted, name]) => { + assert(decrypted); + assert(name); + assert.fail(); + }) + .catch(err => { + assert(1); + }); + }); +}); diff --git a/test/server/server.test.js b/test/server/server.test.js index 8e2dc191..2ad1c05c 100644 --- a/test/server/server.test.js +++ b/test/server/server.test.js @@ -4,7 +4,6 @@ const proxyquire = require('proxyquire'); const request = require('supertest'); const fs = require('fs'); - const logStub = {}; logStub.info = sinon.stub(); logStub.error = sinon.stub(); @@ -38,17 +37,21 @@ describe('Server integration tests', function() { storage.flushall(); storage.quit(); server.close(); - }) + }); function upload() { - return request(server).post('/upload') - .field('fname', 'test_upload.txt') - .set('X-File-Metadata', JSON.stringify({ - aad: '11111', - id: '111111111111111111111111', - filename: 'test_upload.txt' - })) - .attach('file', './test/test_upload.txt') + return request(server) + .post('/upload') + .field('fname', 'test_upload.txt') + .set( + 'X-File-Metadata', + JSON.stringify({ + aad: '11111', + id: '111111111111111111111111', + filename: 'test_upload.txt' + }) + ) + .attach('file', './test/test_upload.txt'); } it('Responds with a 200 when the service is up', function() { @@ -56,115 +59,123 @@ describe('Server integration tests', function() { }); it('Rejects with a 404 when a file id is not valid', function() { - return request(server).post('/upload/123') - .field('fname', 'test_upload.txt') - .set('X-File-Metadata', JSON.stringify({ - 'silly': 'text' - })) - .attach('file', './test/test_upload.txt') - .expect(404) - }) + return request(server) + .post('/upload/123') + .field('fname', 'test_upload.txt') + .set( + 'X-File-Metadata', + JSON.stringify({ + silly: 'text' + }) + ) + .attach('file', './test/test_upload.txt') + .expect(404); + }); it('Accepts a file and stores it when properly uploaded', function(done) { upload().then(res => { - assert(res.body.hasOwnProperty('delete')); - uuid = res.body.delete; - assert(res.body.hasOwnProperty('url')); - assert(res.body.hasOwnProperty('id')); - fileId = res.body.id; - fs.access('./static/' + fileId, fs.constants.F_OK, err => { - if (err) { - done(new Error('The file does not exist')); - } else { - done(); - } - }) - }) - }) + assert(res.body.hasOwnProperty('delete')); + uuid = res.body.delete; + assert(res.body.hasOwnProperty('url')); + assert(res.body.hasOwnProperty('id')); + fileId = res.body.id; + fs.access('./static/' + fileId, fs.constants.F_OK, err => { + if (err) { + done(new Error('The file does not exist')); + } else { + done(); + } + }); + }); + }); it('Responds with a 200 if a file exists', function() { - return request(server).get('/exists/' + fileId) - .expect(200) - }) + return request(server).get('/exists/' + fileId).expect(200); + }); it('Exists in the redis server', function() { - return storage.exists(fileId) - .then(() => assert(1)) - .catch(err => assert.fail()) - }) + return storage + .exists(fileId) + .then(() => assert(1)) + .catch(err => assert.fail()); + }); it('Fails delete if the delete token does not match', function() { - return request(server).post('/delete/' + fileId) - .send({ delete_token: 11 }) - .expect(404); - }) + return request(server) + .post('/delete/' + fileId) + .send({ delete_token: 11 }) + .expect(404); + }); it('Fails delete if the id is invalid', function() { - return request(server).post('/delete/1') - .expect(404); - }) + return request(server).post('/delete/1').expect(404); + }); - it('Successfully deletes if the id is valid and the delete token matches', function(done) { - request(server).post('/delete/' + fileId) - .send({ delete_token: uuid }) - .expect(200) - .then(() => { - fs.access('./static/' + fileId, fs.constants.F_OK, err => { - if (err) { - done(); - } else { - done(new Error('The file does not exist')); - } - }) - }) - }) + it('Successfully deletes if the id is valid and the delete token matches', function( + done + ) { + request(server) + .post('/delete/' + fileId) + .send({ delete_token: uuid }) + .expect(200) + .then(() => { + fs.access('./static/' + fileId, fs.constants.F_OK, err => { + if (err) { + done(); + } else { + done(new Error('The file does not exist')); + } + }); + }); + }); it('Responds with a 404 if a file does not exist', function() { - return request(server).get('/exists/notfound') - .expect(404) - }) + return request(server).get('/exists/notfound').expect(404); + }); it('Uploads properly after a delete', function(done) { upload().then(res => { - assert(res.body.hasOwnProperty('delete')); - uuid = res.body.delete; - assert(res.body.hasOwnProperty('url')); - assert(res.body.hasOwnProperty('id')); - fileId = res.body.id; - fs.access('./static/' + fileId, fs.constants.F_OK, err => { - if (err) { - done(new Error('The file does not exist')); - } else { - done(); - } - }) - }) - }) + assert(res.body.hasOwnProperty('delete')); + uuid = res.body.delete; + assert(res.body.hasOwnProperty('url')); + assert(res.body.hasOwnProperty('id')); + fileId = res.body.id; + fs.access('./static/' + fileId, fs.constants.F_OK, err => { + if (err) { + done(new Error('The file does not exist')); + } else { + done(); + } + }); + }); + }); it('Responds with a 200 for the download page', function() { - return request(server).get('/download/' + fileId) - .expect(200); - }) + return request(server).get('/download/' + fileId).expect(200); + }); it('Downloads a file properly', function() { - return request(server).get('/assets/download/' + fileId) - .then(res => { - assert(res.header.hasOwnProperty('content-disposition')); - assert(res.header.hasOwnProperty('content-type')) - assert(res.header.hasOwnProperty('content-length')) - assert(res.header.hasOwnProperty('x-file-metadata')) - assert.equal(res.header['content-disposition'], 'attachment; filename=test_upload.txt') - assert.equal(res.header['content-type'], 'application/octet-stream') - }) - }) + return request(server).get('/assets/download/' + fileId).then(res => { + assert(res.header.hasOwnProperty('content-disposition')); + assert(res.header.hasOwnProperty('content-type')); + assert(res.header.hasOwnProperty('content-length')); + assert(res.header.hasOwnProperty('x-file-metadata')); + assert.equal( + res.header['content-disposition'], + 'attachment; filename=test_upload.txt' + ); + assert.equal(res.header['content-type'], 'application/octet-stream'); + }); + }); it('The file is deleted after one download', function() { assert(!fs.existsSync('./static/' + fileId)); - }) + }); it('No longer exists in the redis server', function() { - return storage.exists(fileId) - .then(() => assert.fail()) - .catch(err => assert(1)) - }) + return storage + .exists(fileId) + .then(() => assert.fail()) + .catch(err => assert(1)); + }); }); diff --git a/test/unit/aws.storage.test.js b/test/unit/aws.storage.test.js index b4507e38..d9c142e2 100644 --- a/test/unit/aws.storage.test.js +++ b/test/unit/aws.storage.test.js @@ -110,9 +110,9 @@ describe('Testing Set using aws', function() { it('Should pass when the file is successfully uploaded', function() { const buf = Buffer.alloc(10); sinon.stub(crypto, 'randomBytes').returns(buf); - s3Stub.upload.returns({promise: () => Promise.resolve()}); + s3Stub.upload.returns({ promise: () => Promise.resolve() }); return storage - .set('123', {on: sinon.stub()}, 'Filename.moz', {}) + .set('123', { on: sinon.stub() }, 'Filename.moz', {}) .then(() => { assert(expire.calledOnce); assert(expire.calledWith('123', 86400)); @@ -121,9 +121,9 @@ describe('Testing Set using aws', function() { }); it('Should fail if there was an error during uploading', function() { - s3Stub.upload.returns({promise: () => Promise.reject()}); + s3Stub.upload.returns({ promise: () => Promise.reject() }); return storage - .set('123', {on: sinon.stub()}, 'Filename.moz', 'url.com') + .set('123', { on: sinon.stub() }, 'Filename.moz', 'url.com') .then(_reply => assert.fail()) .catch(err => assert(1)); });