/**
* @file Asynchronous SRA cryptosystem interface.
*
* @version 0.2.0
*/
const _defaultHostScript = "./scripts/libs/SRACryptoWorker.js"; //default WorkerHost script
let _hosts = new Array();
let _queue = new Array(); //queue of requests; automatically adjusted as WorkerHosts become available (ready)
let _maxHosts = 4; //max number of concurrent Workerhost instances
let _numHosts = 0; //current number of WorkerHost instances
/**
* @class Uses Web Workers to asynchronously perform various SRA cryptosystem
* functions.
*
* @see {@link SRACryptoWorker.js}
*/
class SRACrypto {
/**
* An encryption/decryption key pair and associated prime value. A <code>null</code>
* object indicates that the keypair is being generated. All values
* are either in hexadecimal (pre-pended with "0x"), or decimal. Keys may be
* swapped prior to first use if desired.
*
* @typedef {Object} keypair
* @property {String} encKey A string representation of the encryption key.
* @property {String} decKey A String representation of the decryption key.
* @property {String} prime A string representation of the associated prime number.
*
*
*/
/**
* Creates an instance of the SRACrypto class.
*
* @constructs
* @param {Number} [hostInstances=4] The maximum number of concurrent {@link WorkerHost}
* instances to use / manage within the new SRACrypto instance.
*/
constructor (hostInstances) {
if (isNaN(hostInstances) == false) {
_maxHosts = hostInstances;
}
for (var count = 0; count < hostInstances; count++) {
var newHost = new WorkerHost(_defaultHostScript, true);
newHost.addEventListener("ready", this.onHostReady)
SRACrypto.workerHosts.push (newHost);
}
}
/**
* Invokes an asynchronous SRA cryptosystem method using a free {@link WorkerHost}
* instance. If no instance is currently available the request is added to
* the queue and made when the first available host signals that it's ready.
*
* @param {String} method The method to invoke in the hosted Worker instance.
* @param {Object} params The parameters to invoke the method with.
* @param {*} [requestID=undefined] A request ID that can be used to track the
* request over its lifetime (useful when re-assembling multiple discrete
* invokations that could otherwise result in a race condition).
*
* @return {Promise} An asynchronous promise object that will either resolve
* and return a result (data type and format vary depending on the method
* invoked), or a rejection / error throw.
*
* @example
* //simple 1 worker example
* let SRA = new SRACrypto (1);
*
* //generate a random, 1024-bit prime value and return it in the given radix (base):
* //MUST BE either -> 16 (hex) or 10 (decimal) -- other bases not internally recognized by
* //crypto worker.
* SRA.invoke("randomPrime", {bitLength:1024, radix:16}).then(event => {
* console.log ("Generated prime value (hex): " + event.data.result);
* });
*
* @example
* //using 2 workers; here we don't care about the order of generated keypairs
* var SRA = new SRACrypto(2);
*
* //generate 25 random keypairs (usually we want just one)
* for (var count = 0; count < 25; count++) {
* //"primeVal" was pre-generated in a previous step; MUST BE in hex or decimal (radix=16 or radix=10)
* SRA.invoke("randomKeypair", {prime:primeVal}).then(event =>
* {
* console.log ("Generated keypair: "+JSON.stringify(event.data.result));
* });
* }
*
* @example
* //using 4 workers but here we need to store results in the order requested
* var SRA = new SRACrypto(4);
*
* //generate 52 quadraric residues (e.g. as plaintext identifiers for a card deck)
* var cards = new Array();
* for (var count = 0; count < 25; count++) {
* //primeVal was pre-generated in a previous step, count is the result index (requestID in result)
* SRA.invoke("randomQuadResidues", {prime:primeVal, numValues:1}, count).then(event =>
* {
* cards[event.data.requestID] = event.data.result;
* for (var count2 = startNum; count2 < cards.length; count2++) {
* if (isNaN(cards[count2])) {
* return;
* }
* }
* //all values accounted for
* for (count2 = 0; count2 < cards.length; count2++) {
* //we could include additional information from an exteral source
* //here (e.g. card names, values, colours, etc.)
* console.log ("Card #" + count2 + ": " + cards[count2]);
* }
* }
* });
* }
*
* @example
* //similar to the example above but using a promise to resolve ordered results
* var SRA = new SRACrypto(4);
*
* //encrypt 52 "cards" (quadratic residues) with a given keypair
* function doEncrypt(cards, keypairObject) {
* var promise = new Promise((resolve, reject) => {
* var encCards = new Array(cards.length);
* for (var count=0; count < cards.length; count++) {
* SRA.invoke("encrypt", {value: cards[count], keypair: keypairObject}, count).then(event =>
* {
* encCards[event.data.requestID] = event.data.result;
* for (var count2 = startNum; count2 < decCards.length; count2++) {
* if (isNaN(ordereredResults[count2])) {
* return;
* }
* }
* //promise is resolved only when all cards have been stored
* resolve (encCards);
* });
* }
* });
* return (promise);
* }
*
* //cards and keypair were pre-generated in a previous step
* doEncrypt(cards, keypair).then(encCards => {
* for (var count=0; count < encCards.length; count++ )
* console.log ("Card #"+count+" "+cards[count]+" encrypted to "+encCards[count]);
* }
* })
*
* @example
* //using an intermediate async function to perform similar functionality as above
* var SRA = new SRACrypto(4);
*
* //decrypt a deck of single-encrypted cards
* function doDecrypt(encCards, keypair) {
* var promise = new Promise((resolve, reject) => {
* var decCards = new Array(encCards.length);
* for (var count=0; count < encCards.length; count++) {
* SRA.invoke("decrypt", {value:encCards[count], keypair:keypair}, count).then(event =>
* {
* decCards[event.data.requestID] = event.data.result;
* for (var count2 = startNum; count2 < decCards.length; count2++) {
* if (isNaN(ordereredResults[count2])) {
* return;
* }
* }
* resolve (ordereredResults);
* }
* });
* }
* });
* return (promise);
* }
*
* async function decryptAsync(encryptedCardsArray, encryptingKeypairObject) {
* var decCards = await doDecrypt(encryptedCardsArray, encryptingKeypairObject);
* for (var count=0; count<decCards.length; count++) {
* console.log ("Card #"+count+" "+encryptedCardsArray[count]+" decypted to "+decCards[count]);
* }
* }
*
* //encCards and keypair were pre-generated in a previous step
* decryptAsync(encCards, keypair);
*
* @example
* var SRA = new SRACrypto(4);
* //a smaller async example of the above, this time using Promises.all to
* //resolve the queued results but still using requestIDs for ordering
*
* async function decryptAsync(encCards, keypair) {
* var promises = new Array();
* for (var count=0; count < encCards.length; count++) {
* promises.push(SRA.invoke("decrypt", {value:encCards[count], keypair:keypair}, count));
* }
* var resultPromises = await Promise.all(promises);
* //at this point we know that all promises have been fulfilled
* var decCards = new Array(resultPromises.length);
* for (var count=0; count < resultPromises.length; count++) {
* decCards[resultPromises[count].data.requestID] = resultPromises[count].data.result;
* }
* return (decCards);
* }
*
* //encCards and keypair were pre-generated in a previous step
* decryptAsync(encCards, keypair).then(decCards => {
* for (var count=0; count<decCards.length; count++) {
* console.log ("Card #"+count+" "+encCards[count]+" decypted to "+decCards[count]);
* }
* })
*
* @example
* var SRA = new SRACrypto(4);
* //an even smaller example of the above without using a requestID
*
* var promises = new Array();
* async function decryptAsync(encCards, keypair) {
* var promises = new Array();
* for (var count=0; count < encCards.length; count++) {
* promises.push(SRA.invoke("decrypt", {value:encCards[count], keypair:keypair}));
* }
* var resultPromises = await Promise.all(promises);
* return (resultPromises);
* }
*
* //encCards and keypair were pre-generated in a previous step
* decryptAsync(encCards, keypair).then(results => {
* for (var count=0; count<results.length; count++) {
* console.log ("Card #"+count+" "+encCards[count]+" decrypted to "+results[count].data.result);
* }
* })
*/
async invoke (method, params, requestID) {
for (var count = 0; count < SRACrypto.workerHosts.length; count++) {
var currentHost = SRACrypto.workerHosts[count];
if (currentHost.ready) {
return (currentHost.invoke(method, params, requestID));
}
}
//no available hosts at this moment
var promise = new Promise ((resolve, reject) => {
var queueObj = new Object();
queueObj.resolve = resolve;
queueObj.reject = reject;
queueObj.method = method;
queueObj.params = params;
queueObj.requestID = requestID;
_queue.push(queueObj);
});
return (promise);
}
/**
* Event listener triggered when a {@link WorkerHost} instance reports that it
* is ready for new requests. The internal queue is automatically adjusted
* and a new request triggered if there are queued requests.
*/
async onHostReady (event) {
//note we use custom "source" property instead of "target" here
var currentHost = event.source;
if (_queue.length > 0) {
var currentQueueItem = _queue.shift();
var result = await currentHost.invoke(currentQueueItem.method, currentQueueItem.params, currentQueueItem.requestID);
currentQueueItem.resolve(result);
}
}
/**
* @property {Array} workerHosts All of the registered {@link WorkerHost} instances
* being managed by this class instance.
*/
static get workerHosts () {
return (_hosts);
}
/**
* @property {Array} requestQueue All currently queued requests being managed by
* the class instance.
*/
static get requestQueue () {
return (_queue);
}
}