diff --git a/frontend/src/fileReceiver.js b/frontend/src/fileReceiver.js index d73fae3c..37924038 100644 --- a/frontend/src/fileReceiver.js +++ b/frontend/src/fileReceiver.js @@ -1,12 +1,9 @@ const EventEmitter = require('events'); -const { strToIv } = require('./utils'); - -const Raven = window.Raven; +const { hexToArray } = require('./utils'); class FileReceiver extends EventEmitter { constructor() { super(); - this.salt = strToIv(location.pathname.slice(10, -1)); } download() { @@ -34,11 +31,12 @@ class FileReceiver extends EventEmitter { const blob = new Blob([this.response]); const fileReader = new FileReader(); fileReader.onload = function() { + const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata')); resolve({ data: this.result, - fname: xhr - .getResponseHeader('Content-Disposition') - .match(/=(.+)/)[1] + aad: meta.aad, + filename: meta.filename, + iv: meta.id }); }; @@ -54,36 +52,53 @@ class FileReceiver extends EventEmitter { { kty: 'oct', k: location.hash.slice(1), - alg: 'A128CBC', + alg: 'A128GCM', ext: true }, { - name: 'AES-CBC' + name: 'AES-GCM' }, true, ['encrypt', 'decrypt'] ) - ]) - .then(([fdata, key]) => { - const salt = this.salt; + ]).then(([fdata, key]) => { + return Promise.all([ + window.crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: hexToArray(fdata.iv), + additionalData: hexToArray(fdata.aad) + }, + key, + fdata.data + ), + new Promise((resolve, reject) => { + resolve(fdata.filename); + }), + new Promise((resolve, reject) => { + resolve(hexToArray(fdata.aad)); + }) + ]); + }).then(([decrypted, fname, proposedHash]) => { + return window.crypto.subtle.digest('SHA-256', decrypted).then(calculatedHash => { + const integrity = new Uint8Array(calculatedHash).toString() === proposedHash.toString(); + if (!integrity) { + return new Promise((resolve, reject) => { + console.log('This file has been tampered with.') + reject(); + }) + } + return Promise.all([ - window.crypto.subtle.decrypt( - { - name: 'AES-CBC', - iv: salt - }, - key, - fdata.data - ), new Promise((resolve, reject) => { - resolve(fdata.fname); + resolve(decrypted); + }), + new Promise((resolve, reject) => { + resolve(fname); }) ]); }) - .catch(err => { - Raven.captureException(err); - return Promise.reject(err); - }); + }) } } diff --git a/frontend/src/fileSender.js b/frontend/src/fileSender.js index 546613af..8be5f4ae 100644 --- a/frontend/src/fileSender.js +++ b/frontend/src/fileSender.js @@ -1,5 +1,5 @@ const EventEmitter = require('events'); -const { ivToStr } = require('./utils'); +const { arrayToHex } = require('./utils'); const Raven = window.Raven; @@ -7,7 +7,7 @@ class FileSender extends EventEmitter { constructor(file) { super(); this.file = file; - this.iv = window.crypto.getRandomValues(new Uint8Array(16)); + this.iv = window.crypto.getRandomValues(new Uint8Array(12)); } static delete(fileId, token) { @@ -37,46 +37,56 @@ class FileSender extends EventEmitter { upload() { return Promise.all([ - window.crypto.subtle.generateKey( - { - name: 'AES-CBC', - length: 128 - }, - true, - ['encrypt', 'decrypt'] - ), + window.crypto.subtle + .generateKey( + { + name: 'AES-GCM', + length: 128 + }, + true, + ['encrypt', 'decrypt'] + ) + .catch(err => + console.log('There was an error generating a crypto key') + ), new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsArrayBuffer(this.file); reader.onload = function(event) { - resolve(new Uint8Array(this.result)); + const plaintext = new Uint8Array(this.result); + window.crypto.subtle.digest('SHA-256', plaintext).then(hash => { + resolve({plaintext: plaintext, hash: new Uint8Array(hash)}); + }) }; reader.onerror = function(err) { reject(err); }; }) ]) - .then(([secretKey, plaintext]) => { + .then(([secretKey, file]) => { return Promise.all([ - window.crypto.subtle.encrypt( - { - name: 'AES-CBC', - iv: this.iv - }, - secretKey, - plaintext - ), - window.crypto.subtle.exportKey('jwk', secretKey) + window.crypto.subtle + .encrypt( + { + name: 'AES-GCM', + iv: this.iv, + additionalData: file.hash, + tagLength: 128 + }, + secretKey, + file.plaintext + ), + window.crypto.subtle.exportKey('jwk', secretKey), + new Promise((resolve, reject) => { resolve(file.hash) }) ]); }) - .then(([encrypted, keydata]) => { + .then(([encrypted, keydata, hash]) => { return new Promise((resolve, reject) => { const file = this.file; - const fileId = ivToStr(this.iv); + const fileId = arrayToHex(this.iv); const dataView = new DataView(encrypted); const blob = new Blob([dataView], { type: file.type }); const fd = new FormData(); - fd.append('fname', file.name); fd.append('data', blob, file.name); const xhr = new XMLHttpRequest(); @@ -94,14 +104,22 @@ class FileSender extends EventEmitter { const responseObj = JSON.parse(xhr.responseText); resolve({ url: responseObj.url, - fileId: fileId, + fileId: responseObj.id, secretKey: keydata.k, deleteToken: responseObj.uuid }); } }; - xhr.open('post', '/upload/' + fileId, true); + xhr.open('post', '/upload', true); + xhr.setRequestHeader( + 'X-File-Metadata', + JSON.stringify({ + aad: arrayToHex(hash), + id: fileId, + filename: file.name + }) + ); xhr.send(fd); }); }) diff --git a/frontend/src/utils.js b/frontend/src/utils.js index e17534fa..2e6400ce 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -1,4 +1,4 @@ -function ivToStr(iv) { +function arrayToHex(iv) { let hexStr = ''; for (const i in iv) { if (iv[i] < 16) { @@ -11,8 +11,8 @@ function ivToStr(iv) { return hexStr; } -function strToIv(str) { - const iv = new Uint8Array(16); +function hexToArray(str) { + const iv = new Uint8Array(str.length / 2); for (let i = 0; i < str.length; i += 2) { iv[i / 2] = parseInt(str.charAt(i) + str.charAt(i + 1), 16); } @@ -33,7 +33,7 @@ function notify(str) { } module.exports = { - ivToStr, - strToIv, + arrayToHex, + hexToArray, notify }; diff --git a/server/portal_server.js b/server/portal_server.js index 8684417f..c6ed936d 100644 --- a/server/portal_server.js +++ b/server/portal_server.js @@ -8,6 +8,7 @@ const bytes = require('bytes'); const conf = require('./config.js'); const storage = require('./storage.js'); const Raven = require('raven'); +const crypto = require('crypto'); if (conf.sentry_dsn) { Raven.config(conf.sentry_dsn).install(); @@ -79,13 +80,14 @@ app.get('/assets/download/:id', (req, res) => { } storage - .filename(id) - .then(reply => { + .metadata(id) + .then(meta => { storage.length(id).then(contentLength => { res.writeHead(200, { - 'Content-Disposition': 'attachment; filename=' + reply, + 'Content-Disposition': 'attachment; filename=' + meta.filename, 'Content-Type': 'application/octet-stream', - 'Content-Length': contentLength + 'Content-Length': contentLength, + 'X-File-Metadata': JSON.stringify(meta) }); const file_stream = storage.get(id); @@ -135,21 +137,24 @@ app.post('/delete/:id', (req, res) => { .catch(err => res.sendStatus(404)); }); -app.post('/upload/:id', (req, res, next) => { - if (!validateID(req.params.id)) { - res.sendStatus(404); - return; - } - +app.post('/upload', (req, res, next) => { + const newId = crypto.randomBytes(5).toString('hex'); + const meta = JSON.parse(req.header('X-File-Metadata')); + meta.delete = crypto.randomBytes(10).toString('hex'); + log.info('meta', meta); req.pipe(req.busboy); + req.busboy.on('file', (fieldname, file, filename) => { - log.info('Uploading:', req.params.id); + log.info('Uploading:', newId); - const protocol = conf.env === 'production' ? 'https' : req.protocol; - const url = `${protocol}://${req.get('host')}/download/${req.params.id}/`; - - storage.set(req.params.id, file, filename, url).then(linkAndID => { - res.json(linkAndID); + storage.set(newId, file, filename, meta).then(() => { + const protocol = conf.env === 'production' ? 'https' : req.protocol; + const url = `${protocol}://${req.get('host')}/download/${newId}/`; + res.json({ + url, + delete: meta.delete, + id: newId + }); }); }); }); @@ -171,5 +176,5 @@ app.listen(conf.listen_port, () => { }); const validateID = route_id => { - return route_id.match(/^[0-9a-fA-F]{32}$/) !== null; -}; + return route_id.match(/^[0-9a-fA-F]{10}$/) !== null; +}; \ No newline at end of file diff --git a/server/storage.js b/server/storage.js index dedf0040..38559ba3 100644 --- a/server/storage.js +++ b/server/storage.js @@ -4,7 +4,6 @@ const s3 = new AWS.S3(); const conf = require('./config.js'); const fs = require('fs'); const path = require('path'); -const crypto = require('crypto'); const mozlog = require('./log.js'); @@ -27,9 +26,12 @@ if (conf.s3_bucket) { length: awsLength, get: awsGet, set: awsSet, + aad: aad, + setField: setField, delete: awsDelete, forceDelete: awsForceDelete, - ping: awsPing + ping: awsPing, + metadata }; } else { module.exports = { @@ -38,12 +40,27 @@ if (conf.s3_bucket) { length: localLength, get: localGet, set: localSet, + aad: aad, + setField: setField, delete: localDelete, forceDelete: localForceDelete, - ping: localPing + ping: localPing, + metadata }; } +function metadata(id) { + return new Promise((resolve, reject) => { + redis_client.hgetall(id, (err, reply) => { + if (!err) { + resolve(reply); + } else { + reject(err); + } + }); + }); +} + function filename(id) { return new Promise((resolve, reject) => { redis_client.hget(id, 'filename', (err, reply) => { @@ -68,6 +85,22 @@ function exists(id) { }); } +function setField(id, key, value) { + redis_client.hset(id, key, value); +} + +function aad(id) { + return new Promise((resolve, reject) => { + redis_client.hget(id, 'aad', (err, reply) => { + if (!err) { + resolve(reply); + } else { + reject(); + } + }); + }); +} + function localLength(id) { return new Promise((resolve, reject) => { try { @@ -82,24 +115,19 @@ function localGet(id) { return fs.createReadStream(path.join(__dirname, '../static', id)); } -function localSet(id, file, filename, url) { +function localSet(newId, file, filename, meta) { return new Promise((resolve, reject) => { - const fstream = fs.createWriteStream(path.join(__dirname, '../static', id)); + const fstream = fs.createWriteStream(path.join(__dirname, '../static', newId)); file.pipe(fstream); fstream.on('close', () => { - const uuid = crypto.randomBytes(10).toString('hex'); - - redis_client.hmset([id, 'filename', filename, 'delete', uuid]); - redis_client.expire(id, 86400000); - log.info('localSet:', 'Upload Finished of ' + id); - resolve({ - uuid: uuid, - url: url - }); + redis_client.hmset(newId, meta); + redis_client.expire(newId, 86400000); + log.info('localSet:', 'Upload Finished of ' + newId); + resolve(meta.delete); }); fstream.on('error', () => { - log.error('localSet:', 'Failed upload of ' + id); + log.error('localSet:', 'Failed upload of ' + newId); reject(); }); }); @@ -163,10 +191,10 @@ function awsGet(id) { } } -function awsSet(id, file, filename, url) { +function awsSet(newId, file, filename, meta) { const params = { Bucket: conf.s3_bucket, - Key: id, + Key: newId, Body: file }; @@ -176,16 +204,11 @@ function awsSet(id, file, filename, url) { log.info('awsUploadError:', err.stack); // an error occurred reject(); } else { - const uuid = crypto.randomBytes(10).toString('hex'); + redis_client.hmset(newId, meta); - redis_client.hmset([id, 'filename', filename, 'delete', uuid]); - - redis_client.expire(id, 86400000); + redis_client.expire(newId, 86400000); log.info('awsUploadFinish', 'Upload Finished of ' + filename); - resolve({ - uuid: uuid, - url: url - }); + resolve(meta.delete); } }); }); diff --git a/test/aws.storage.test.js b/test/aws.storage.test.js index 8dbb468a..eef1f2a7 100644 --- a/test/aws.storage.test.js +++ b/test/aws.storage.test.js @@ -112,11 +112,8 @@ describe('Testing Set using aws', function() { sinon.stub(crypto, 'randomBytes').returns(buf); s3Stub.upload.callsArgWith(1, null, {}); return storage - .set('123', {}, 'Filename.moz', 'url.com') - .then(reply => { - assert.equal(reply.uuid, buf.toString('hex')); - assert.equal(reply.url, 'url.com'); - assert.notEqual(reply.uuid, null); + .set('123', {}, 'Filename.moz', {}) + .then(() => { assert(expire.calledOnce); assert(expire.calledWith('123', 86400000)); }) diff --git a/test/local.storage.test.js b/test/local.storage.test.js index 659710a6..3a9993b2 100644 --- a/test/local.storage.test.js +++ b/test/local.storage.test.js @@ -122,10 +122,9 @@ describe('Testing Set to local filesystem', function() { fsStub.createWriteStream.returns({ on: stub }); return storage - .set('test', { pipe: sinon.stub() }, 'Filename.moz', 'moz.la') - .then(reply => { - assert(reply.uuid); - assert.equal(reply.url, 'moz.la'); + .set('test', { pipe: sinon.stub() }, 'Filename.moz', {}) + .then(() => { + assert(1); }) .catch(err => assert.fail()); });