feat: add WebCrypto polyfill for browsers that don't have it in WebWorkers

This commit is contained in:
thecodrr
2020-09-18 15:39:21 +05:00
parent 04fa3634a8
commit d18d58b668
2 changed files with 104 additions and 43 deletions

View File

@@ -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;
};

View File

@@ -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
);
});
}