import Nanobus from 'nanobus'; import { arrayToHex, bytes } from './utils'; export default class FileSender extends Nanobus { constructor(file) { super('FileSender'); this.file = file; this.msg = 'importingFile'; this.progress = [0, 1]; this.cancelled = false; this.iv = window.crypto.getRandomValues(new Uint8Array(12)); this.uploadXHR = new XMLHttpRequest(); this.key = window.crypto.subtle.generateKey( { name: 'AES-GCM', length: 128 }, true, ['encrypt'] ); } static delete(id, token) { return new Promise((resolve, reject) => { if (!id || !token) { return reject(); } const xhr = new XMLHttpRequest(); xhr.open('POST', `/api/delete/${id}`); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { resolve(); } }; xhr.send(JSON.stringify({ delete_token: token })); }); } get progressRatio() { return this.progress[0] / this.progress[1]; } get sizes() { return { partialSize: bytes(this.progress[0]), totalSize: bytes(this.progress[1]) }; } cancel() { this.cancelled = true; if (this.msg === 'fileSizeProgress') { this.uploadXHR.abort(); } } readFile() { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsArrayBuffer(this.file); reader.onload = function(event) { const plaintext = new Uint8Array(this.result); resolve(plaintext); }; reader.onerror = function(err) { reject(err); }; }); } uploadFile(encrypted, keydata) { return new Promise((resolve, reject) => { const file = this.file; const id = arrayToHex(this.iv); const dataView = new DataView(encrypted); const blob = new Blob([dataView], { type: file.type }); const fd = new FormData(); fd.append('data', blob, file.name); const xhr = this.uploadXHR; xhr.upload.addEventListener('progress', e => { if (e.lengthComputable) { this.progress = [e.loaded, e.total]; this.emit('progress', this.progress); } }); xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { this.progress = [1, 1]; this.msg = 'notifyUploadDone'; const responseObj = JSON.parse(xhr.responseText); return resolve({ url: responseObj.url, id: responseObj.id, secretKey: keydata.k, deleteToken: responseObj.delete }); } this.msg = 'errorPageHeader'; reject(new Error(xhr.status)); } }; xhr.open('post', '/api/upload', true); xhr.setRequestHeader( 'X-File-Metadata', JSON.stringify({ id: id, filename: encodeURIComponent(file.name) }) ); xhr.send(fd); this.msg = 'fileSizeProgress'; }); } async upload() { const key = await this.key; const plaintext = await this.readFile(); if (this.cancelled) { throw new Error(0); } this.msg = 'encryptingFile'; this.emit('encrypting'); const encrypted = await window.crypto.subtle.encrypt( { name: 'AES-GCM', iv: this.iv, tagLength: 128 }, key, plaintext ); if (this.cancelled) { throw new Error(0); } const keydata = await window.crypto.subtle.exportKey('jwk', key); return this.uploadFile(encrypted, keydata); } }