Add optional password to the download url

This commit is contained in:
Danny Coates 2017-08-31 09:43:36 -07:00
parent 837747f8f7
commit bc24a069da
No known key found for this signature in database
GPG key ID: 4C442633C62E00CB
28 changed files with 805 additions and 241 deletions

View file

@ -153,6 +153,7 @@ export default function(state, emitter) {
state.storage.totalUploads += 1;
emitter.emit('pushState', `/share/${info.id}`);
} catch (err) {
console.error(err);
state.transfer = null;
if (err.message === '0') {
//cancelled. do nothing
@ -161,23 +162,51 @@ export default function(state, emitter) {
}
state.raven.captureException(err);
metrics.stoppedUpload({ size, type, err });
emitter.emit('replaceState', '/error');
emitter.emit('pushState', '/error');
}
});
emitter.on('download', async file => {
const size = file.size;
emitter.on('password', async ({ password, file }) => {
try {
await FileSender.setPassword(password, file);
metrics.addedPassword({ size: file.size });
file.password = password;
state.storage.writeFiles();
} catch (e) {
console.error(e);
}
render();
});
emitter.on('preview', async () => {
const file = state.fileInfo;
const url = `/api/download/${file.id}`;
const receiver = new FileReceiver(url, file.key);
const receiver = new FileReceiver(url, file);
receiver.on('progress', updateProgress);
receiver.on('decrypting', render);
state.transfer = receiver;
const links = openLinksInNewTab();
try {
await receiver.getMetadata(file.nonce);
} catch (e) {
if (e.message === '401') {
file.password = null;
if (!file.pwd) {
return emitter.emit('pushState', '/404');
}
}
}
render();
});
emitter.on('download', async file => {
state.transfer.on('progress', render);
state.transfer.on('decrypting', render);
const links = openLinksInNewTab();
const size = file.size;
try {
const start = Date.now();
metrics.startedDownload({ size: file.size, ttl: file.ttl });
const f = await receiver.download();
const f = await state.transfer.download(file.nonce);
const time = Date.now() - start;
const speed = size / (time / 1000);
await delay(1000);
@ -187,13 +216,14 @@ export default function(state, emitter) {
metrics.completedDownload({ size, time, speed });
emitter.emit('pushState', '/completed');
} catch (err) {
console.error(err);
// TODO cancelled download
const location = err.message === 'notfound' ? '/404' : '/error';
if (location === '/error') {
state.raven.captureException(err);
metrics.stoppedDownload({ size, err });
}
emitter.emit('replaceState', location);
emitter.emit('pushState', location);
} finally {
state.transfer = null;
openLinksInNewTab(links, false);

View file

@ -1,25 +1,104 @@
import Nanobus from 'nanobus';
import { hexToArray, bytes } from './utils';
import { arrayToB64, b64ToArray, bytes } from './utils';
export default class FileReceiver extends Nanobus {
constructor(url, k) {
constructor(url, file) {
super('FileReceiver');
this.key = window.crypto.subtle.importKey(
'jwk',
{
k,
kty: 'oct',
alg: 'A128GCM',
ext: true
},
{
name: 'AES-GCM'
},
this.secretKeyPromise = window.crypto.subtle.importKey(
'raw',
b64ToArray(file.key),
'HKDF',
false,
['decrypt']
['deriveKey']
);
this.encryptKeyPromise = this.secretKeyPromise.then(sk => {
const encoder = new TextEncoder();
return window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('encryption'),
hash: 'SHA-256'
},
sk,
{
name: 'AES-GCM',
length: 128
},
false,
['decrypt']
);
});
if (file.pwd) {
const encoder = new TextEncoder();
console.log(file.password + file.url);
this.authKeyPromise = window.crypto.subtle
.importKey(
'raw',
encoder.encode(file.password),
{ name: 'PBKDF2' },
false,
['deriveKey']
)
.then(pwdKey =>
window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode(file.url),
iterations: 100,
hash: 'SHA-256'
},
pwdKey,
{
name: 'HMAC',
hash: 'SHA-256'
},
true,
['sign']
)
);
} else {
this.authKeyPromise = this.secretKeyPromise.then(sk => {
const encoder = new TextEncoder();
return window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('authentication'),
hash: 'SHA-256'
},
sk,
{
name: 'HMAC',
hash: { name: 'SHA-256' }
},
false,
['sign']
);
});
}
this.metaKeyPromise = this.secretKeyPromise.then(sk => {
const encoder = new TextEncoder();
return window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('metadata'),
hash: 'SHA-256'
},
sk,
{
name: 'AES-GCM',
length: 128
},
false,
['decrypt']
);
});
this.file = file;
this.url = url;
this.msg = 'fileSizeProgress';
this.state = 'initialized';
this.progress = [0, 1];
}
@ -38,7 +117,65 @@ export default class FileReceiver extends Nanobus {
// TODO
}
downloadFile() {
fetchMetadata(sig) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
this.file.nonce = nonce;
if (xhr.status === 200) {
return resolve(xhr.response);
}
reject(new Error(xhr.status));
}
};
xhr.onerror = () => reject(new Error(0));
xhr.ontimeout = () => reject(new Error(0));
xhr.open('get', `/api/metadata/${this.file.id}`);
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
xhr.responseType = 'json';
xhr.timeout = 2000;
xhr.send();
});
}
async getMetadata(nonce) {
try {
const authKey = await this.authKeyPromise;
const sig = await window.crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(nonce)
);
const data = await this.fetchMetadata(new Uint8Array(sig));
const metaKey = await this.metaKeyPromise;
const json = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: new Uint8Array(12),
tagLength: 128
},
metaKey,
b64ToArray(data.metadata)
);
const decoder = new TextDecoder();
const meta = JSON.parse(decoder.decode(json));
this.file.name = meta.name;
this.file.type = meta.type;
this.file.iv = meta.iv;
this.file.size = data.size;
this.file.ttl = data.ttl;
this.state = 'ready';
} catch (e) {
this.state = 'invalid';
throw e;
}
}
downloadFile(sig) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
@ -49,52 +186,67 @@ export default class FileReceiver extends Nanobus {
}
};
xhr.onload = function(event) {
xhr.onload = event => {
if (xhr.status === 404) {
reject(new Error('notfound'));
return;
}
const blob = new Blob([this.response]);
const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
if (xhr.status !== 200) {
return reject(new Error(xhr.status));
}
const blob = new Blob([xhr.response]);
const fileReader = new FileReader();
fileReader.onload = function() {
resolve({
data: this.result,
name: meta.filename,
type: meta.mimeType,
iv: meta.id
});
resolve(this.result);
};
fileReader.readAsArrayBuffer(blob);
};
xhr.open('get', this.url);
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
xhr.responseType = 'blob';
xhr.send();
});
}
async download() {
const key = await this.key;
const file = await this.downloadFile();
this.msg = 'decryptingFile';
this.emit('decrypting');
const plaintext = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: hexToArray(file.iv),
tagLength: 128
},
key,
file.data
);
this.msg = 'downloadFinish';
return {
plaintext,
name: decodeURIComponent(file.name),
type: file.type
};
async download(nonce) {
this.state = 'downloading';
this.emit('progress', this.progress);
try {
const encryptKey = await this.encryptKeyPromise;
const authKey = await this.authKeyPromise;
const sig = await window.crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(nonce)
);
const ciphertext = await this.downloadFile(new Uint8Array(sig));
this.msg = 'decryptingFile';
this.emit('decrypting');
const plaintext = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: b64ToArray(this.file.iv),
tagLength: 128
},
encryptKey,
ciphertext
);
this.msg = 'downloadFinish';
this.state = 'complete';
return {
plaintext,
name: decodeURIComponent(this.file.name),
type: this.file.type
};
} catch (e) {
this.state = 'invalid';
throw e;
}
}
}

View file

@ -1,5 +1,5 @@
import Nanobus from 'nanobus';
import { arrayToHex, bytes } from './utils';
import { arrayToB64, b64ToArray, bytes } from './utils';
export default class FileSender extends Nanobus {
constructor(file) {
@ -10,13 +10,13 @@ export default class FileSender extends Nanobus {
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']
this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
this.secretKey = window.crypto.subtle.importKey(
'raw',
this.rawSecret,
'HKDF',
false,
['deriveKey']
);
}
@ -71,14 +71,12 @@ export default class FileSender extends Nanobus {
});
}
uploadFile(encrypted, keydata) {
uploadFile(encrypted, metadata, rawAuth) {
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 blob = new Blob([dataView], { type: 'application/octet-stream' });
const fd = new FormData();
fd.append('data', blob, file.name);
fd.append('data', blob);
const xhr = this.uploadXHR;
@ -92,14 +90,18 @@ export default class FileSender extends Nanobus {
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
const nonce = xhr
.getResponseHeader('WWW-Authenticate')
.split(' ')[1];
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
secretKey: arrayToB64(this.rawSecret),
deleteToken: responseObj.delete,
nonce
});
}
this.msg = 'errorPageHeader';
@ -110,18 +112,62 @@ export default class FileSender extends Nanobus {
xhr.open('post', '/api/upload', true);
xhr.setRequestHeader(
'X-File-Metadata',
JSON.stringify({
id: id,
filename: encodeURIComponent(file.name)
})
arrayToB64(new Uint8Array(metadata))
);
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(rawAuth)}`);
xhr.send(fd);
this.msg = 'fileSizeProgress';
});
}
async upload() {
const key = await this.key;
const encoder = new TextEncoder();
const secretKey = await this.secretKey;
const encryptKey = await window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('encryption'),
hash: 'SHA-256'
},
secretKey,
{
name: 'AES-GCM',
length: 128
},
false,
['encrypt']
);
const authKey = await window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('authentication'),
hash: 'SHA-256'
},
secretKey,
{
name: 'HMAC',
hash: 'SHA-256'
},
true,
['sign']
);
const metaKey = await window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('metadata'),
hash: 'SHA-256'
},
secretKey,
{
name: 'AES-GCM',
length: 128
},
false,
['encrypt']
);
const plaintext = await this.readFile();
if (this.cancelled) {
throw new Error(0);
@ -134,13 +180,112 @@ export default class FileSender extends Nanobus {
iv: this.iv,
tagLength: 128
},
key,
encryptKey,
plaintext
);
const metadata = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: new Uint8Array(12),
tagLength: 128
},
metaKey,
encoder.encode(
JSON.stringify({
iv: arrayToB64(this.iv),
name: this.file.name,
type: this.file.type
})
)
);
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
if (this.cancelled) {
throw new Error(0);
}
const keydata = await window.crypto.subtle.exportKey('jwk', key);
return this.uploadFile(encrypted, keydata);
return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth));
}
static async setPassword(password, file) {
const encoder = new TextEncoder();
const secretKey = await window.crypto.subtle.importKey(
'raw',
b64ToArray(file.secretKey),
'HKDF',
false,
['deriveKey']
);
const authKey = await window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('authentication'),
hash: 'SHA-256'
},
secretKey,
{
name: 'HMAC',
hash: 'SHA-256'
},
true,
['sign']
);
const sig = await window.crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(file.nonce)
);
const pwdKey = await window.crypto.subtle.importKey(
'raw',
encoder.encode(password),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
const newAuthKey = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode(file.url),
iterations: 100,
hash: 'SHA-256'
},
pwdKey,
{
name: 'HMAC',
hash: 'SHA-256'
},
true,
['sign']
);
const rawAuth = await window.crypto.subtle.exportKey('raw', newAuthKey);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
return resolve(xhr.response);
}
if (xhr.status === 401) {
const nonce = xhr
.getResponseHeader('WWW-Authenticate')
.split(' ')[1];
file.nonce = nonce;
}
reject(new Error(xhr.status));
}
};
xhr.onerror = () => reject(new Error(0));
xhr.ontimeout = () => reject(new Error(0));
xhr.open('post', `/api/password/${file.id}`);
xhr.setRequestHeader(
'Authorization',
`send-v1 ${arrayToB64(new Uint8Array(sig))}`
);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'json';
xhr.timeout = 2000;
xhr.send(JSON.stringify({ auth: arrayToB64(new Uint8Array(rawAuth)) }));
});
}
}

View file

@ -147,6 +147,15 @@ function completedUpload(params) {
});
}
function addedPassword(params) {
return sendEvent('sender', 'password-added', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads
});
}
function startedDownload(params) {
return sendEvent('recipient', 'download-started', {
cm1: params.size,
@ -262,6 +271,7 @@ export {
cancelledDownload,
stoppedDownload,
completedDownload,
addedPassword,
restart,
unsupported
};

View file

@ -3,7 +3,10 @@ const download = require('../templates/download');
module.exports = function(state, emit) {
if (state.transfer) {
return download(state, emit);
const s = state.transfer.state;
if (s === 'downloading' || s === 'complete') {
return download(state, emit);
}
}
return preview(state, emit);
};

View file

@ -2,7 +2,8 @@ const welcome = require('../templates/welcome');
const upload = require('../templates/upload');
module.exports = function(state, emit) {
if (state.transfer) {
if (state.transfer && state.transfer.iv) {
//TODO relying on 'iv' is gross
return upload(state, emit);
}
return welcome(state, emit);

View file

@ -92,11 +92,7 @@ class Storage {
}
getFileById(id) {
try {
return JSON.parse(this.engine.getItem(id));
} catch (e) {
return null;
}
return this._files.find(f => f.id === id);
}
get(id) {
@ -114,6 +110,10 @@ class Storage {
this._files.push(file);
this.engine.setItem(file.id, JSON.stringify(file));
}
writeFiles() {
this._files.forEach(f => this.engine.setItem(f.id, JSON.stringify(f)));
}
}
export default new Storage();

View file

@ -0,0 +1,41 @@
const html = require('choo/html');
module.exports = function(state, emit) {
const fileInfo = state.fileInfo;
const label =
fileInfo.password === null
? html`
<label class="red"
for="unlock-input">${state.translate('incorrectPassword')}</label>`
: html`
<label for="unlock-input">
${state.translate('unlockInputLabel')}
</label>`;
const div = html`
<div class="enterPassword">
${label}
<form id="unlock" onsubmit=${checkPassword}>
<input id="unlock-input"
autocomplete="off"
placeholder="${state.translate('unlockInputPlaceholder')}"
type="password"/>
<input type="submit"
id="unlock-btn"
class="btn"
value="${state.translate('unlockButtonLabel')}"/>
</form>
</div>`;
function checkPassword(event) {
event.preventDefault();
const password = document.getElementById('unlock-input').value;
if (password.length > 0) {
document.getElementById('unlock-btn').disabled = true;
state.fileInfo.url = window.location.href;
state.fileInfo.password = password;
emit('preview');
}
}
return div;
};

View file

@ -1,6 +1,7 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const notFound = require('./notFound');
const downloadPassword = require('./downloadPassword');
const { bytes } = require('../utils');
function getFileFromDOM() {
@ -8,11 +9,9 @@ function getFileFromDOM() {
if (!el) {
return null;
}
const data = el.dataset;
return {
name: data.name,
size: parseInt(data.size, 10),
ttl: parseInt(data.ttl, 10)
nonce: el.getAttribute('data-nonce'),
pwd: !!+el.getAttribute('data-requires-password')
};
}
@ -24,40 +23,47 @@ module.exports = function(state, emit) {
state.fileInfo.id = state.params.id;
state.fileInfo.key = state.params.key;
const fileInfo = state.fileInfo;
const size = bytes(fileInfo.size);
const size = fileInfo.size
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
: '';
let action = html`
<div>
<img src="${assets.get('illustration_download.svg')}"
id="download-img"
alt="${state.translate('downloadAltText')}"/>
<div>
<button id="download-btn"
class="btn"
onclick=${download}>${state.translate('downloadButtonLabel')}
</button>
</div>
</div>`;
if (fileInfo.pwd && !fileInfo.password) {
action = downloadPassword(state, emit);
} else if (!state.transfer) {
emit('preview');
}
const title = fileInfo.name
? state.translate('downloadFileName', { filename: fileInfo.name })
: state.translate('downloadFileTitle');
const div = html`
<div id="page-one">
<div id="download">
<div id="download-page-one">
<div class="title">
<span id="dl-file"
data-name="${fileInfo.name}"
data-size="${fileInfo.size}"
data-ttl="${fileInfo.ttl}">${state.translate('downloadFileName', {
filename: fileInfo.name
})}</span>
<span id="dl-filesize">${' ' +
state.translate('downloadFileSize', { size })}</span>
data-nonce="${fileInfo.nonce}"
data-requires-password="${fileInfo.pwd}">${title}</span>
<span id="dl-filesize">${' ' + size}</span>
</div>
<div class="description">${state.translate('downloadMessage')}</div>
<img
src="${assets.get('illustration_download.svg')}"
id="download-img"
alt="${state.translate('downloadAltText')}"/>
<div>
<button
id="download-btn"
class="btn"
title="${state.translate('downloadButtonLabel')}"
onclick=${download}>${state.translate(
'downloadButtonLabel'
)}</button>
</div>
${action}
</div>
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
</div>
</div>
`;
function download(event) {
event.preventDefault();
emit('download', fileInfo);

View file

@ -1,6 +1,7 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const notFound = require('./notFound');
const uploadPassword = require('./uploadPassword');
const { allowedCopy, delay, fadeOut } = require('../utils');
module.exports = function(state, emit) {
@ -8,25 +9,37 @@ module.exports = function(state, emit) {
if (!file) {
return notFound(state, emit);
}
const passwordComplete = html`
<div class="selectPassword">
Password: ${file.password}
</div>`;
const passwordSection = file.password
? passwordComplete
: uploadPassword(state, emit);
const div = html`
<div id="share-link" class="fadeIn">
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
<div id="share-window">
<div id="copy-text">${state.translate('copyUrlFormLabelWithName', {
filename: file.name
})}</div>
<div id="copy-text">
${state.translate('copyUrlFormLabelWithName', {
filename: file.name
})}</div>
<div id="copy">
<input id="link" type="url" value="${file.url}" readonly="true"/>
<button id="copy-btn" class="btn" title="${state.translate(
'copyUrlFormButton'
)}" onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
<button id="copy-btn"
class="btn"
title="${state.translate('copyUrlFormButton')}"
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
</div>
<button id="delete-file" class="btn" title="${state.translate(
'deleteFileButton'
)}" onclick=${deleteFile}>${state.translate('deleteFileButton')}</button>
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
'sendAnotherFileLink'
)}</a>
${passwordSection}
<button id="delete-file"
class="btn"
title="${state.translate('deleteFileButton')}"
onclick=${deleteFile}>${state.translate('deleteFileButton')}</button>
<a class="send-new"
data-state="completed"
href="/"
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
</div>
</div>
`;

View file

@ -0,0 +1,37 @@
const html = require('choo/html');
module.exports = function(state, emit) {
const file = state.storage.getFileById(state.params.id);
const div = html`
<div class="selectPassword">
<div>
<input id="addPassword" type="checkbox" onchange=${togglePasswordInput}/>
<label for="addPassword">${state.translate(
'requirePasswordCheckbox'
)}</label>
</div>
<form class="setPassword hidden" onsubmit=${setPassword}>
<input id="unlock-input"
autocomplete="off"
placeholder="${state.translate('unlockInputPlaceholder')}"/>
<input type="submit"
id="unlock-btn"
class="btn"
value="${state.translate('addPasswordButton')}"/>
</form>
</div>`;
function togglePasswordInput() {
document.querySelector('.setPassword').classList.toggle('hidden');
}
function setPassword(event) {
event.preventDefault();
const password = document.getElementById('unlock-input').value;
if (password.length > 0) {
emit('password', { password, file });
}
}
return div;
};

View file

@ -9,24 +9,31 @@ module.exports = function(state, emit) {
<div class="title">${state.translate('uploadPageHeader')}</div>
<div class="description">
<div>${state.translate('uploadPageExplainer')}</div>
<a href="https://testpilot.firefox.com/experiments/send" class="link">${state.translate(
'uploadPageLearnMore'
)}</a>
<a href="https://testpilot.firefox.com/experiments/send"
class="link">${state.translate('uploadPageLearnMore')}</a>
</div>
<div class="upload-window" ondragover=${dragover} ondragleave=${dragleave}>
<div id="upload-img"><img src="${assets.get(
'upload.svg'
)}" title="${state.translate('uploadSvgAlt')}"/></div>
<div class="upload-window"
ondragover=${dragover}
ondragleave=${dragleave}>
<div id="upload-img">
<img src="${assets.get('upload.svg')}"
title="${state.translate('uploadSvgAlt')}"/>
</div>
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
<span id="file-size-msg"><em>${state.translate(
'uploadPageSizeMessage'
)}</em></span>
<form method="post" action="upload" enctype="multipart/form-data">
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} onfocus=${onfocus} onblur=${onblur} />
<label for="file-upload" id="browse" class="btn browse" title="${state.translate(
'uploadPageBrowseButton1'
)}">${state.translate('uploadPageBrowseButton1')}</label>
</form>
<span id="file-size-msg">
<em>${state.translate('uploadPageSizeMessage')}</em>
</span>
<label for="file-upload"
id="browse"
class="btn browse"
title="${state.translate('uploadPageBrowseButton1')}">
${state.translate('uploadPageBrowseButton1')}</label>
<input id="file-upload"
type="file"
name="fileUploaded"
onfocus=${onfocus}
onblur=${onblur}
onchange=${upload} />
</div>
${fileList(state, emit)}
</div>

View file

@ -1,23 +1,18 @@
function arrayToHex(iv) {
let hexStr = '';
// eslint-disable-next-line prefer-const
for (let i in iv) {
if (iv[i] < 16) {
hexStr += '0' + iv[i].toString(16);
} else {
hexStr += iv[i].toString(16);
}
}
return hexStr;
const b64 = require('base64-js');
function arrayToB64(array) {
return b64
.fromByteArray(array)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
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);
}
return iv;
function b64ToArray(str) {
str = (str + '==='.slice((str.length + 3) % 4))
.replace(/-/g, '+')
.replace(/_/g, '/');
return b64.toByteArray(str);
}
function notify(str) {
@ -105,6 +100,9 @@ const LOCALIZE_NUMBERS = !!(
const UNITS = ['B', 'kB', 'MB', 'GB'];
function bytes(num) {
if (num < 1) {
return '0B';
}
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
const n = Number(num / Math.pow(1000, exponent));
const nStr = LOCALIZE_NUMBERS
@ -147,8 +145,8 @@ module.exports = {
bytes,
percent,
copyToClipboard,
arrayToHex,
hexToArray,
arrayToB64,
b64ToArray,
notify,
canHasSend,
isFile,