add streaming

This commit is contained in:
Emily Hou 2018-06-20 17:05:33 -07:00
parent 34cb970f11
commit 1bd7e4d486
16 changed files with 438 additions and 187 deletions

View File

@ -91,47 +91,98 @@ export async function setPassword(id, owner_token, keychain) {
return response.ok;
}
export function uploadFile(
encrypted,
function asyncInitWebSocket(server) {
return new Promise(resolve => {
const ws = new WebSocket(server);
ws.onopen = () => {
resolve(ws);
};
});
}
async function upload(
ws,
stream,
streamInfo,
metadata,
verifierB64,
keychain,
onprogress
) {
const xhr = new XMLHttpRequest();
const upload = {
cancel: function() {
xhr.abort();
},
result: new Promise(function(resolve, reject) {
xhr.addEventListener('loadend', function() {
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
if (authHeader) {
keychain.nonce = parseNonce(authHeader);
}
if (xhr.status === 200) {
const responseObj = JSON.parse(xhr.responseText);
return resolve({
url: responseObj.url,
id: responseObj.id,
ownerToken: responseObj.owner
});
}
reject(new Error(xhr.status));
});
})
const metadataHeader = arrayToB64(new Uint8Array(metadata));
const fileMeta = {
fileMetadata: metadataHeader,
authorization: `send-v1 ${verifierB64}`
};
const blob = new Blob([encrypted], { type: 'application/octet-stream' });
xhr.upload.addEventListener('progress', function(event) {
if (event.lengthComputable) {
onprogress([event.loaded, event.total]);
}
//send file header
ws.send(JSON.stringify(fileMeta));
function listenForRes() {
return new Promise((resolve, reject) => {
ws.addEventListener('message', function(msg) {
const response = JSON.parse(msg.data);
resolve({
url: response.url,
id: response.id,
ownerToken: response.owner
});
xhr.open('post', '/api/upload', true);
xhr.setRequestHeader('X-File-Metadata', arrayToB64(new Uint8Array(metadata)));
xhr.setRequestHeader('Authorization', `send-v1 ${verifierB64}`);
xhr.send(blob);
return upload;
});
});
}
const resPromise = listenForRes();
const reader = stream.getReader();
let state = await reader.read();
let size = 0;
while (!state.done) {
const buf = state.value;
ws.send(buf);
if (ws.readyState !== 1) {
throw new Error(0); //should this be here
}
onprogress([Math.min(streamInfo.fileSize, size), streamInfo.fileSize]);
size += streamInfo.recordSize;
state = await reader.read();
}
const res = await resPromise;
ws.close();
return res;
}
export async function uploadWs(
encrypted,
info,
metadata,
verifierB64,
keychain,
onprogress
) {
const host = window.location.hostname;
const port = window.location.port;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = await asyncInitWebSocket(`${protocol}//${host}:${port}/api/ws`);
//console.log(`made connection to websocket: ws://${host}:${port}/api/ws`)
return {
cancel: function() {
ws.close(4000, 'upload cancelled');
},
result: upload(
ws,
encrypted,
info,
metadata,
verifierB64,
keychain,
onprogress
)
};
}
function download(id, keychain, onprogress, canceller) {
@ -151,11 +202,7 @@ function download(id, keychain, onprogress, canceller) {
}
const blob = new Blob([xhr.response]);
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(blob);
fileReader.onload = function() {
resolve(this.result);
};
resolve(blob);
});
xhr.addEventListener('progress', function(event) {
if (event.lengthComputable && event.target.status === 200) {

View File

@ -1,41 +0,0 @@
const streams = require('web-streams-polyfill');
class BlobSlicer {
constructor(blob, size, decrypt) {
this.blob = blob;
this.size = size;
this.index = 0;
this.decrypt = decrypt;
}
pull(controller) {
return new Promise((resolve, reject) => {
const bytesLeft = this.blob.size - this.index;
if (bytesLeft <= 0) {
controller.close();
return resolve();
}
let size = 0;
if (this.decrypt && this.index === 0) {
size = Math.min(21, bytesLeft);
} else {
size = Math.min(this.size, bytesLeft);
}
const blob = this.blob.slice(this.index, this.index + size);
const reader = new FileReader();
reader.onload = function() {
controller.enqueue(new Uint8Array(this.result));
resolve();
};
reader.onerror = reject;
reader.readAsArrayBuffer(blob);
this.index += size;
});
}
}
export default class BlobSliceStream extends streams.ReadableStream {
constructor(blob, size, decrypt) {
super(new BlobSlicer(blob, size, decrypt));
}
}

View File

@ -1,10 +1,12 @@
require('buffer');
import { ReadableStream, TransformStream } from 'web-streams-polyfill';
const NONCE_LENGTH = 12;
const TAG_LENGTH = 16;
const KEY_LENGTH = 16;
const MODE_ENCRYPT = 'encrypt';
const MODE_DECRYPT = 'decrypt';
const RS = 1024 * 1024;
const encoder = new TextEncoder();
@ -14,27 +16,15 @@ function generateSalt(len) {
return randSalt.buffer;
}
/*
mode: string, either 'encrypt' or 'decrypt'
ikm: Uint8Array containing key of KEY_LENGTH length
rs: int containing record size, optional
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
The transform stream takes data as UInt8Arrays on the writable side, and outputs
UInt8Arrays on the readable side.
*/
export default class ECETransformer {
export class ECETransformer {
constructor(mode, ikm, rs, salt) {
this.mode = mode;
this.prevChunk;
this.params = {};
this.seq = 0;
this.firstchunk = true;
this.rs = rs || 1024;
this.rs = rs;
this.ikm = ikm.buffer;
this.params.salt = salt;
if (!salt) {
this.params.salt = generateSalt(KEY_LENGTH);
}
this.salt = salt;
}
async generateKey() {
@ -49,7 +39,7 @@ export default class ECETransformer {
return window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: this.params.salt,
salt: this.salt,
info: encoder.encode('Content-Encoding: aes128gcm\0'),
hash: 'SHA-256'
},
@ -77,7 +67,7 @@ export default class ECETransformer {
await window.crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: this.params.salt,
salt: this.salt,
info: encoder.encode('Content-Encoding: nonce\0'),
hash: 'SHA-256'
},
@ -95,15 +85,14 @@ export default class ECETransformer {
}
generateNonce(seq) {
const nonce = Buffer.from(this.params.nonceBase);
if (seq > 0xffffffff) {
throw new Error('record sequence number exceeds limit');
}
const nonce = Buffer.from(this.nonceBase);
const m = nonce.readUIntBE(nonce.length - 4, 4);
const xor = (m ^ seq) >>> 0; //forces unsigned int xor
nonce.writeUIntBE(xor, nonce.length - 4, 4);
const m2 = nonce.readUIntBE(nonce.length - 8, 4);
const xor2 = (m2 ^ (seq >>> 4)) >>> 0;
nonce.writeUIntBE(xor2, nonce.length - 8, 4);
return nonce;
}
@ -147,7 +136,7 @@ export default class ECETransformer {
const nums = Buffer.alloc(5);
nums.writeUIntBE(this.rs, 0, 4);
nums.writeUIntBE(0, 4, 1);
return Buffer.concat([Buffer.from(this.params.salt), nums]);
return Buffer.concat([Buffer.from(this.salt), nums]);
}
//salt is arraybuffer, rs is int, length is int
@ -167,7 +156,7 @@ export default class ECETransformer {
const nonce = this.generateNonce(seq);
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce },
this.params.key,
this.key,
this.pad(buffer, isLast)
);
return Buffer.from(encrypted);
@ -181,7 +170,7 @@ export default class ECETransformer {
iv: nonce,
tagLength: 128
},
this.params.key,
this.key,
buffer
);
@ -190,8 +179,8 @@ export default class ECETransformer {
async start(controller) {
if (this.mode === MODE_ENCRYPT) {
this.params.key = await this.generateKey();
this.params.nonceBase = await this.generateNonceBase();
this.key = await this.generateKey();
this.nonceBase = await this.generateNonceBase();
controller.enqueue(this.createHeader());
} else if (this.mode !== MODE_DECRYPT) {
throw new Error('mode must be either encrypt or decrypt');
@ -208,10 +197,10 @@ export default class ECETransformer {
if (this.seq === 0) {
//the first chunk during decryption contains only the header
const header = this.readHeader(this.prevChunk);
this.params.salt = header.salt;
this.salt = header.salt;
this.rs = header.rs;
this.params.key = await this.generateKey();
this.params.nonceBase = await this.generateNonceBase();
this.key = await this.generateKey();
this.nonceBase = await this.generateNonceBase();
} else {
controller.enqueue(
await this.decryptRecord(this.prevChunk, this.seq - 1, isLast)
@ -235,3 +224,71 @@ export default class ECETransformer {
}
}
}
class BlobSlicer {
constructor(blob, rs, mode) {
this.blob = blob;
this.index = 0;
this.mode = mode;
this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : rs;
}
pull(controller) {
return new Promise((resolve, reject) => {
const bytesLeft = this.blob.size - this.index;
if (bytesLeft <= 0) {
controller.close();
return resolve();
}
let size = 1;
if (this.mode === MODE_DECRYPT && this.index === 0) {
size = Math.min(21, bytesLeft);
} else {
size = Math.min(this.chunkSize, bytesLeft);
}
const blob = this.blob.slice(this.index, this.index + size);
const reader = new FileReader();
reader.onload = () => {
controller.enqueue(new Uint8Array(reader.result));
resolve();
};
reader.onerror = reject;
reader.readAsArrayBuffer(blob);
this.index += size;
});
}
}
export class BlobSliceStream extends ReadableStream {
constructor(blob, size, mode) {
super(new BlobSlicer(blob, size, mode));
}
}
/*
input: a blob containing data to be transformed
key: Uint8Array containing key of size KEY_LENGTH
mode: string, either 'encrypt' or 'decrypt'
rs: int containing record size, optional
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
*/
export default class ECE {
constructor(input, key, mode, rs, salt) {
if (rs === undefined) {
rs = RS;
}
if (salt === undefined) {
salt = generateSalt(KEY_LENGTH);
}
this.streamInfo = {
recordSize: rs,
fileSize: input.size + 16 * Math.floor(input.size / (rs - 17))
};
input = new BlobSliceStream(input, rs, mode);
const ts = new TransformStream(new ECETransformer(mode, key, rs, salt));
this.stream = input.pipeThrough(ts);
}
}

View File

@ -51,6 +51,28 @@ export default class FileReceiver extends Nanobus {
this.state = 'ready';
}
async streamToArrayBuffer(stream) {
const reader = stream.getReader();
const chunks = [];
let length = 0;
let state = await reader.read();
while (!state.done) {
chunks.push(state.value);
length += state.value.length;
state = await reader.read();
}
const result = new Int8Array(length);
let offset = 0;
for (let i = 0; i < chunks.length; i++) {
result.set(chunks[i], offset);
offset += chunks[i].length;
}
return result.buffer;
}
async download(noSave = false) {
this.state = 'downloading';
this.downloadRequest = await downloadFile(
@ -61,13 +83,19 @@ export default class FileReceiver extends Nanobus {
this.emit('progress');
}
);
try {
const ciphertext = await this.downloadRequest.result;
this.downloadRequest = null;
this.msg = 'decryptingFile';
this.state = 'decrypting';
this.emit('decrypting');
const plaintext = await this.keychain.decryptFile(ciphertext);
const dec = await this.keychain.decryptStream(ciphertext);
const plainstream = dec.stream;
const plaintext = await this.streamToArrayBuffer(plainstream);
if (!noSave) {
await saveFile({
plaintext,

View File

@ -3,7 +3,7 @@ import Nanobus from 'nanobus';
import OwnedFile from './ownedFile';
import Keychain from './keychain';
import { arrayToB64, bytes } from './utils';
import { uploadFile } from './api';
import { uploadWs } from './api';
export default class FileSender extends Nanobus {
constructor(file) {
@ -59,20 +59,19 @@ export default class FileSender extends Nanobus {
async upload() {
const start = Date.now();
const plaintext = await this.readFile();
if (this.cancelled) {
throw new Error(0);
}
this.msg = 'encryptingFile';
this.emit('encrypting');
const encrypted = await this.keychain.encryptFile(plaintext);
const enc = await this.keychain.encryptStream(this.file);
const metadata = await this.keychain.encryptMetadata(this.file);
const authKeyB64 = await this.keychain.authKeyB64();
if (this.cancelled) {
throw new Error(0);
}
this.uploadRequest = uploadFile(
encrypted,
this.uploadRequest = await uploadWs(
enc.stream,
enc.streamInfo,
metadata,
authKeyB64,
this.keychain,
@ -81,6 +80,11 @@ export default class FileSender extends Nanobus {
this.emit('progress');
}
);
if (this.cancelled) {
throw new Error(0);
}
this.msg = 'fileSizeProgress';
this.emit('progress'); // HACK to kick MS Edge
try {

View File

@ -1,5 +1,5 @@
import { arrayToB64, b64ToArray } from './utils';
import ECE from './ece.js';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
@ -179,6 +179,16 @@ export default class Keychain {
return ciphertext;
}
async encryptStream(plaintext) {
const enc = new ECE(plaintext, this.rawSecret, 'encrypt');
return enc;
}
async decryptStream(encstream) {
const dec = new ECE(encstream, this.rawSecret, 'decrypt');
return dec;
}
async decryptFile(ciphertext) {
const encryptKey = await this.encryptKeyPromise;
const plaintext = await window.crypto.subtle.decrypt(

115
package-lock.json generated
View File

@ -485,8 +485,7 @@
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
"dev": true
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
},
"asynckit": {
"version": "0.4.0",
@ -2613,8 +2612,7 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cosmiconfig": {
"version": "4.0.0",
@ -3628,7 +3626,6 @@
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz",
"integrity": "sha512-JzYSLYMhoVVBe8+mbHQ4KgpvHpm0DZpJuL8PY93Vyv1fW7jYJ90LoXa1di/CVbJM+TgMs91rbDapE/RNIfnJsA==",
"dev": true,
"requires": {
"end-of-stream": "1.4.1",
"inherits": "2.0.3",
@ -3640,7 +3637,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
@ -3655,7 +3651,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
@ -3719,7 +3714,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
"dev": true,
"requires": {
"once": "1.4.0"
}
@ -4373,6 +4367,24 @@
}
}
},
"express-ws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz",
"integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==",
"requires": {
"ws": "5.2.0"
},
"dependencies": {
"ws": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.0.tgz",
"integrity": "sha512-c18dMeW+PEQdDFzkhDsnBAlS4Z8KGStBQQUcQ5mf7Nf689jyGk0594L+i9RaQuf4gog6SvWLJorz2NfSaqxZ7w==",
"requires": {
"async-limiter": "1.0.0"
}
}
}
},
"extend": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@ -12122,7 +12134,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1.0.2"
}
@ -15780,8 +15791,7 @@
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
},
"progress": {
"version": "2.0.0",
@ -15903,6 +15913,19 @@
"proxy-from-env": "1.0.0",
"rimraf": "2.6.2",
"ws": "3.3.3"
},
"dependencies": {
"ws": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"dev": true,
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1",
"ultron": "1.1.1"
}
}
}
},
"q": {
@ -17664,8 +17687,7 @@
"stream-shift": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
"dev": true
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI="
},
"stream-to-observable": {
"version": "0.2.0",
@ -18722,8 +18744,7 @@
"ultron": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
"dev": true
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
},
"unassert": {
"version": "1.5.1",
@ -19138,8 +19159,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"util.promisify": {
"version": "1.0.0",
@ -19815,6 +19835,53 @@
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
"dev": true
},
"websocket-stream": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.1.2.tgz",
"integrity": "sha512-lchLOk435iDWs0jNuL+hiU14i3ERSrMA0IKSiJh7z6X/i4XNsutBZrtqu2CPOZuA4G/zabiqVAos0vW+S7GEVw==",
"requires": {
"duplexify": "3.5.4",
"inherits": "2.0.3",
"readable-stream": "2.3.6",
"safe-buffer": "5.1.1",
"ws": "3.3.3",
"xtend": "4.0.1"
},
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.1.1",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "5.1.1"
}
},
"ws": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1",
"ultron": "1.1.1"
}
}
}
},
"whatwg-encoding": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz",
@ -19903,8 +19970,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"wreck": {
"version": "12.5.1",
@ -19943,14 +20009,11 @@
}
},
"ws": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"dev": true,
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.0.tgz",
"integrity": "sha512-c18dMeW+PEQdDFzkhDsnBAlS4Z8KGStBQQUcQ5mf7Nf689jyGk0594L+i9RaQuf4gog6SvWLJorz2NfSaqxZ7w==",
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1",
"ultron": "1.1.1"
"async-limiter": "1.0.0"
}
},
"x-is-function": {

View File

@ -124,13 +124,16 @@
"cldr-core": "^32.0.0",
"convict": "^4.0.1",
"express": "^4.16.2",
"express-ws": "^4.0.0",
"fluent": "^0.6.3",
"fluent-langneg": "^0.1.0",
"helmet": "^3.12.0",
"mkdirp": "^0.5.1",
"mozlog": "^2.2.0",
"raven": "^2.4.2",
"redis": "^2.8.0"
"redis": "^2.8.0",
"websocket-stream": "^5.1.2",
"ws": "^5.2.0"
},
"availableLanguages": [
"en-US",

View File

@ -3,6 +3,14 @@ const locales = require('../common/locales');
const routes = require('./routes');
const pages = require('./routes/pages');
const tests = require('../test/frontend/routes');
const express = require('express');
const expressWs = require('express-ws');
const config = require('./config');
const wsapp = express();
expressWs(wsapp, null, { perMessageDeflate: false });
wsapp.ws('/api/ws', require('./routes/ws'));
wsapp.listen(8081, config.listen_address);
module.exports = function(app, devServer) {
assets.setMiddleware(devServer.middleware);

View File

@ -4,13 +4,15 @@ const Raven = require('raven');
const config = require('./config');
const routes = require('./routes');
const pages = require('./routes/pages');
const expressWs = require('express-ws');
if (config.sentry_dsn) {
Raven.config(config.sentry_dsn).install();
}
const app = express();
expressWs(app, null, { perMessageDeflate: false });
app.ws('/api/ws', require('./routes/ws')); //want to move this into routes/index.js but it's not working...
routes(app);
app.use(

View File

@ -62,6 +62,10 @@ module.exports = function(app) {
app.post(`/api/params/:id${ID_REGEX}`, owner, require('./params'));
app.post(`/api/info/:id${ID_REGEX}`, owner, require('./info'));
if (!IS_DEV) {
app.ws('/api/ws', require('./ws'));
}
app.get('/__version__', function(req, res) {
res.sendFile(require.resolve('../../dist/version.json'));
});

68
server/routes/ws.js Normal file
View File

@ -0,0 +1,68 @@
const crypto = require('crypto');
const storage = require('../storage');
const config = require('../config');
const mozlog = require('../log');
const Limiter = require('../limiter');
const wsStream = require('websocket-stream/stream');
const log = mozlog('send.upload');
module.exports = async function(ws, req) {
let fileStream;
try {
ws.on('close', e => {
if (e !== 1000) {
if (fileStream !== undefined) {
fileStream.destroy();
}
}
});
let first = true;
ws.on('message', function(message) {
if (first) {
const newId = crypto.randomBytes(5).toString('hex');
const owner = crypto.randomBytes(10).toString('hex');
const fileInfo = JSON.parse(message);
const metadata = fileInfo.fileMetadata;
const auth = fileInfo.authorization;
/*
if (!metadata || !auth) {
return res.sendStatus(400);
}
*/
const meta = {
owner,
metadata,
auth: auth.split(' ')[1],
nonce: crypto.randomBytes(16).toString('base64')
};
const limiter = new Limiter(config.max_file_size);
fileStream = wsStream(ws, { binary: true }).pipe(limiter);
storage.set(newId, fileStream, meta);
const protocol = config.env === 'production' ? 'https' : req.protocol;
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
ws.send(
JSON.stringify({
url,
owner: meta.owner,
id: newId,
authentication: `send-v1 ${meta.nonce}`
})
);
first = false;
}
});
} catch (e) {
log.error('upload', e);
//res.sendStatus(500);
}
};

View File

@ -3,21 +3,29 @@ import * as api from '../../../app/api';
import Keychain from '../../../app/keychain';
const encoder = new TextEncoder();
const plaintext = encoder.encode('hello world!');
const plaintext = new Blob([encoder.encode('hello world!')]);
const metadata = {
name: 'test.txt',
type: 'text/plain'
};
describe('API', function() {
describe('uploadFile', function() {
describe('websocket upload', function() {
it('returns file info on success', async function() {
const keychain = new Keychain();
const encrypted = await keychain.encryptFile(plaintext);
const enc = await keychain.encryptStream(plaintext);
const meta = await keychain.encryptMetadata(metadata);
const verifierB64 = await keychain.authKeyB64();
const p = function() {};
const up = api.uploadFile(encrypted, meta, verifierB64, keychain, p);
const up = await api.uploadWs(
enc.stream,
enc.streamInfo,
meta,
verifierB64,
keychain,
p
);
const result = await up.result;
assert.ok(result.url);
assert.ok(result.id);
@ -26,11 +34,18 @@ describe('API', function() {
it('can be cancelled', async function() {
const keychain = new Keychain();
const encrypted = await keychain.encryptFile(plaintext);
const enc = await keychain.encryptStream(plaintext);
const meta = await keychain.encryptMetadata(metadata);
const verifierB64 = await keychain.authKeyB64();
const p = function() {};
const up = api.uploadFile(encrypted, meta, verifierB64, keychain, p);
const up = await api.uploadWs(
enc.stream,
enc.streamInfo,
meta,
verifierB64,
keychain,
p
);
up.cancel();
try {
await up.result;

View File

@ -1,13 +1,10 @@
const streams = require('web-streams-polyfill');
const ece = require('http_ece');
require('buffer');
import assert from 'assert';
import { b64ToArray } from '../../../app/utils';
import ECETransformer from '../../../app/ece.js';
import BlobSliceStream from '../../../app/blobslicer.js';
import ECE from '../../../app/ece.js';
const decoder = new TextDecoder('utf-8');
const rs = 36;
const str = 'You are the dancing queen, young and sweet, only seventeen.';
@ -33,28 +30,10 @@ describe('Streaming', function() {
const salt = b64ToArray(testSalt).buffer;
const blob = new Blob([str], { type: 'text/plain' });
it('blob slice stream works', async function() {
const rs = await new BlobSliceStream(blob, 100);
const reader = rs.getReader();
let result = '';
let state = await reader.read();
while (!state.done) {
result = decoder.decode(state.value);
state = await reader.read();
}
assert.equal(result, str);
});
it('can encrypt', async function() {
const enc = new streams.TransformStream(
new ECETransformer('encrypt', key, rs, salt)
);
const encStream = new ECE(blob, key, 'encrypt', rs, salt).stream;
const reader = encStream.getReader();
const rstream = await new BlobSliceStream(blob, rs - 17);
const reader = rstream.pipeThrough(enc).getReader();
let result = Buffer.from([]);
let state = await reader.read();
@ -68,12 +47,9 @@ describe('Streaming', function() {
it('can decrypt', async function() {
const encBlob = new Blob([encrypted]);
const dec = new streams.TransformStream(
new ECETransformer('decrypt', key, rs)
);
const decStream = await new ECE(encBlob, key, 'decrypt', rs).stream;
const rstream = await new BlobSliceStream(encBlob, rs, true);
const reader = rstream.pipeThrough(dec).getReader();
const reader = decStream.getReader();
let result = Buffer.from([]);
let state = await reader.read();

View File

@ -93,7 +93,7 @@ describe('Upload / Download flow', function() {
fs.cancel(); // before encrypting
try {
await up;
assert.fail('not cancelled');
assert.fail('not cancelled 1');
} catch (e) {
assert.equal(e.message, '0');
}
@ -101,7 +101,7 @@ describe('Upload / Download flow', function() {
fs.once('encrypting', () => fs.cancel());
try {
await fs.upload();
assert.fail('not cancelled');
assert.fail('not cancelled 2');
} catch (e) {
assert.equal(e.message, '0');
}
@ -109,7 +109,7 @@ describe('Upload / Download flow', function() {
fs.once('progress', () => fs.cancel());
try {
await fs.upload();
assert.fail('not cancelled');
assert.fail('not cancelled 3');
} catch (e) {
assert.equal(e.message, '0');
}

View File

@ -209,6 +209,13 @@ module.exports = {
devServer: {
compress: true,
host: '0.0.0.0',
before: IS_DEV ? require('./server/dev') : undefined
before: IS_DEV ? require('./server/dev') : undefined,
proxy: {
'/api/ws': {
target: 'ws://localhost:8081',
ws: true,
secure: false
}
}
}
};