From f08fc0a4f7b56524c9af5cdd62d79613380c9a0f Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 1 Oct 2014 21:18:05 +0200 Subject: [PATCH] Refactor web worker async proxy to use promises. --- src/openpgp.js | 4 +- src/worker/async_proxy.js | 301 ++++++++++++++++++++++---------------- src/worker/worker.js | 132 ++++++++--------- test/unittests.html | 1 + test/worker/api.js | 99 ++++--------- 5 files changed, 264 insertions(+), 273 deletions(-) diff --git a/src/openpgp.js b/src/openpgp.js index 1921891a..42699a2c 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -239,8 +239,8 @@ function generateKeyPair(options) { /** * Are we in a browser and do we support worker? */ -function useWorker(callback) { - if (typeof window === 'undefined' || !window.Worker || typeof callback !== 'function') { +function useWorker() { + if (typeof window === 'undefined' || !window.Worker) { return false; } diff --git a/src/worker/async_proxy.js b/src/worker/async_proxy.js index 9305849d..63a10e30 100644 --- a/src/worker/async_proxy.js +++ b/src/worker/async_proxy.js @@ -24,11 +24,12 @@ * @module async_proxy */ +'use strict'; + var crypto = require('../crypto'), packet = require('../packet'), key = require('../key.js'), - type_keyid = require('../type/keyid.js'), - enums = require('../enums.js'); + type_keyid = require('../type/keyid.js'); var INITIAL_RANDOM_SEED = 50000, // random bytes seeded to worker RANDOM_SEED_REQUEST = 20000; // random bytes seeded after worker request @@ -49,6 +50,24 @@ function AsyncProxy(path) { this.tasks = []; } +/** + * Command pattern that wraps synchronous code into a promise + * @param {Object} self The current this + * @param {function} cmd The synchronous function with a return value + * to be wrapped in a promise + * @return {Promise} The promise wrapped around cmd + */ +AsyncProxy.prototype.execute = function(cmd) { + var self = this; + + var promise = new Promise(function(resolve, reject) { + cmd(); + self.tasks.push({ resolve:resolve, reject:reject }); + }); + + return promise; +}; + /** * Message handling */ @@ -56,7 +75,13 @@ AsyncProxy.prototype.onMessage = function(event) { var msg = event.data; switch (msg.event) { case 'method-return': - this.tasks.shift()(msg.err ? new Error(msg.err) : null, msg.data); + if (msg.err) { + // fail + this.tasks.shift().reject(new Error(msg.err)); + } else { + // success + this.tasks.shift().resolve(msg.data); + } break; case 'request-seed': this.seedRandom(RANDOM_SEED_REQUEST); @@ -98,21 +123,23 @@ AsyncProxy.prototype.terminate = function() { * Encrypts message text with keys * @param {(Array|module:key~Key)} keys array of keys or single key, used to encrypt the message * @param {String} text message as native JavaScript string - * @param {Function} callback receives encrypted ASCII armored message */ -AsyncProxy.prototype.encryptMessage = function(keys, text, callback) { - if (!keys.length) { - keys = [keys]; - } - keys = keys.map(function(key) { - return key.toPacketlist(); +AsyncProxy.prototype.encryptMessage = function(keys, text) { + var self = this; + + return self.execute(function() { + if (!keys.length) { + keys = [keys]; + } + keys = keys.map(function(key) { + return key.toPacketlist(); + }); + self.worker.postMessage({ + event: 'encrypt-message', + keys: keys, + text: text + }); }); - this.worker.postMessage({ - event: 'encrypt-message', - keys: keys, - text: text - }); - this.tasks.push(callback); }; /** @@ -120,40 +147,43 @@ AsyncProxy.prototype.encryptMessage = function(keys, text, callback) { * @param {(Array|module:key~Key)} publicKeys array of keys or single key, used to encrypt the message * @param {module:key~Key} privateKey private key with decrypted secret key data for signing * @param {String} text message as native JavaScript string - * @param {Function} callback receives encrypted ASCII armored message */ -AsyncProxy.prototype.signAndEncryptMessage = function(publicKeys, privateKey, text, callback) { - if (!publicKeys.length) { - publicKeys = [publicKeys]; - } - publicKeys = publicKeys.map(function(key) { - return key.toPacketlist(); +AsyncProxy.prototype.signAndEncryptMessage = function(publicKeys, privateKey, text) { + var self = this; + + return self.execute(function() { + if (!publicKeys.length) { + publicKeys = [publicKeys]; + } + publicKeys = publicKeys.map(function(key) { + return key.toPacketlist(); + }); + privateKey = privateKey.toPacketlist(); + self.worker.postMessage({ + event: 'sign-and-encrypt-message', + publicKeys: publicKeys, + privateKey: privateKey, + text: text + }); }); - privateKey = privateKey.toPacketlist(); - this.worker.postMessage({ - event: 'sign-and-encrypt-message', - publicKeys: publicKeys, - privateKey: privateKey, - text: text - }); - this.tasks.push(callback); }; /** * Decrypts message * @param {module:key~Key} privateKey private key with decrypted secret key data * @param {module:message~Message} message the message object with the encrypted data - * @param {Function} callback receives decrypted message as as native JavaScript string - * or null if no literal data found */ -AsyncProxy.prototype.decryptMessage = function(privateKey, message, callback) { - privateKey = privateKey.toPacketlist(); - this.worker.postMessage({ - event: 'decrypt-message', - privateKey: privateKey, - message: message +AsyncProxy.prototype.decryptMessage = function(privateKey, message) { + var self = this; + + return self.execute(function() { + privateKey = privateKey.toPacketlist(); + self.worker.postMessage({ + event: 'decrypt-message', + privateKey: privateKey, + message: message + }); }); - this.tasks.push(callback); }; /** @@ -161,82 +191,91 @@ AsyncProxy.prototype.decryptMessage = function(privateKey, message, callback) { * @param {module:key~Key} privateKey private key with decrypted secret key data * @param {(Array|module:key~Key)} publicKeys array of keys or single key to verify signatures * @param {module:message~Message} message the message object with signed and encrypted data - * @param {Function} callback receives decrypted message as as native JavaScript string - * with verified signatures or null if no literal data found */ -AsyncProxy.prototype.decryptAndVerifyMessage = function(privateKey, publicKeys, message, callback) { - privateKey = privateKey.toPacketlist(); - if (!publicKeys.length) { - publicKeys = [publicKeys]; - } - publicKeys = publicKeys.map(function(key) { - return key.toPacketlist(); - }); - this.worker.postMessage({ - event: 'decrypt-and-verify-message', - privateKey: privateKey, - publicKeys: publicKeys, - message: message - }); - this.tasks.push(function(err, data) { - if (data) { +AsyncProxy.prototype.decryptAndVerifyMessage = function(privateKey, publicKeys, message) { + var self = this; + + var promise = new Promise(function(resolve, reject) { + privateKey = privateKey.toPacketlist(); + if (!publicKeys.length) { + publicKeys = [publicKeys]; + } + publicKeys = publicKeys.map(function(key) { + return key.toPacketlist(); + }); + self.worker.postMessage({ + event: 'decrypt-and-verify-message', + privateKey: privateKey, + publicKeys: publicKeys, + message: message + }); + + self.tasks.push({ resolve:function(data) { data.signatures = data.signatures.map(function(sig) { sig.keyid = type_keyid.fromClone(sig.keyid); return sig; }); - } - callback(err, data); + resolve(data); + }, reject:reject }); }); + + return promise; }; /** * Signs a cleartext message * @param {(Array|module:key~Key)} privateKeys array of keys or single key, with decrypted secret key data to sign cleartext * @param {String} text cleartext - * @param {Function} callback receives ASCII armored message */ -AsyncProxy.prototype.signClearMessage = function(privateKeys, text, callback) { - if (!privateKeys.length) { - privateKeys = [privateKeys]; - } - privateKeys = privateKeys.map(function(key) { - return key.toPacketlist(); +AsyncProxy.prototype.signClearMessage = function(privateKeys, text) { + var self = this; + + return self.execute(function() { + if (!privateKeys.length) { + privateKeys = [privateKeys]; + } + privateKeys = privateKeys.map(function(key) { + return key.toPacketlist(); + }); + self.worker.postMessage({ + event: 'sign-clear-message', + privateKeys: privateKeys, + text: text + }); }); - this.worker.postMessage({ - event: 'sign-clear-message', - privateKeys: privateKeys, - text: text - }); - this.tasks.push(callback); }; /** * Verifies signatures of cleartext signed message * @param {(Array|module:key~Key)} publicKeys array of keys or single key, to verify signatures * @param {module:cleartext~CleartextMessage} message cleartext message object with signatures - * @param {Function} callback receives cleartext with status of verified signatures */ -AsyncProxy.prototype.verifyClearSignedMessage = function(publicKeys, message, callback) { - if (!publicKeys.length) { - publicKeys = [publicKeys]; - } - publicKeys = publicKeys.map(function(key) { - return key.toPacketlist(); - }); - this.worker.postMessage({ - event: 'verify-clear-signed-message', - publicKeys: publicKeys, - message: message - }); - this.tasks.push(function(err, data) { - if (data) { +AsyncProxy.prototype.verifyClearSignedMessage = function(publicKeys, message) { + var self = this; + + var promise = new Promise(function(resolve, reject) { + if (!publicKeys.length) { + publicKeys = [publicKeys]; + } + publicKeys = publicKeys.map(function(key) { + return key.toPacketlist(); + }); + self.worker.postMessage({ + event: 'verify-clear-signed-message', + publicKeys: publicKeys, + message: message + }); + + self.tasks.push({ resolve:function(data) { data.signatures = data.signatures.map(function(sig) { sig.keyid = type_keyid.fromClone(sig.keyid); return sig; }); - } - callback(err, data); + resolve(data); + }, reject:reject }); }); + + return promise; }; /** @@ -247,42 +286,50 @@ AsyncProxy.prototype.verifyClearSignedMessage = function(publicKeys, message, ca * @param {Integer} numBits number of bits for the key creation. (should be 1024+, generally) * @param {String} userId assumes already in form of "User Name " * @param {String} passphrase The passphrase used to encrypt the resulting private key - * @param {Function} callback receives object with key and public and private armored texts */ -AsyncProxy.prototype.generateKeyPair = function(options, callback) { - this.worker.postMessage({ - event: 'generate-key-pair', - options: options - }); - this.tasks.push(function(err, data) { - if (data) { +AsyncProxy.prototype.generateKeyPair = function(options) { + var self = this; + + var promise = new Promise(function(resolve, reject) { + self.worker.postMessage({ + event: 'generate-key-pair', + options: options + }); + + self.tasks.push({ resolve:function(data) { var packetlist = packet.List.fromStructuredClone(data.key); data.key = new key.Key(packetlist); - } - callback(err, data); + resolve(data); + }, reject:reject }); }); + + return promise; }; /** * Decrypts secret part of all secret key packets of key. * @param {module:key~Key} privateKey private key with encrypted secret key data * @param {String} password password to unlock the key - * @param {Function} callback receives decrypted key */ -AsyncProxy.prototype.decryptKey = function(privateKey, password, callback) { - privateKey = privateKey.toPacketlist(); - this.worker.postMessage({ - event: 'decrypt-key', - privateKey: privateKey, - password: password - }); - this.tasks.push(function(err, data) { - if (data) { +AsyncProxy.prototype.decryptKey = function(privateKey, password) { + var self = this; + + var promise = new Promise(function(resolve, reject) { + privateKey = privateKey.toPacketlist(); + self.worker.postMessage({ + event: 'decrypt-key', + privateKey: privateKey, + password: password + }); + + self.tasks.push({ resolve:function(data) { var packetlist = packet.List.fromStructuredClone(data); data = new key.Key(packetlist); - } - callback(err, data); + resolve(data); + }, reject:reject }); }); + + return promise; }; /** @@ -290,23 +337,27 @@ AsyncProxy.prototype.decryptKey = function(privateKey, password, callback) { * @param {module:key~Key} privateKey private key with encrypted secret key data * @param {Array} keyIds * @param {String} password password to unlock the key - * @param {Function} callback receives decrypted key */ -AsyncProxy.prototype.decryptKeyPacket = function(privateKey, keyIds, password, callback) { - privateKey = privateKey.toPacketlist(); - this.worker.postMessage({ - event: 'decrypt-key-packet', - privateKey: privateKey, - keyIds: keyIds, - password: password - }); - this.tasks.push(function(err, data) { - if (data) { +AsyncProxy.prototype.decryptKeyPacket = function(privateKey, keyIds, password) { + var self = this; + + var promise = new Promise(function(resolve, reject) { + privateKey = privateKey.toPacketlist(); + self.worker.postMessage({ + event: 'decrypt-key-packet', + privateKey: privateKey, + keyIds: keyIds, + password: password + }); + + self.tasks.push({ resolve:function(data) { var packetlist = packet.List.fromStructuredClone(data); data = new key.Key(packetlist); - } - callback(err, data); + resolve(data); + }, reject:reject }); }); + + return promise; }; module.exports = AsyncProxy; diff --git a/src/worker/worker.js b/src/worker/worker.js index 680eddf7..5e6baeb9 100644 --- a/src/worker/worker.js +++ b/src/worker/worker.js @@ -5,7 +5,7 @@ // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 3.0 of the License, or (at your option) any later version. -// +// // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU @@ -24,11 +24,12 @@ var MAX_SIZE_RANDOM_BUFFER = 60000; window.openpgp.crypto.random.randomBuffer.init(MAX_SIZE_RANDOM_BUFFER); -onmessage = function (event) { +self.onmessage = function (event) { var data = null, err = null, msg = event.data, correct = false; + switch (msg.event) { case 'seed-random': if (!(msg.buf instanceof Uint8Array)) { @@ -37,93 +38,78 @@ onmessage = function (event) { window.openpgp.crypto.random.randomBuffer.set(msg.buf); break; case 'encrypt-message': - try { - if (!msg.keys.length) { - msg.keys = [msg.keys]; - } - msg.keys = msg.keys.map(packetlistCloneToKey); - data = window.openpgp.encryptMessage(msg.keys, msg.text); - } catch (e) { - err = e.message; + if (!msg.keys.length) { + msg.keys = [msg.keys]; } - response({event: 'method-return', data: data, err: err}); + msg.keys = msg.keys.map(packetlistCloneToKey); + window.openpgp.encryptMessage(msg.keys, msg.text).then(function(data) { + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); break; case 'sign-and-encrypt-message': - try { - if (!msg.publicKeys.length) { - msg.publicKeys = [msg.publicKeys]; - } - msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey); - msg.privateKey = packetlistCloneToKey(msg.privateKey); - data = window.openpgp.signAndEncryptMessage(msg.publicKeys, msg.privateKey, msg.text); - } catch (e) { - err = e.message; + if (!msg.publicKeys.length) { + msg.publicKeys = [msg.publicKeys]; } - response({event: 'method-return', data: data, err: err}); + msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey); + msg.privateKey = packetlistCloneToKey(msg.privateKey); + window.openpgp.signAndEncryptMessage(msg.publicKeys, msg.privateKey, msg.text).then(function(data) { + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); break; case 'decrypt-message': - try { - msg.privateKey = packetlistCloneToKey(msg.privateKey); - msg.message = packetlistCloneToMessage(msg.message.packets); - data = window.openpgp.decryptMessage(msg.privateKey, msg.message); - } catch (e) { - err = e.message; - } - response({event: 'method-return', data: data, err: err}); + msg.privateKey = packetlistCloneToKey(msg.privateKey); + msg.message = packetlistCloneToMessage(msg.message.packets); + window.openpgp.decryptMessage(msg.privateKey, msg.message).then(function(data) { + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); break; case 'decrypt-and-verify-message': - try { - msg.privateKey = packetlistCloneToKey(msg.privateKey); - if (!msg.publicKeys.length) { - msg.publicKeys = [msg.publicKeys]; - } - msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey); - msg.message = packetlistCloneToMessage(msg.message.packets); - data = window.openpgp.decryptAndVerifyMessage(msg.privateKey, msg.publicKeys, msg.message); - } catch (e) { - err = e.message; + msg.privateKey = packetlistCloneToKey(msg.privateKey); + if (!msg.publicKeys.length) { + msg.publicKeys = [msg.publicKeys]; } - response({event: 'method-return', data: data, err: err}); + msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey); + msg.message = packetlistCloneToMessage(msg.message.packets); + window.openpgp.decryptAndVerifyMessage(msg.privateKey, msg.publicKeys, msg.message).then(function(data) { + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); break; case 'sign-clear-message': - try { - msg.privateKeys = msg.privateKeys.map(packetlistCloneToKey); - data = window.openpgp.signClearMessage(msg.privateKeys, msg.text); - } catch (e) { - err = e.message; - } - response({event: 'method-return', data: data, err: err}); + msg.privateKeys = msg.privateKeys.map(packetlistCloneToKey); + window.openpgp.signClearMessage(msg.privateKeys, msg.text).then(function(data) { + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); break; case 'verify-clear-signed-message': - try { - if (!msg.publicKeys.length) { - msg.publicKeys = [msg.publicKeys]; - } - msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey); - var packetlist = window.openpgp.packet.List.fromStructuredClone(msg.message.packets); - msg.message = new window.openpgp.cleartext.CleartextMessage(msg.message.text, packetlist); - data = window.openpgp.verifyClearSignedMessage(msg.publicKeys, msg.message); - } catch (e) { - err = e.message; + if (!msg.publicKeys.length) { + msg.publicKeys = [msg.publicKeys]; } - response({event: 'method-return', data: data, err: err}); + msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey); + var packetlist = window.openpgp.packet.List.fromStructuredClone(msg.message.packets); + msg.message = new window.openpgp.cleartext.CleartextMessage(msg.message.text, packetlist); + window.openpgp.verifyClearSignedMessage(msg.publicKeys, msg.message).then(function(data) { + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); break; case 'generate-key-pair': - try { - window.openpgp.generateKeyPair(msg.options, function(error, data) { - if (error) { - err = error.message; - response({event: 'method-return', data: data, err: err}); - return; - } - - data.key = data.key.toPacketlist(); - response({event: 'method-return', data: data, err: err}); - }); - } catch (e) { - err = e.message; - response({event: 'method-return', data: data, err: err}); - } + window.openpgp.generateKeyPair(msg.options).then(function(data) { + data.key = data.key.toPacketlist(); + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); break; case 'decrypt-key': try { diff --git a/test/unittests.html b/test/unittests.html index c241aa0d..4b5e3efd 100644 --- a/test/unittests.html +++ b/test/unittests.html @@ -18,6 +18,7 @@