Merge pull request #189 from mozilla/csp

Add CSP directives
This commit is contained in:
Danny Coates 2017-07-12 11:21:55 -07:00 committed by GitHub
commit 52173bf6e7
9 changed files with 170 additions and 124 deletions

View File

@ -9,7 +9,8 @@ $(document).ready(function() {
$('#send-file').click(() => { $('#send-file').click(() => {
window.location.replace(`${window.location.origin}`); window.location.replace(`${window.location.origin}`);
}); });
const download = () => { $('#download-btn').click(download);
function download() {
const fileReceiver = new FileReceiver(); const fileReceiver = new FileReceiver();
const name = document.createElement('p'); const name = document.createElement('p');
const $btn = $('#download-btn'); const $btn = $('#download-btn');
@ -37,20 +38,20 @@ $(document).ready(function() {
fileReceiver.on('decrypting', isStillDecrypting => { fileReceiver.on('decrypting', isStillDecrypting => {
// The file is being decrypted // The file is being decrypted
if (isStillDecrypting) { if (isStillDecrypting) {
console.log('Decrypting') console.log('Decrypting');
} else { } else {
console.log('Done decrypting') console.log('Done decrypting');
} }
}) });
fileReceiver.on('hashing', isStillHashing => { fileReceiver.on('hashing', isStillHashing => {
// The file is being hashed to make sure a malicious user hasn't tampered with it // The file is being hashed to make sure a malicious user hasn't tampered with it
if (isStillHashing) { if (isStillHashing) {
console.log('Checking file integrity') console.log('Checking file integrity');
} else { } else {
console.log('Integrity check done') console.log('Integrity check done');
} }
}) });
fileReceiver fileReceiver
.download() .download()
@ -84,7 +85,5 @@ $(document).ready(function() {
Raven.captureException(err); Raven.captureException(err);
return Promise.reject(err); return Promise.reject(err);
}); });
}; }
window.download = download;
}); });

View File

@ -61,52 +61,60 @@ class FileReceiver extends EventEmitter {
true, true,
['encrypt', 'decrypt'] ['encrypt', 'decrypt']
) )
]).then(([fdata, key]) => { ])
this.emit('decrypting', true); .then(([fdata, key]) => {
return Promise.all([ this.emit('decrypting', true);
window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: hexToArray(fdata.iv),
additionalData: hexToArray(fdata.aad)
},
key,
fdata.data
).then(decrypted => {
this.emit('decrypting', false);
return new Promise((resolve, reject) => {
resolve(decrypted);
})
}),
new Promise((resolve, reject) => {
resolve(fdata.filename);
}),
new Promise((resolve, reject) => {
resolve(hexToArray(fdata.aad));
})
]);
}).then(([decrypted, fname, proposedHash]) => {
this.emit('hashing', true);
return window.crypto.subtle.digest('SHA-256', decrypted).then(calculatedHash => {
this.emit('hashing', false);
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([ return Promise.all([
window.crypto.subtle
.decrypt(
{
name: 'AES-GCM',
iv: hexToArray(fdata.iv),
additionalData: hexToArray(fdata.aad)
},
key,
fdata.data
)
.then(decrypted => {
this.emit('decrypting', false);
return new Promise((resolve, reject) => {
resolve(decrypted);
});
}),
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
resolve(decrypted); resolve(fdata.filename);
}), }),
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
resolve(fname); resolve(hexToArray(fdata.aad));
}) })
]); ]);
}) })
}) .then(([decrypted, fname, proposedHash]) => {
this.emit('hashing', true);
return window.crypto.subtle
.digest('SHA-256', decrypted)
.then(calculatedHash => {
this.emit('hashing', false);
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([
new Promise((resolve, reject) => {
resolve(decrypted);
}),
new Promise((resolve, reject) => {
resolve(fname);
})
]);
});
});
} }
} }

View File

@ -61,8 +61,8 @@ class FileSender extends EventEmitter {
window.crypto.subtle.digest('SHA-256', plaintext).then(hash => { window.crypto.subtle.digest('SHA-256', plaintext).then(hash => {
self.emit('hashing', false); self.emit('hashing', false);
self.emit('encrypting', true); self.emit('encrypting', true);
resolve({plaintext: plaintext, hash: new Uint8Array(hash)}); resolve({ plaintext: plaintext, hash: new Uint8Array(hash) });
}) });
}; };
reader.onerror = function(err) { reader.onerror = function(err) {
reject(err); reject(err);
@ -81,14 +81,17 @@ class FileSender extends EventEmitter {
}, },
secretKey, secretKey,
file.plaintext file.plaintext
).then(encrypted => { )
.then(encrypted => {
self.emit('encrypting', false); self.emit('encrypting', false);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
resolve(encrypted); resolve(encrypted);
}) });
}), }),
window.crypto.subtle.exportKey('jwk', secretKey), window.crypto.subtle.exportKey('jwk', secretKey),
new Promise((resolve, reject) => { resolve(file.hash) }) new Promise((resolve, reject) => {
resolve(file.hash);
})
]); ]);
}) })
.then(([encrypted, keydata, hash]) => { .then(([encrypted, keydata, hash]) => {

View File

@ -8,8 +8,10 @@ $(document).ready(function() {
gcmCompliant().catch(err => { gcmCompliant().catch(err => {
$('#page-one').hide(); $('#page-one').hide();
$('#compliance-error').show(); $('#compliance-error').show();
}) });
$('#file-upload').change(onUpload);
$('#page-one').on('dragover', allowDrop).on('drop', onUpload);
// reset copy button // reset copy button
const $copyBtn = $('#copy-btn'); const $copyBtn = $('#copy-btn');
$copyBtn.attr('disabled', false); $copyBtn.attr('disabled', false);
@ -61,11 +63,11 @@ $(document).ready(function() {
}); });
// on file upload by browse or drag & drop // on file upload by browse or drag & drop
window.onUpload = event => { function onUpload(event) {
event.preventDefault(); event.preventDefault();
let file = ''; let file = '';
if (event.type === 'drop') { if (event.type === 'drop') {
file = event.dataTransfer.files[0]; file = event.originalEvent.dataTransfer.files[0];
} else { } else {
file = event.target.files[0]; file = event.target.files[0];
} }
@ -88,29 +90,29 @@ $(document).ready(function() {
fileSender.on('loading', isStillLoading => { fileSender.on('loading', isStillLoading => {
// The file is loading into Firefox at this stage // The file is loading into Firefox at this stage
if (isStillLoading) { if (isStillLoading) {
console.log('Processing') console.log('Processing');
} else { } else {
console.log('Finished processing') console.log('Finished processing');
} }
}) });
fileSender.on('hashing', isStillHashing => { fileSender.on('hashing', isStillHashing => {
// The file is being hashed // The file is being hashed
if (isStillHashing) { if (isStillHashing) {
console.log('Hashing'); console.log('Hashing');
} else { } else {
console.log('Finished hashing') console.log('Finished hashing');
} }
}) });
fileSender.on('encrypting', isStillEncrypting => { fileSender.on('encrypting', isStillEncrypting => {
// The file is being encrypted // The file is being encrypted
if (isStillEncrypting) { if (isStillEncrypting) {
console.log('Encrypting'); console.log('Encrypting');
} else { } else {
console.log('Finished encrypting') console.log('Finished encrypting');
} }
}) });
fileSender fileSender
.upload() .upload()
@ -143,11 +145,11 @@ $(document).ready(function() {
$('#page-one').hide(); $('#page-one').hide();
$('#upload-error').show(); $('#upload-error').show();
}); });
}; }
window.allowDrop = function(ev) { function allowDrop(ev) {
ev.preventDefault(); ev.preventDefault();
}; }
function checkExistence(id, populate) { function checkExistence(id, populate) {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();

View File

@ -34,34 +34,38 @@ function notify(str) {
function gcmCompliant() { function gcmCompliant() {
try { try {
return window.crypto.subtle.generateKey( return window.crypto.subtle
{ .generateKey(
name: 'AES-GCM',
length: 128
},
true,
['encrypt', 'decrypt']
).then(key => {
return window.crypto.subtle.encrypt(
{ {
name: 'AES-GCM', name: 'AES-GCM',
iv: window.crypto.getRandomValues(new Uint8Array(12)), length: 128
additionalData: window.crypto.getRandomValues(new Uint8Array(6)),
tagLength: 128
}, },
key, true,
new ArrayBuffer(8) ['encrypt', 'decrypt']
) )
.then(() => { .then(key => {
return Promise.resolve() return window.crypto.subtle
.encrypt(
{
name: 'AES-GCM',
iv: window.crypto.getRandomValues(new Uint8Array(12)),
additionalData: window.crypto.getRandomValues(new Uint8Array(6)),
tagLength: 128
},
key,
new ArrayBuffer(8)
)
.then(() => {
return Promise.resolve();
})
.catch(err => {
return Promise.reject();
});
}) })
.catch(err => { .catch(err => {
return Promise.reject() return Promise.reject();
}) });
}).catch(err => { } catch (err) {
return Promise.reject();
})
} catch(err) {
return Promise.reject(); return Promise.reject();
} }
} }

View File

@ -32,6 +32,30 @@ app.engine(
app.set('view engine', 'handlebars'); app.set('view engine', 'handlebars');
app.use(helmet()); app.use(helmet());
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ['\'self\''],
connectSrc: [
'\'self\'',
'https://sentry.prod.mozaws.net',
'https://www.google-analytics.com',
'https://ssl.google-analytics.com'
],
imgSrc: [
'\'self\'',
'https://www.google-analytics.com',
'https://ssl.google-analytics.com'
],
scriptSrc: ['\'self\'', 'https://ssl.google-analytics.com'],
styleSrc: ['\'self\'', 'https://code.cdn.mozilla.net'],
fontSrc: ['\'self\'', 'https://code.cdn.mozilla.net'],
formAction: ['\'none\''],
frameAncestors: ['\'none\''],
objectSrc: ['\'none\'']
}
})
);
app.use(busboy()); app.use(busboy());
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(express.static(STATIC_PATH)); app.use(express.static(STATIC_PATH));
@ -92,33 +116,35 @@ app.get('/assets/download/:id', (req, res) => {
storage storage
.metadata(id) .metadata(id)
.then(meta => { .then(meta => {
storage.length(id).then(contentLength => { storage
res.writeHead(200, { .length(id)
'Content-Disposition': 'attachment; filename=' + meta.filename, .then(contentLength => {
'Content-Type': 'application/octet-stream', res.writeHead(200, {
'Content-Length': contentLength, 'Content-Disposition': 'attachment; filename=' + meta.filename,
'X-File-Metadata': JSON.stringify(meta) 'Content-Type': 'application/octet-stream',
}); 'Content-Length': contentLength,
const file_stream = storage.get(id); 'X-File-Metadata': JSON.stringify(meta)
});
const file_stream = storage.get(id);
file_stream.on('end', () => { file_stream.on('end', () => {
storage storage
.forceDelete(id) .forceDelete(id)
.then(err => { .then(err => {
if (!err) { if (!err) {
log.info('Deleted:', id); log.info('Deleted:', id);
} }
}) })
.catch(err => { .catch(err => {
log.info('DeleteError:', id); log.info('DeleteError:', id);
}); });
}); });
file_stream.pipe(res); file_stream.pipe(res);
}) })
.catch(err => { .catch(err => {
res.sendStatus(404); res.sendStatus(404);
}); });
}) })
.catch(err => { .catch(err => {
res.sendStatus(404); res.sendStatus(404);
@ -157,15 +183,17 @@ app.post('/upload', (req, res, next) => {
try { try {
meta = JSON.parse(req.header('X-File-Metadata')); meta = JSON.parse(req.header('X-File-Metadata'));
} catch(err) { } catch (err) {
res.sendStatus(400); res.sendStatus(400);
return; return;
} }
if (!validateIV(meta.id) || if (
!meta.hasOwnProperty('aad') || !validateIV(meta.id) ||
!meta.hasOwnProperty('id') || !meta.hasOwnProperty('aad') ||
!meta.hasOwnProperty('filename')) { !meta.hasOwnProperty('id') ||
!meta.hasOwnProperty('filename')
) {
res.sendStatus(404); res.sendStatus(404);
return; return;
} }
@ -216,4 +244,4 @@ const validateIV = route_id => {
module.exports = { module.exports = {
server: server, server: server,
storage: storage storage: storage
} };

View File

@ -129,7 +129,9 @@ function localGet(id) {
function localSet(newId, file, filename, meta) { function localSet(newId, file, filename, meta) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fstream = fs.createWriteStream(path.join(__dirname, '../static', newId)); const fstream = fs.createWriteStream(
path.join(__dirname, '../static', newId)
);
file.pipe(fstream); file.pipe(fstream);
fstream.on('close', () => { fstream.on('close', () => {
redis_client.hmset(newId, meta); redis_client.hmset(newId, meta);

View File

@ -23,7 +23,7 @@
</div> </div>
<div id="download-page-one"> <div id="download-page-one">
<div> <div>
<button id="download-btn" onclick="download()">Download File</button> <button id="download-btn">Download File</button>
</div> </div>
<div id='expired-img'> <div id='expired-img'>
<img src='/resources/link_expired.png' /> <img src='/resources/link_expired.png' />

View File

@ -19,7 +19,7 @@
<div class="title"> <div class="title">
Share your files quickly, privately and securely. Share your files quickly, privately and securely.
</div> </div>
<div class="upload-window" ondrop="onUpload(event)" ondragover="allowDrop(event)"> <div class="upload-window">
<div id="upload-img"><img src="/resources/upload.svg" alt="Upload"/></div> <div id="upload-img"><img src="/resources/upload.svg" alt="Upload"/></div>
<div> <div>
DRAG &amp; DROP DRAG &amp; DROP
@ -31,7 +31,7 @@
<div id="browse"> <div id="browse">
<form method="post" action="upload" enctype="multipart/form-data"> <form method="post" action="upload" enctype="multipart/form-data">
<label for="file-upload" class="file-upload">browse</label> <label for="file-upload" class="file-upload">browse</label>
<input id="file-upload" type="file" onchange="onUpload(event)" name="fileUploaded" /> <input id="file-upload" type="file" name="fileUploaded" />
</form> </form>
</div> </div>
</div> </div>