diff --git a/frontend/src/download.js b/frontend/src/download.js index 2522f1e2..2d348cba 100644 --- a/frontend/src/download.js +++ b/frontend/src/download.js @@ -5,26 +5,29 @@ import Storage from './storage'; import * as links from './links'; import * as metrics from './metrics'; import * as progress from './progress'; -import $ from 'jquery/dist/jquery.slim'; const storage = new Storage(); function onUnload(size) { metrics.cancelledDownload({ size }); } -function download() { - const $downloadBtn = $('#download-btn'); - const $title = $('.title'); - const $file = $('#dl-file'); - const size = Number($file.attr('data-size')); - const ttl = Number($file.attr('data-ttl')); +async function download() { + const downloadBtn = document.getElementById('download-btn'); + const downloadPanel = document.getElementById('download-page-one'); + const progressPanel = document.getElementById('download-progress'); + const file = document.getElementById('dl-file'); + const size = Number(file.getAttribute('data-size')); + const ttl = Number(file.getAttribute('data-ttl')); const unloadHandler = onUnload.bind(null, size); const startTime = Date.now(); - const fileReceiver = new FileReceiver(); + const fileReceiver = new FileReceiver( + '/assets' + location.pathname.slice(0, -1), + location.hash.slice(1) + ); - $downloadBtn.attr('disabled', 'disabled'); - $('#download-page-one').attr('hidden', true); - $('#download-progress').removeAttr('hidden'); + downloadBtn.disabled = true; + downloadPanel.hidden = true; + progressPanel.hidden = false; metrics.startedDownload({ size, ttl }); links.setOpenInNewTab(true); window.addEventListener('unload', unloadHandler); @@ -41,76 +44,72 @@ function download() { document.l10n.formatValue('decryptingFile').then(progress.setText); }); - fileReceiver - .download() - .catch(err => { - metrics.stoppedDownload({ size, err }); + try { + const file = await fileReceiver.download(); + const endTime = Date.now(); + const time = endTime - startTime; + const downloadTime = endTime - downloadEnd; + const speed = size / (downloadTime / 1000); - if (err.message === 'notfound') { - location.reload(); - } else { - document.l10n.formatValue('errorPageHeader').then(translated => { - $title.text(translated); - }); - $downloadBtn.attr('hidden', true); - $('#expired-img').removeAttr('hidden'); - } - throw err; - }) - .then(([decrypted, file]) => { - const fname = file.name; - const endTime = Date.now(); - const time = endTime - startTime; - const downloadTime = endTime - downloadEnd; - const speed = size / (downloadTime / 1000); - storage.totalDownloads += 1; - metrics.completedDownload({ size, time, speed }); - progress.setText(' '); - document.l10n - .formatValues('downloadNotification', 'downloadFinish') - .then(translated => { - notify(translated[0]); - $title.text(translated[1]); - }); + links.setOpenInNewTab(false); + storage.totalDownloads += 1; + metrics.completedDownload({ size, time, speed }); + progress.setText(' '); + document.l10n + .formatValues('downloadNotification', 'downloadFinish') + .then(translated => { + notify(translated[0]); + document.getElementById('dl-title').textContent = translated[1]; + document.querySelector('#download-progress .description').textContent = + ' '; + }); + const dataView = new DataView(file.plaintext); + const blob = new Blob([dataView], { type: file.type }); + const downloadUrl = URL.createObjectURL(blob); - const dataView = new DataView(decrypted); - const blob = new Blob([dataView], { type: file.type }); - const downloadUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = downloadUrl; + if (window.navigator.msSaveBlob) { + window.navigator.msSaveBlob(blob, file.name); + return; + } + a.download = file.name; + document.body.appendChild(a); + a.click(); + URL.revokeObjectURL(downloadUrl); + } catch (err) { + metrics.stoppedDownload({ size, err }); - const a = document.createElement('a'); - a.href = downloadUrl; - if (window.navigator.msSaveBlob) { - // if we are in microsoft edge or IE - window.navigator.msSaveBlob(blob, fname); - return; - } - a.download = fname; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(downloadUrl); - }) - .catch(err => { - Raven.captureException(err); - return Promise.reject(err); - }) - .then(() => links.setOpenInNewTab(false)); + if (err.message === 'notfound') { + location.reload(); + } else { + progressPanel.hidden = true; + downloadPanel.hidden = true; + document.getElementById('upload-error').hidden = false; + } + Raven.captureException(err); + } } -$(() => { - const $file = $('#dl-file'); - const filename = $file.attr('data-filename'); - const b = Number($file.attr('data-size')); +document.addEventListener('DOMContentLoaded', function() { + const file = document.getElementById('dl-file'); + const filename = file.getAttribute('data-filename'); + const b = Number(file.getAttribute('data-size')); const size = bytes(b); - document.l10n - .formatValue('downloadFileSize', { size }) - .then(str => $('#dl-filesize').text(str)); + document.l10n.formatValue('downloadFileSize', { size }).then(str => { + document.getElementById('dl-filesize').textContent = str; + }); document.l10n .formatValue('downloadingPageProgress', { filename, size }) - .then(str => $('#dl-title').text(str)); + .then(str => { + document.getElementById('dl-title').textContent = str; + }); gcmCompliant() .then(() => { - $('#download-btn').on('click', download); + document + .getElementById('download-btn') + .addEventListener('click', download); }) .catch(err => { metrics.unsupported({ err }).then(() => { diff --git a/frontend/src/fileReceiver.js b/frontend/src/fileReceiver.js index 53506a7d..3b9401b2 100644 --- a/frontend/src/fileReceiver.js +++ b/frontend/src/fileReceiver.js @@ -2,87 +2,80 @@ import EventEmitter from 'events'; import { hexToArray } from './utils'; export default class FileReceiver extends EventEmitter { - constructor() { + constructor(url, k) { super(); + this.key = window.crypto.subtle.importKey( + 'jwk', + { + k, + kty: 'oct', + alg: 'A128GCM', + ext: true + }, + { + name: 'AES-GCM' + }, + false, + ['decrypt'] + ); + this.url = url; } - download() { - return window.crypto.subtle - .importKey( - 'jwk', - { - kty: 'oct', - k: location.hash.slice(1), - alg: 'A128GCM', - ext: true - }, - { - name: 'AES-GCM' - }, - true, - ['encrypt', 'decrypt'] - ) - .then(key => { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); + downloadFile() { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); - xhr.onprogress = event => { - if (event.lengthComputable && event.target.status !== 404) { - this.emit('progress', [event.loaded, event.total]); - } - }; + xhr.onprogress = event => { + if (event.lengthComputable && event.target.status !== 404) { + this.emit('progress', [event.loaded, event.total]); + } + }; - xhr.onload = function(event) { - if (xhr.status === 404) { - reject(new Error('notfound')); - return; - } + xhr.onload = function(event) { + if (xhr.status === 404) { + reject(new Error('notfound')); + return; + } - const blob = new Blob([this.response]); - const type = xhr.getResponseHeader('Content-Type'); - const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata')); - const fileReader = new FileReader(); - fileReader.onload = function() { - resolve([ - { - data: this.result, - filename: meta.filename, - type, - iv: meta.id - }, - key - ]); - }; + const blob = new Blob([this.response]); + const type = xhr.getResponseHeader('Content-Type'); + const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata')); + const fileReader = new FileReader(); + fileReader.onload = function() { + resolve({ + data: this.result, + name: meta.filename, + type, + iv: meta.id + }); + }; - fileReader.readAsArrayBuffer(blob); - }; + fileReader.readAsArrayBuffer(blob); + }; - xhr.open('get', '/assets' + location.pathname.slice(0, -1), true); - xhr.responseType = 'blob'; - xhr.send(); - }); - }) - .then(([fdata, key]) => { - this.emit('decrypting'); - return Promise.all([ - window.crypto.subtle - .decrypt( - { - name: 'AES-GCM', - iv: hexToArray(fdata.iv), - tagLength: 128 - }, - key, - fdata.data - ) - .then(decrypted => { - return Promise.resolve(decrypted); - }), - { - name: decodeURIComponent(fdata.filename), - type: fdata.type - } - ]); - }); + xhr.open('get', this.url); + xhr.responseType = 'blob'; + xhr.send(); + }); + } + + async download() { + const key = await this.key; + const file = await this.downloadFile(); + this.emit('decrypting'); + const plaintext = await window.crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: hexToArray(file.iv), + tagLength: 128 + }, + key, + file.data + ); + return { + plaintext, + name: decodeURIComponent(file.name), + type: file.type + }; } } diff --git a/views/download.handlebars b/views/download.handlebars index 25080ce3..b78f6460 100644 --- a/views/download.handlebars +++ b/views/download.handlebars @@ -35,5 +35,10 @@ +