Browse Source

Implemented multi-file upload/download

Danny Coates 6 years ago
parent
commit
7bf104960e
10 changed files with 104 additions and 123 deletions
  1. 8 20
      app/api.js
  2. 33 0
      app/archive.js
  3. 0 27
      app/blobSlicer.js
  4. 4 6
      app/dragManager.js
  5. 25 44
      app/ece.js
  6. 2 0
      app/fileReceiver.js
  7. 7 12
      app/fileSender.js
  8. 6 10
      app/keychain.js
  9. 4 1
      app/pages/welcome/index.js
  10. 15 3
      app/serviceWorker.js

+ 8 - 20
app/api.js

@@ -1,4 +1,5 @@
 import { arrayToB64, b64ToArray, delay } from './utils';
+import { ECE_RECORD_SIZE } from './ece';
 
 function post(obj) {
   return {
@@ -78,7 +79,8 @@ export async function metadata(id, keychain) {
       ttl: data.ttl,
       iv: meta.iv,
       name: meta.name,
-      type: meta.type
+      type: meta.type,
+      manifest: meta.manifest
     };
   }
   throw new Error(result.response.status);
@@ -126,14 +128,7 @@ function listenForResponse(ws, canceller) {
   });
 }
 
-async function upload(
-  stream,
-  streamInfo,
-  metadata,
-  verifierB64,
-  onprogress,
-  canceller
-) {
+async function upload(stream, metadata, verifierB64, onprogress, canceller) {
   const host = window.location.hostname;
   const port = window.location.port;
   const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -166,10 +161,10 @@ async function upload(
 
       ws.send(buf);
 
-      onprogress([size, streamInfo.fileSize]);
+      onprogress(size);
       size += buf.length;
       state = await reader.read();
-      while (ws.bufferedAmount > streamInfo.recordSize * 2) {
+      while (ws.bufferedAmount > ECE_RECORD_SIZE * 2) {
         await delay();
       }
     }
@@ -185,7 +180,7 @@ async function upload(
   }
 }
 
-export function uploadWs(encrypted, info, metadata, verifierB64, onprogress) {
+export function uploadWs(encrypted, metadata, verifierB64, onprogress) {
   const canceller = { cancelled: false };
 
   return {
@@ -193,14 +188,7 @@ export function uploadWs(encrypted, info, metadata, verifierB64, onprogress) {
       canceller.error = new Error(0);
       canceller.cancelled = true;
     },
-    result: upload(
-      encrypted,
-      info,
-      metadata,
-      verifierB64,
-      onprogress,
-      canceller
-    )
+    result: upload(encrypted, metadata, verifierB64, onprogress, canceller)
   };
 }
 

+ 33 - 0
app/archive.js

@@ -0,0 +1,33 @@
+import { blobStream, concatStream } from './streams';
+
+export default class Archive {
+  constructor(files) {
+    this.files = Array.from(files);
+  }
+
+  get name() {
+    return this.files.length > 1 ? 'Send-Archive.zip' : this.files[0].name;
+  }
+
+  get type() {
+    return this.files.length > 1 ? 'send-archive' : this.files[0].type;
+  }
+
+  get size() {
+    return this.files.reduce((total, file) => total + file.size, 0);
+  }
+
+  get manifest() {
+    return {
+      files: this.files.map(file => ({
+        name: file.name,
+        size: file.size,
+        type: file.type
+      }))
+    };
+  }
+
+  get stream() {
+    return concatStream(this.files.map(file => blobStream(file)));
+  }
+}

+ 0 - 27
app/blobSlicer.js

@@ -1,27 +0,0 @@
-export default class BlobSlicer {
-  constructor(blob, size) {
-    this.blob = blob;
-    this.index = 0;
-    this.chunkSize = size;
-  }
-
-  pull(controller) {
-    return new Promise((resolve, reject) => {
-      const bytesLeft = this.blob.size - this.index;
-      if (bytesLeft <= 0) {
-        controller.close();
-        return resolve();
-      }
-      const 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;
-    });
-  }
-}

+ 4 - 6
app/dragManager.js

@@ -1,5 +1,6 @@
 /* global MAXFILESIZE */
-const { bytes } = require('./utils');
+import Archive from './archive';
+import { bytes } from './utils';
 
 export default function(state, emitter) {
   emitter.on('DOMContentLoaded', () => {
@@ -18,11 +19,8 @@ export default function(state, emitter) {
         if (target.files.length === 0) {
           return;
         }
-        if (target.files.length > 1) {
-          // eslint-disable-next-line no-alert
-          return alert(state.translate('uploadPageMultipleFilesAlert'));
-        }
-        const file = target.files[0];
+        const file = new Archive(target.files);
+
         if (file.size === 0) {
           return;
         }

+ 25 - 44
app/ece.js

@@ -1,5 +1,4 @@
 import 'buffer';
-import BlobSlicer from './blobSlicer';
 import { transformStream } from './streams';
 
 const NONCE_LENGTH = 12;
@@ -7,7 +6,7 @@ const TAG_LENGTH = 16;
 const KEY_LENGTH = 16;
 const MODE_ENCRYPT = 'encrypt';
 const MODE_DECRYPT = 'decrypt';
-const RS = 1024 * 64;
+export const ECE_RECORD_SIZE = 1024 * 64;
 
 const encoder = new TextEncoder();
 
@@ -282,52 +281,34 @@ class StreamSlicer {
   }
 }
 
+export function encryptedSize(size, rs = ECE_RECORD_SIZE) {
+  return 21 + size + 16 * Math.floor(size / (rs - 17));
+}
+
 /*
-input: a blob or a ReadableStream containing data to be transformed
+input: a ReadableStream 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) {
-    this.input = input;
-    this.key = key;
-    this.mode = mode;
-    this.rs = rs;
-    this.salt = salt;
-    if (rs === undefined) {
-      this.rs = RS;
-    }
-    if (salt === undefined) {
-      this.salt = generateSalt(KEY_LENGTH);
-    }
-  }
-
-  info() {
-    return {
-      recordSize: this.rs,
-      fileSize:
-        21 + this.input.size + 16 * Math.floor(this.input.size / (this.rs - 17))
-    };
-  }
-
-  transform() {
-    let inputStream;
+export function encryptStream(
+  input,
+  key,
+  rs = ECE_RECORD_SIZE,
+  salt = generateSalt(KEY_LENGTH)
+) {
+  const mode = 'encrypt';
+  const inputStream = transformStream(input, new StreamSlicer(rs, mode));
+  return transformStream(inputStream, new ECETransformer(mode, key, rs, salt));
+}
 
-    if (this.input instanceof Blob) {
-      inputStream = new ReadableStream(
-        new BlobSlicer(this.input, this.rs - 17)
-      );
-    } else {
-      inputStream = transformStream(
-        this.input,
-        new StreamSlicer(this.rs, this.mode)
-      );
-    }
-    return transformStream(
-      inputStream,
-      new ECETransformer(this.mode, this.key, this.rs, this.salt)
-    );
-  }
+/*
+input: a ReadableStream containing data to be transformed
+key:  Uint8Array containing key of size KEY_LENGTH
+rs:   int containing record size, optional
+*/
+export function decryptStream(input, key, rs = ECE_RECORD_SIZE) {
+  const mode = 'decrypt';
+  const inputStream = transformStream(input, new StreamSlicer(rs, mode));
+  return transformStream(inputStream, new ECETransformer(mode, key, rs));
 }

+ 2 - 0
app/fileReceiver.js

@@ -48,6 +48,7 @@ export default class FileReceiver extends Nanobus {
     this.fileInfo.type = meta.type;
     this.fileInfo.iv = meta.iv;
     this.fileInfo.size = meta.size;
+    this.fileInfo.manifest = meta.manifest;
     this.state = 'ready';
   }
 
@@ -105,6 +106,7 @@ export default class FileReceiver extends Nanobus {
         id: this.fileInfo.id,
         filename: this.fileInfo.name,
         type: this.fileInfo.type,
+        manifest: this.fileInfo.manifest,
         key: this.fileInfo.secretKey,
         requiresPassword: this.fileInfo.requiresPassword,
         password: this.fileInfo.password,

+ 7 - 12
app/fileSender.js

@@ -4,6 +4,7 @@ import OwnedFile from './ownedFile';
 import Keychain from './keychain';
 import { arrayToB64, bytes } from './utils';
 import { uploadWs } from './api';
+import { encryptedSize } from './ece';
 
 export default class FileSender extends Nanobus {
   constructor(file) {
@@ -64,21 +65,15 @@ export default class FileSender extends Nanobus {
     }
     this.msg = 'encryptingFile';
     this.emit('encrypting');
-
-    const enc = await this.keychain.encryptStream(this.file);
+    const totalSize = encryptedSize(this.file.size);
+    const encStream = await this.keychain.encryptStream(this.file.stream);
     const metadata = await this.keychain.encryptMetadata(this.file);
     const authKeyB64 = await this.keychain.authKeyB64();
 
-    this.uploadRequest = uploadWs(
-      enc.stream,
-      enc.streamInfo,
-      metadata,
-      authKeyB64,
-      p => {
-        this.progress = p;
-        this.emit('progress');
-      }
-    );
+    this.uploadRequest = uploadWs(encStream, metadata, authKeyB64, p => {
+      this.progress = [p, totalSize];
+      this.emit('progress');
+    });
 
     if (this.cancelled) {
       throw new Error(0);

+ 6 - 10
app/keychain.js

@@ -1,5 +1,5 @@
 import { arrayToB64, b64ToArray } from './utils';
-import ECE from './ece.js';
+import { decryptStream, encryptStream } from './ece.js';
 const encoder = new TextEncoder();
 const decoder = new TextDecoder();
 
@@ -173,24 +173,20 @@ export default class Keychain {
           iv: arrayToB64(this.iv),
           name: metadata.name,
           size: metadata.size,
-          type: metadata.type || 'application/octet-stream'
+          type: metadata.type || 'application/octet-stream',
+          manifest: metadata.manifest || {}
         })
       )
     );
     return ciphertext;
   }
 
-  encryptStream(plaintext) {
-    const ece = new ECE(plaintext, this.rawSecret, 'encrypt');
-    return {
-      stream: ece.transform(),
-      streamInfo: ece.info()
-    };
+  encryptStream(plainStream) {
+    return encryptStream(plainStream, this.rawSecret);
   }
 
   decryptStream(cryptotext) {
-    const ece = new ECE(cryptotext, this.rawSecret, 'decrypt');
-    return ece.transform();
+    return decryptStream(cryptotext, this.rawSecret);
   }
 
   async decryptFile(ciphertext) {

+ 4 - 1
app/pages/welcome/index.js

@@ -33,6 +33,7 @@ module.exports = function(state, emit) {
       <input id="file-upload"
         class="inputFile"
         type="file"
+        multiple
         name="fileUploaded"
         onfocus=${onfocus}
         onblur=${onblur}
@@ -67,8 +68,10 @@ module.exports = function(state, emit) {
 
   async function upload(event) {
     event.preventDefault();
+    const Archive = require('../../archive').default;
     const target = event.target;
-    const file = target.files[0];
+    const file = new Archive(target.files);
+
     if (file.size === 0) {
       return;
     }

+ 15 - 3
app/serviceWorker.js

@@ -1,6 +1,7 @@
 import Keychain from './keychain';
 import { downloadStream } from './api';
 import { transformStream } from './streams';
+import Zip from './zip';
 import contentDisposition from 'content-disposition';
 
 let noSave = false;
@@ -20,6 +21,8 @@ async function decryptStream(id) {
     return new Response(null, { status: 400 });
   }
   try {
+    let size = file.size;
+    let type = file.type;
     const keychain = new Keychain(file.key, file.nonce);
     if (file.requiresPassword) {
       keychain.setPassword(file.password, file.url);
@@ -30,8 +33,16 @@ async function decryptStream(id) {
     const body = await file.download.result;
 
     const decrypted = keychain.decryptStream(body);
+
+    let zipStream = null;
+    if (file.type === 'send-archive') {
+      const zip = new Zip(file.manifest, decrypted);
+      zipStream = zip.stream;
+      type = 'application/zip';
+      size = zip.size;
+    }
     const readStream = transformStream(
-      decrypted,
+      zipStream || decrypted,
       {
         transform(chunk, controller) {
           file.progress += chunk.length;
@@ -48,8 +59,8 @@ async function decryptStream(id) {
 
     const headers = {
       'Content-Disposition': contentDisposition(file.filename),
-      'Content-Type': file.type,
-      'Content-Length': file.size
+      'Content-Type': type,
+      'Content-Length': size
     };
     return new Response(readStream, { headers });
   } catch (e) {
@@ -81,6 +92,7 @@ self.onmessage = event => {
       password: event.data.password,
       url: event.data.url,
       type: event.data.type,
+      manifest: event.data.manifest,
       size: event.data.size,
       progress: 0
     };

Some files were not shown because too many files changed in this diff