Firefox上でOpenPGP.jsのReadableStreamの復号をするとエラー「This message / key probably does not conform to a valid...」する問題のPiping UIでの解決方法
#OpenPGP.js #Firefox #ReadableStream #復号 #セキュリティ
やりたいこと
FirefoxだとOpenPGP.jsを使ってReadableStreamを復号すると以下のエラーがでる。
code:エラーメッセージ
Error: Error during parsing. This message / key probably does not conform to a valid OpenPGP format.
これを対処する方法。
暗号化でも同様に起こる現象のはずで同じ方法で解決できるはず。
タイトルにPiping UIが含まれているのはService Worker内でOpenPGP.jsを使って「ファイルのストリーミング強制保存をクロスオリジンでも実現させるService Workerの裏技ぽい使い方」を使ってダウンロードさせる手法も合わせて使うのでタイトルにも含めた。
利用するFirefoxのバージョン
72.0.2 (64-bit)
エラーが起こる原因
Firefoxだと完全にReadableStreamをサポートできていなくOpenPGP.jsを使うとpolyfillされる。
OpenPGP.jsだとReadableStreamにpolyfillが必要なときはopenpgp.decrypt({message: myNativeReadableStream, ...})のようにmyNativeReadableStreamがnativeの(polyfill前の)ReadableStreamが来ると上記のエラーがでる。
nativeのReadableStreamが来る例と言えばawait fetch()をしたときなど。
例えば以下のCodepenがFirefoxでエラーを起こせる例になる。Google Chromeだと正常に復号できる。
今後のFirefoxのバージョンアップしたときの上記を試してうまくいくようになることも確認できるはず。
詳しくは「Native ReadableStream can not be decrypted on Firefox · Issue #1037 · openpgpjs/openpgpjs」。
対処方法
openpgp.decrypt()はpolyfillなReadableStreamなら復号できる。(openpgp.encrypt()も同様のはず)
そのためnative ReadableStreamと polyfillなReadableStreamを変換する。
これは「Native ReadableStream can not be decrypted on Firefox · Issue #1037 · openpgpjs/openpgpjs」で@twiss さんに教えてもらった。
を使ってその変換ができる。以下でインストールできる。
$ npm i -S @mattiasbuelens/web-streams-adapter
web-streams-adapterの使い方
使い方として、以下のようにcreateReadableStreamWrapper()の引数にどちらかにReadableStreamを与えるとそれに変換するための関数が返ってくる。
code:js
// 変換するための関数が返ってくる。
const toPolyfillReadable = createReadableStreamWrapper( native の ReadableStream );
const toNativeReadable = createReadableStreamWrapper( polyfill の ReadableStream );
web-streams-adapterを使う
Service Workerで使っているので以下のようにimportScripts()している。
importScripts('openpgp/openpgp.min.js')するとpolyfillされるので前にconst NativeReadableStream = ReadableStream;のように退避させている。
importScripts()後でも元々のReadableStreamにアクセスする手法が調べれば見つかるかもしれないが一番確実だしとりあえずこの方法をとる。
code:sw.js
const NativeReadableStream = ReadableStream;
importScripts('openpgp/openpgp.min.js');
importScripts('web-streams-adapter/web-streams-adapter.js');
...
あと下記の関数を作った。
toPolyfillReadableIfNeed()はpolyfillされていれば、nativeのReadableStreamをpolyfillのReadableStreamに変換する。されていなければ変換せずにそのまま返す。
toNativeReadableIfNeedはpolyfillされていれば、polyfillのReadableStreamをnativeに変換する。されていなければそのまま返す。
import { ReadableStream as PolyfillReadableStream } from 'web-streams-polyfill';を使ってpolyfillのReadableStreamクラスを取得することができてそうすると条件分岐いらなくなりそうだが、Service Workerでimportするためにファイルを/publicにコピーするnpmスクリプト書いたりしてWorkboxに合うようにService Workerのスクリプトをうまく生成する方法を見つけておらずそういうことが背景で以下のような関数を定義している。
code:js
/**
* Convert a native ReadableStream to polyfill ReadableStream if ReadableStream class is polyfill.
*
* @param readableStream Native ReadableStream
* @returns {*}
*/
function toPolyfillReadableIfNeed(readableStream) {
// If not polyfilled
if (NativeReadableStream === ReadableStream) {
return readableStream;
// If ReadableStream is polyfill
} else {
// (base: https://github.com/MattiasBuelens/web-streams-adapter/tree/d76e3789d67b1ab3c91699ecc0c42bde897d2298)
// NOTE: ReadableStream is polyfill ReadableStream in this condition
const toPolyfillReadable = WebStreamsAdapter.createReadableStreamWrapper(ReadableStream);
// Convert a native ReadableStream to polyfill ReadableStream
return toPolyfillReadable(readableStream);
}
}
/**
* Convert a polyfill ReadableStream to native ReadableStream if ReadableStream class is polyfill.
*
* @param readableStream Native ReadableStream or polyfill ReadableStream
* @returns {*}
*/
function toNativeReadableIfNeed(readableStream) {
// If not polyfilled
if (NativeReadableStream === ReadableStream) {
return readableStream;
// If ReadableStream is polyfill
} else {
// (base: https://github.com/MattiasBuelens/web-streams-adapter/tree/d76e3789d67b1ab3c91699ecc0c42bde897d2298)
const toNativeReadable = WebStreamsAdapter.createReadableStreamWrapper(NativeReadableStream);
// Convert a polyfill ReadableStream to native ReadableStream
return toNativeReadable(readableStream);
}
}
そうするとtoPolyfillReadableIfNeedを使って以下のようにres.bodyというnativeのReadableStreamをpolyfillのReadableStreamに変換して復号処理ができる。暗号化も同様のはず。
code:js
const decrypted = await openpgp.decrypt({
message: await openpgp.message.read(toPolyfillReadableIfNeed(res.body)),
passwords: password,
format: 'binary'
});
plainStream = decrypted.data;とすると、これをplainStreamがpolyfillのReadableStreamだったりnativeのReadableStreamだったりすることがある。これをnativeのReadableStreamに変換するために以下のようにする。
code:js
toNativeReadableIfNeed(plainStream)
以下はPiping UIでの実際の変更。
new Response(myReadableStream)のmyReadableStreamがpolyfillのReadableStreamだとレスポンスが[object Object]という文字列になってしまうのでそれを考え実装する必要があった。