diff --git a/apps/web/public/crypto.worker.js b/apps/web/public/crypto.worker.js index eaed640e7..81c5cee87 100644 --- a/apps/web/public/crypto.worker.js +++ b/apps/web/public/crypto.worker.js @@ -1,32 +1,50 @@ /* eslint-disable */ var context; -if (!self instanceof Window) { - self.sodium = { - onload: function (_sodium) { - context = { sodium: _sodium }; - sendMessage("loaded"); - }, - }; - importScripts("sodium.js"); - +if (!self.document) { self.addEventListener("message", onMessage); } function onMessage(ev) { const { type, data, messageId } = ev.data; - if (type === "encrypt") { - const { passwordOrKey, data: _data } = data; - const cipher = encrypt.call(context, passwordOrKey, _data); - sendMessage("encrypt", cipher, messageId); - } else if (type === "decrypt") { - const { passwordOrKey, cipher } = data; - const plainText = decrypt.call(context, passwordOrKey, cipher); - sendMessage("decrypt", plainText, messageId); - } else if (type === "deriveKey") { - const { password, salt, exportKey } = data; - const derivedKey = deriveKey.call(context, password, salt, exportKey); - sendMessage("deriveKey", derivedKey, messageId); + try { + switch (type) { + case "encrypt": { + const { passwordOrKey, data: _data } = data; + const cipher = encrypt.call(context, passwordOrKey, _data); + sendMessage("encrypt", cipher, messageId); + break; + } + case "decrypt": { + const { passwordOrKey, cipher } = data; + const plainText = decrypt.call(context, passwordOrKey, cipher); + sendMessage("decrypt", plainText, messageId); + break; + } + case "deriveKey": { + const { password, salt, exportKey } = data; + const derivedKey = deriveKey.call(context, password, salt, exportKey); + sendMessage("deriveKey", derivedKey, messageId); + break; + } + case "load": { + const { seed } = data; + self.sodium = { + onload: function (_sodium) { + context = { sodium: _sodium }; + // create the crypto polyfill if necessary + webCryptoPolyfill(seed, _sodium); + sendMessage("load", {}, messageId); + }, + }; + importScripts("sodium.js"); + break; + } + default: + return; + } + } catch (error) { + sendMessage(type, { error }, messageId); } } @@ -128,10 +146,52 @@ const decrypt = (passwordOrKey, { iv, cipher, salt }) => { return plainText; }; -if (self instanceof Window) { +if (self.document) { self.ncrypto = { decrypt, deriveKey, encrypt, }; } + +/** + * If not available natively, uses a separate CSPRNG to polyfill the Web Crypto API. + * Used in worker threads in some browsers. + * @param seed Securely generated 32-byte key. + */ +const webCryptoPolyfill = (seed, sodium) => { + if ("getRandomValues" in crypto) return; + + const nonce = new Uint32Array(2); + crypto = { + getRandomValues: (array) => { + if (!array) { + throw new TypeError( + `Failed to execute 'getRandomValues' on 'Crypto': ${ + array === null + ? "parameter 1 is not of type 'ArrayBufferView'" + : "1 argument required, but only 0 present" + }.` + ); + } + /* Handle circular dependency between this polyfill and libsodium */ + const sodiumExists = typeof sodium.crypto_stream_chacha20 === "function"; + if (!sodiumExists) { + throw new Error("No CSPRNG found."); + } + ++nonce[nonce[0] === 4294967295 ? 1 : 0]; + const newBytes = sodium().crypto_stream_chacha20( + array.byteLength, + seed, + new Uint8Array(nonce.buffer, nonce.byteOffset, nonce.byteLength) + ); + new Uint8Array(array.buffer, array.byteOffset, array.byteLength).set( + newBytes + ); + sodium().memzero(newBytes); + return array; + }, + subtle: {}, + }; + self.crypto = crypto; +}; diff --git a/apps/web/src/interfaces/crypto.js b/apps/web/src/interfaces/crypto.js index 5c5faa827..ecce5ec23 100644 --- a/apps/web/src/interfaces/crypto.js +++ b/apps/web/src/interfaces/crypto.js @@ -50,36 +50,37 @@ class CryptoWorker { } async _initialize() { if (this.isReady) return; - return new Promise((resolve) => { - this.worker = new Worker("crypto.worker.js"); - this.worker.onmessage = (ev) => { - const { type } = ev.data; - if (type === "loaded") { - this.worker.onmessage = undefined; - this.isReady = true; - resolve(true); - } - }; - }); + this.worker = new Worker("crypto.worker.js"); + const buffer = Buffer.allocUnsafe(32); + crypto.getRandomValues(buffer); + const message = { seed: buffer.buffer }; + await this._communicate("load", message, [message.seed], false); + this.isReady = true; } - _communicate(type, data) { - return new Promise(async (resolve) => { - await this._initialize(); + _communicate(type, data, transferables = [], init = true) { + return new Promise(async (resolve, reject) => { + if (init) await this._initialize(); const messageId = Math.random().toString(36).substr(2, 9); const onMessage = (e) => { - const { type: _type, messageId: _mId } = e.data; + const { type: _type, messageId: _mId, data } = e.data; if (_type === type && _mId === messageId) { this.worker.removeEventListener("message", onMessage); - resolve(e.data.data); + if (data.error) { + return reject(data.error); + } + resolve(data); } }; this.worker.addEventListener("message", onMessage); - this.worker.postMessage({ - type, - data, - messageId, - }); + this.worker.postMessage( + { + type, + data, + messageId, + }, + transferables + ); }); }