diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2ff1484e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +node_modules +public/bundle.js +static/* +!static/info.txt diff --git a/frontend/src/fileSender.js b/frontend/src/fileSender.js new file mode 100644 index 00000000..356c6cf7 --- /dev/null +++ b/frontend/src/fileSender.js @@ -0,0 +1,103 @@ +const EventEmitter = require('events'); +const { ivToStr } = require('./utils'); + +class FileSender extends EventEmitter { + constructor(file) { + super(); + this.file = file; + this.iv = window.crypto.getRandomValues(new Uint8Array(16)); + } + + static delete(fileId, token) { + return new Promise((resolve, reject) => { + if (!fileId || !token) { + return resolve(); + } + let xhr = new XMLHttpRequest(); + xhr.open('post', '/delete/' + fileId, true); + xhr.setRequestHeader('Content-Type', 'application/json'); + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + resolve(); + } + + if (xhr.status === 200) { + console.log('The file was successfully deleted.'); + } else { + console.log('The file has expired, or has already been deleted.'); + } + }; + + xhr.send(JSON.stringify({ delete_token: token })); + }); + } + + upload() { + return Promise.all([ + window.crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 128 + }, + true, + ['encrypt', 'decrypt'] + ), + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(this.file); + reader.onload = function(event) { + resolve(new Uint8Array(this.result)); + }; + }) + ]) + .then(([secretKey, plaintext]) => { + return Promise.all([ + window.crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: this.iv + }, + secretKey, + plaintext + ), + window.crypto.subtle.exportKey('jwk', secretKey) + ]); + }) + .then(([encrypted, keydata]) => { + return new Promise((resolve, reject) => { + let file = this.file; + let fileId = ivToStr(this.iv); + let dataView = new DataView(encrypted); + let blob = new Blob([dataView], { type: file.type }); + let fd = new FormData(); + fd.append('fname', file.name); + fd.append('data', blob, file.name); + + let xhr = new XMLHttpRequest(); + xhr.open('post', '/upload/' + fileId, true); + + xhr.upload.addEventListener('progress', e => { + if (e.lengthComputable) { + let percentComplete = Math.floor(e.loaded / e.total * 100); + this.emit('progress', percentComplete); + } + }); + + xhr.onreadystatechange = () => { + if (xhr.readyState == XMLHttpRequest.DONE) { + resolve({ + fileId: fileId, + secretKey: keydata.k, + deleteToken: xhr.responseText + }); + } + }; + + xhr.send(fd); + }); + }); + } +} + +module.exports = FileSender; diff --git a/frontend/src/upload.js b/frontend/src/upload.js index 2cc529b4..c2d13eb1 100644 --- a/frontend/src/upload.js +++ b/frontend/src/upload.js @@ -1,125 +1,11 @@ -const EventEmitter = require('events'); -const UIWrapper = require('./ui').UIWrapper; +const FileSender = require('./fileSender'); let onChange = event => { - let file = event.target.files[0]; - let reader = new FileReader(); - reader.readAsArrayBuffer(file); + const file = event.target.files[0]; - let random_iv = window.crypto.getRandomValues(new Uint8Array(16)); - let hex = ivToStr(random_iv); - - reader.onload = function(event) { - let self = this; - window.crypto.subtle - .generateKey( - { - name: 'AES-CBC', - length: 128 - }, - true, - ['encrypt', 'decrypt'] - ) - .then(key => { - let arrayBuffer = self.result; - let array = new Uint8Array(arrayBuffer); - - window.crypto.subtle - .encrypt( - { - name: 'AES-CBC', - iv: random_iv - }, - key, - array - ) - .then(uploadFile.bind(null, file, hex, key)) - .catch(err => console.error(err)); - }) - .catch(err => console.error(err)); - }; -}; - -window.onChange = onChange; - -let uploadFile = (file, hex, key, encrypted) => { - let dataView = new DataView(encrypted); - let blob = new Blob([dataView], { type: file.type }); - - let fd = new FormData(); - fd.append('fname', file.name); - fd.append('data', blob, file.name); - - let xhr = new XMLHttpRequest(); - xhr.open('post', '/upload/' + hex, true); - - let listelem = setupUI(); - listelem.emit('name', file.name); - xhr.upload.addEventListener('progress', updateProgress.bind(null, listelem)); - - xhr.onreadystatechange = () => { - if (xhr.readyState == XMLHttpRequest.DONE) { - window.crypto.subtle.exportKey('jwk', key).then(keydata => { - localStorage.setItem(hex, xhr.responseText); - - listelem.emit( - 'link', - 'http://localhost:3000/download/' + hex + '/#' + keydata.k - ); - - console.log( - 'Share this link with a friend: http://localhost:3000/download/' + - hex + - '/#' + - keydata.k - ); - }); - } - }; - - xhr.send(fd); -}; - -let updateProgress = (UIelem, e) => { - if (e.lengthComputable) { - let percentComplete = Math.floor(e.loaded / e.total * 100); - UIelem.emit('progress', 'Progress: ' + percentComplete + '%'); - - if (percentComplete === 100) { - let btn = document.createElement('button'); - btn.innerText = 'Delete from server'; - btn.addEventListener('click', () => { - let segments = UIelem.link.innerText.split('/'); - let key = segments[segments.length - 2]; - - let xhr = new XMLHttpRequest(); - xhr.open('post', '/delete/' + key, true); - xhr.setRequestHeader('Content-Type', 'application/json'); - if (!localStorage.getItem(key)) return; - - xhr.send(JSON.stringify({ delete_token: localStorage.getItem(key) })); - - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - document.getElementById('uploaded_files').removeChild(UIelem.li); - localStorage.removeItem(key); - } - - if (xhr.status === 200) { - console.log('The file was successfully deleted.'); - } else { - console.log('The file has expired, or has already been deleted.'); - } - }; - }); - UIelem.li.appendChild(btn); - } - } -}; - -let setupUI = () => { let li = document.createElement('li'); let name = document.createElement('p'); + name.innerText = file.name; li.appendChild(name); let link = document.createElement('a'); @@ -130,27 +16,28 @@ let setupUI = () => { document.getElementById('uploaded_files').appendChild(li); - return new UIWrapper(li, name, link, progress); + const fileSender = new FileSender(file); + fileSender.on('progress', percentComplete => { + progress.innerText = `Progress: ${percentComplete}%`; + }); + fileSender.upload().then(info => { + const url = `${window.location.origin}/${info.fileId}/#${info.secretKey}`; + localStorage.setItem(info.fileId, info.deleteToken); + link.innerText = url; + link.setAttribute('href', url); + let btn = document.createElement('button'); + btn.innerText = 'Delete from server'; + btn.addEventListener('click', () => { + FileSender.delete( + info.fileId, + localStorage.getItem(info.fileId) + ).then(() => { + document.getElementById('uploaded_files').removeChild(li); + localStorage.removeItem(info.fileId); + }); + }); + li.appendChild(btn); + }); }; -let ivToStr = iv => { - let hexStr = ''; - for (let i in iv) { - if (iv[i] < 16) { - hexStr += '0' + iv[i].toString(16); - } else { - hexStr += iv[i].toString(16); - } - } - window.hexStr = hexStr; - return hexStr; -}; - -let strToIv = str => { - let iv = new Uint8Array(16); - for (let i = 0; i < str.length; i += 2) { - iv[i / 2] = parseInt(str.charAt(i) + str.charAt(i + 1), 16); - } - - return iv; -}; +window.onChange = onChange; diff --git a/frontend/src/utils.js b/frontend/src/utils.js new file mode 100644 index 00000000..547211fe --- /dev/null +++ b/frontend/src/utils.js @@ -0,0 +1,26 @@ +function ivToStr(iv) { + let hexStr = ''; + for (let i in iv) { + if (iv[i] < 16) { + hexStr += '0' + iv[i].toString(16); + } else { + hexStr += iv[i].toString(16); + } + } + window.hexStr = hexStr; + return hexStr; +} + +function strToIv(str) { + let iv = new Uint8Array(16); + for (let i = 0; i < str.length; i += 2) { + iv[i / 2] = parseInt(str.charAt(i) + str.charAt(i + 1), 16); + } + + return iv; +} + +module.exports = { + ivToStr, + strToIv +};