str4d / wage

A WASM package and web app for encrypting and decrypting age-encrypted files, powered by rage.
https://rage-encry.pt
Apache License 2.0
77 stars 10 forks source link

webapp: Encryption fails in Chrome #29

Open str4d opened 3 years ago

str4d commented 3 years ago

Environment

What were you trying to do

I generated a new identity, and then tried to encrypt a file to it.

What happened

App.vue:250 Uncaught (in promise) TypeError: Failed to execute 'pipeTo' on 'ReadableStream': parameter 1 is not of type 'WritableStream'.
    at o.encryptSingleFile (App.vue:250)
    at App.vue:231
  encryptSingleFile @ App.vue:250
  (anonymous) @ App.vue:231
  Promise.then (async)    
  encryptSingleFile @ App.vue:250
  (anonymous) @ App.vue:231
  Promise.then (async)    
  (anonymous) @ App.vue:229
  prepareEncryptStream @ App.vue:221
  encryptToRecipients @ App.vue:225
fyears commented 3 years ago

Hi. Same issue here in passphrase mode in Chrome. But (strangely) the page works in Firefox...

str4d commented 2 years ago

I am now seeing the same exception in Firefox 107 on macOS 13.0.1 with 5de7b79330a142a4fffa42b8631f96e34a984946:

Uncaught (in promise) TypeError: ReadableStream.pipeTo: Argument 1 does not implement interface WritableStream.
    encryptSingleFile App.vue:250
    encryptWithPassphrase App.vue:241
    promise callback*encryptWithPassphrase/< App.vue:239
    prepareEncryptStream App.vue:221
    encryptWithPassphrase App.vue:236
    VueJS 4
    encryptFile EncryptPane.vue:232
    VueJS 33
MattiasBuelens commented 2 years ago

I think the problem is that you're using native streams and polyfilled streams interchangeably. And unfortunately, web-streams-polyfill doesn't support this... 😞

Consider the following snippet: https://github.com/str4d/wage/blob/1a647173026fece59d2985f2547befc93f3e5ffc/www/src/App.vue#L245-L251

this.dropFiles[0] is a native File, so this.dropFiles[0].stream() is a native ReadableStream. However, by loading the polyfill, window.WritableStream is replaced by the polyfill's version, so the constructed this.downloadStream is a polyfilled WritableStream. The native ReadableStream.pipeTo() only works with native WritableStream destinations, so that's why that call throws.

One solution would be to manually convert all your native streams to polyfilled ones before usage. I made a small library to help with that. You add a bit of initialization code:

import { createReadableStreamWrapper, createWritableStreamWrapper } from '@mattiasbuelens/web-streams-adapter';
const toPolyfillReadable = createReadableStreamWrapper(window.ReadableStream);
const toPolyfillWritable = createWritableStreamWrapper(window.WritableStream);

and then you convert where necessary:

encryptSingleFile() {
  let fileStream = toPolyfillReadable(this.dropFiles[0].stream());
  let downloadStream = toPolyfillWritable(this.downloadStream);
  return fileStream.pipeTo(downloadStream).then(this.reset);
},
str4d commented 2 years ago

Aha, that makes a lot of sense in hindsight! I was definitely assuming that the polyfill was handling this for me (or that the interface was being duck-typed). It might be useful to mention the wrapper library in the polyfill library documentation (if it's not already there; I don't recall seeing it when I implemented this). I'll try this and see if it resolves the issue.

MattiasBuelens commented 2 years ago

Aha, that makes a lot of sense in hindsight! I was definitely assuming that the polyfill was handling this for me (or that the interface was being duck-typed).

I was trying to make that work for version 4.0 of the polyfill, but unfortunately I had to throw in the towel (see https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issue-1442802866). 😞

It might be useful to mention the wrapper library in the polyfill library documentation (if it's not already there; I don't recall seeing it when I implemented this).

Yes, the documentation needs improvement. 😅 I'm also considering adding these conversion methods to web-streams-polyfill itself, because they're so commonly needed.