|
| 1 | + |
| 2 | +# Flow.js with end to end encryption (E2EE) |
| 3 | + |
| 4 | +Warning: Crypto is complex, you have to clearly understand what you're doing. Risk is to create fake security sensation for users. |
| 5 | +Quote from [mdn](https://developer.mozilla.org/fr/docs/Web/API/SubtleCrypto): |
| 6 | +"If you're not sure you know what you are doing, you probably shouldn't be using this API." |
| 7 | + |
| 8 | +Warning 2: This code will only works with `flow.js` versions `3.x`. |
| 9 | + |
| 10 | +End to end encryption means you encrypt (and decrypt) data on client side and server side has no idea what data are about. |
| 11 | + |
| 12 | +## There are multiple ways to encrypt files before send them to server: |
| 13 | + |
| 14 | +- **Load and encrypt full file and, then, give this big blob to `flow.js` which will takes care of spliting and sending it as usual.** |
| 15 | + |
| 16 | +The big downside of this approach is that, at one time, you will have full plaintext file AND full cyphertext file in browser memory which is critical if you want to allow users to send big files on multiple devices (each device/os has his own memory managment policy). |
| 17 | + |
| 18 | +- **Add plaintext file to `flow.js` and, then, load & encrypt file chunks on the fly just before sending POST server request.** |
| 19 | + |
| 20 | +Here is an example: |
| 21 | + |
| 22 | +```js |
| 23 | +const flow = new Flow({ |
| 24 | + testChunks: false, |
| 25 | + target: '/upload', |
| 26 | + chunkSize: 10 * 1024 * 1024, |
| 27 | + allowDuplicateUploads: true, |
| 28 | + forceChunkSize: false, |
| 29 | + simultaneousUploads: 4, |
| 30 | + uploadMethod: 'POST', |
| 31 | + fileParameterName: 'file', |
| 32 | + // Asynchronous function called before each chunk upload request |
| 33 | + asyncReadFileFn: async function(flowObj, startByte, endByte, fileType, chunk) { |
| 34 | + // Load file chunk in memory |
| 35 | + const plaintextbytes = await readFileChunk(flowObj.file, startByte, endByte); |
| 36 | + // Encrypt chunk |
| 37 | + const cypherbytes = await encryptFileChunk(plaintextbytes, window.ivbytes, window.key); |
| 38 | + |
| 39 | + // Update chunk size to match encrypted chunk [Add 16 bytes from initialization vector] |
| 40 | + chunk.chunkSize = chunk.chunkSize + 16; |
| 41 | + |
| 42 | + // Return new blob ready to send |
| 43 | + const blob = new Blob([cypherbytes], {type: 'application/octet-stream'}); |
| 44 | + return blob; |
| 45 | + } |
| 46 | +}); |
| 47 | + |
| 48 | +flow.on('fileAdded', file => { |
| 49 | + // Update file size to match encrypted file [Add 16 bytes from initialization vector for each encrypted chunk] |
| 50 | + file.size += file.chunks.length * 16; |
| 51 | +}); |
| 52 | + |
| 53 | + |
| 54 | +// Add an HTML5 File object to the list of files. |
| 55 | +// The library will takes care about splitting in chunks |
| 56 | +flow.addFile(file); |
| 57 | + |
| 58 | +function readFileChunk(file, startByte, endByte) { |
| 59 | + return new Promise(resolve => { |
| 60 | + const reader = new FileReader(); |
| 61 | + reader.onload = () => { |
| 62 | + const bytes = new Uint8Array(reader.result); |
| 63 | + resolve(bytes); |
| 64 | + }; |
| 65 | + const blob = file.slice(startByte, endByte); |
| 66 | + reader.readAsArrayBuffer(blob); |
| 67 | + }); |
| 68 | +} |
| 69 | + |
| 70 | +async function encryptFileChunk(plaintextbytes, iv, key) { |
| 71 | + let cypherchunkbytes = await window.crypto.subtle.encrypt({name: 'AES-GCM', iv}, key, plaintextbytes); |
| 72 | + |
| 73 | + if(cypherchunkbytes) { |
| 74 | + cypherchunkbytes = new Uint8Array(cypherchunkbytes); |
| 75 | + return cypherchunkbytes; |
| 76 | + } |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +- **Encrypt the file as a stream using an asymmetric StreamEncryptor and [openpgpjs](https://openpgpjs.org/).** |
| 81 | + |
| 82 | +Here is an example: |
| 83 | + |
| 84 | +```js |
| 85 | +class StreamEncryptor { |
| 86 | + constructor(gpgKeys) { |
| 87 | + this.gpgKeys = gpgKeys; |
| 88 | + this._reader = []; |
| 89 | + } |
| 90 | + |
| 91 | + async init(flowObj) { |
| 92 | + const { message } = await openpgp.encrypt({ |
| 93 | + message: openpgp.message.fromBinary(flowObj.file.stream(), flowObj.file.name), |
| 94 | + publicKeys: this.gpgKeys |
| 95 | + }); |
| 96 | + |
| 97 | + this._reader[flowObj.uniqueIdentifier] = openpgp.stream.getReader(message.packets.write()); |
| 98 | + flowObj.size = flowObj.file.size + compute_pgp_overhead(this.gpgKeys, flowObj.file.name); |
| 99 | + } |
| 100 | + |
| 101 | + async read(flowObj, startByte, endByte, fileType, chunk) { |
| 102 | + const buffer = await this._reader[flowObj.uniqueIdentifier].readBytes(flowObj.chunkSize); |
| 103 | + if (buffer && buffer.length) { |
| 104 | + return new Blob([buffer], {type: 'application/octet-stream'}); |
| 105 | + } |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +var encryptor = new StreamEncryptor(gpgKeys); |
| 110 | +new Flow({ |
| 111 | + // ... |
| 112 | + asyncReadFileFn: encryptor.read.bind(encryptor), |
| 113 | + initFileFn: encryptor.init.bind(encryptor), |
| 114 | + forceChunkSize: true, |
| 115 | +}); |
| 116 | +``` |
| 117 | + |
0 commit comments