Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add callback to make key gen async #7

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,36 @@ Generate a RSA PEM key pair from pure JS
```js
var keypair = require('keypair');

var pair = keypair();
console.log(pair);
var syncPair = keypair();
console.log('Synchronously generated keypair')
console.log(syncPair);

keypair(function(asyncPair) {
console.log('Asynchronously generated keypair');
console.log(asyncPair);
});
```

outputs

```
$ node example.js
Synchronously generated keypair
{ public: '-----BEGIN RSA PUBLIC KEY-----\r\nMIGJAoGBAM3CosR73CBNcJsLv5E90NsFt6qN1uziQ484gbOoule8leXHFbyIzPQRozgEpSpi\r\nwhr6d2/c0CfZHEJ3m5tV0klxfjfM7oqjRMURnH/rmBjcETQ7qzIISZQ/iptJ3p7Gi78X5ZMh\r\nLNtDkUFU9WaGdiEb+SnC39wjErmJSfmGb7i1AgMBAAE=\r\n-----END RSA PUBLIC KEY-----\n',
private: '-----BEGIN RSA PRIVATE KEY-----\r\nMIICXAIBAAKBgQDNwqLEe9wgTXCbC7+RPdDbBbeqjdbs4kOPOIGzqLpXvJXlxxW8iMz0EaM4\r\nBKUqYsIa+ndv3NAn2RxCd5ubVdJJcX43zO6Ko0TFEZx/65gY3BE0O6syCEmUP4qbSd6exou/\r\nF+WTISzbQ5FBVPVmhnYhG/kpwt/cIxK5iUn5hm+4tQIDAQABAoGBAI+8xiPoOrA+KMnG/T4j\r\nJsG6TsHQcDHvJi7o1IKC/hnIXha0atTX5AUkRRce95qSfvKFweXdJXSQ0JMGJyfuXgU6dI0T\r\ncseFRfewXAa/ssxAC+iUVR6KUMh1PE2wXLitfeI6JLvVtrBYswm2I7CtY0q8n5AGimHWVXJP\r\nLfGV7m0BAkEA+fqFt2LXbLtyg6wZyxMA/cnmt5Nt3U2dAu77MzFJvibANUNHE4HPLZxjGNXN\r\n+a6m0K6TD4kDdh5HfUYLWWRBYQJBANK3carmulBwqzcDBjsJ0YrIONBpCAsXxk8idXb8jL9a\r\nNIg15Wumm2enqqObahDHB5jnGOLmbasizvSVqypfM9UCQCQl8xIqy+YgURXzXCN+kwUgHinr\r\nutZms87Jyi+D8Br8NY0+Nlf+zHvXAomD2W5CsEK7C+8SLBr3k/TsnRWHJuECQHFE9RA2OP8W\r\noaLPuGCyFXaxzICThSRZYluVnWkZtxsBhW2W8z1b8PvWUE7kMy7TnkzeJS2LSnaNHoyxi7Ia\r\nPQUCQCwWU4U+v4lD7uYBw00Ga/xt+7+UqFPlPVdz1yyr4q24Zxaw0LgmuEvgU5dycq8N7Jxj\r\nTubX0MIRR+G9fmDBBl8=\r\n-----END RSA PRIVATE KEY-----\n' }
Asynchronously generated keypair
{ public: '-----BEGIN RSA PUBLIC KEY-----\r\nMIGJAoGBAM3CosR73CBNcJsLv5E90NsFt6qN1uziQ484gbOoule8leXHFbyIzPQRozgEpSpi\r\nwhr6d2/c0CfZHEJ3m5tV0klxfjfM7oqjRMURnH/rmBjcETQ7qzIISZQ/iptJ3p7Gi78X5ZMh\r\nLNtDkUFU9WaGdiEb+SnC39wjErmJSfmGb7i1AgMBAAE=\r\n-----END RSA PUBLIC KEY-----\n',
private: '-----BEGIN RSA PRIVATE KEY-----\r\nMIICXAIBAAKBgQDNwqLEe9wgTXCbC7+RPdDbBbeqjdbs4kOPOIGzqLpXvJXlxxW8iMz0EaM4\r\nBKUqYsIa+ndv3NAn2RxCd5ubVdJJcX43zO6Ko0TFEZx/65gY3BE0O6syCEmUP4qbSd6exou/\r\nF+WTISzbQ5FBVPVmhnYhG/kpwt/cIxK5iUn5hm+4tQIDAQABAoGBAI+8xiPoOrA+KMnG/T4j\r\nJsG6TsHQcDHvJi7o1IKC/hnIXha0atTX5AUkRRce95qSfvKFweXdJXSQ0JMGJyfuXgU6dI0T\r\ncseFRfewXAa/ssxAC+iUVR6KUMh1PE2wXLitfeI6JLvVtrBYswm2I7CtY0q8n5AGimHWVXJP\r\nLfGV7m0BAkEA+fqFt2LXbLtyg6wZyxMA/cnmt5Nt3U2dAu77MzFJvibANUNHE4HPLZxjGNXN\r\n+a6m0K6TD4kDdh5HfUYLWWRBYQJBANK3carmulBwqzcDBjsJ0YrIONBpCAsXxk8idXb8jL9a\r\nNIg15Wumm2enqqObahDHB5jnGOLmbasizvSVqypfM9UCQCQl8xIqy+YgURXzXCN+kwUgHinr\r\nutZms87Jyi+D8Br8NY0+Nlf+zHvXAomD2W5CsEK7C+8SLBr3k/TsnRWHJuECQHFE9RA2OP8W\r\noaLPuGCyFXaxzICThSRZYluVnWkZtxsBhW2W8z1b8PvWUE7kMy7TnkzeJS2LSnaNHoyxi7Ia\r\nPQUCQCwWU4U+v4lD7uYBw00Ga/xt+7+UqFPlPVdz1yyr4q24Zxaw0LgmuEvgU5dycq8N7Jxj\r\nTubX0MIRR+G9fmDBBl8=\r\n-----END RSA PRIVATE KEY-----\n' }
```

## Performance

Performance greatly depends on the bit size of the generated private key. With 1024 bits you get a key in 0.5s-2s, with 2048 bits it takes 8s-20s, on the same machine. As this will block the event loop while generating the key,
make sure that's ok or to spawn a child process or run it inside a webworker.
make sure that's ok or provide a callback to run asynchronously.

## API

### keypair([opts])
### keypair([opts], [callback])

Get an RSA PEM key pair.

Expand All @@ -40,6 +50,8 @@ Get an RSA PEM key pair.
* `bits`: the size for the private key in bits. Default: **2048**.
* `e`: the public exponent to use. Default: **65537**.

`callback` if provided makes `keypair` asynchronous. `callback` must be a function.

## Installation

With [npm](http://npmjs.org) do
Expand Down
207 changes: 38 additions & 169 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,44 @@ var util = forge.util = {};
* Expose `keypair`.
*/

module.exports = function (opts) {
module.exports = function (opts, callback) {
var wrappedCallback;

if (arguments.length === 1) {
if (typeof opts === 'function') {
callback = opts;
opts = undefined;
}
}

if (!opts) opts = {};
if (typeof opts.bits == 'undefined') opts.bits = 2048;
var keypair = forge.rsa.generateKeyPair(opts);
keypair = {
public: fix(forge.pki.publicKeyToRSAPublicKeyPem(keypair.publicKey, 72)),
private: fix(forge.pki.privateKeyToPem(keypair.privateKey, 72))
};
return keypair;

if (callback) {
wrappedCallback = function generateKeyPairCallback(err, keypair) {
callback(err, fixKeypair(keypair));
};
}

var keypair = forge.rsa.generateKeyPair(null, null, opts, wrappedCallback);
Copy link

@jgrosso jgrosso Sep 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either null should be undefined or the definition of generateKeyPair should check these parameters for null as well as undefined (https://github.com/juliangruber/keypair/blob/master/index.js#L4310 and https://github.com/juliangruber/keypair/blob/master/index.js#L4313). Otherwise, I don't think the default values for these options will be set correctly.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// If we are async this will just return undefined.
return fixKeypair(keypair);
};

function fix (str) {
return str.replace(/\r/g, '') + '\n'
}

function fixKeypair(keypair) {
if (!keypair) return;

return {
public: fix(forge.pki.publicKeyToRSAPublicKeyPem(keypair.publicKey, 72)),
private: fix(forge.pki.privateKeyToPem(keypair.privateKey, 72))
};
}

/**
* util.fillString
*/
Expand Down Expand Up @@ -4092,171 +4115,17 @@ function _generateKeyPair(state, options, callback) {
options = {};
}

// web workers unavailable, use setImmediate
if(false || typeof(Worker) === 'undefined') {
function step() {
// 10 ms gives 5ms of leeway for other calculations before dropping
// below 60fps (1000/60 == 16.67), but in reality, the number will
// likely be higher due to an 'atomic' big int modPow
if(forge.pki.rsa.stepKeyPairGenerationState(state, 10)) {
return callback(null, state.keys);
}
forge.util.setImmediate(step);
function step() {
// 10 ms gives 5ms of leeway for other calculations before dropping
// below 60fps (1000/60 == 16.67), but in reality, the number will
// likely be higher due to an 'atomic' big int modPow
if(forge.pki.rsa.stepKeyPairGenerationState(state, 10)) {
return callback(null, state.keys);
}
return step();
}

// use web workers to generate keys
var numWorkers = options.workers || 2;
var workLoad = options.workLoad || 100;
var range = workLoad * 30/8;
var workerScript = options.workerScript || 'forge/prime.worker.js';
var THIRTY = new BigInteger(null);
THIRTY.fromInt(30);
var op_or = function(x,y) { return x|y; };
generate();

function generate() {
// find p and then q (done in series to simplify setting worker number)
getPrime(state.pBits, function(err, num) {
if(err) {
return callback(err);
}
state.p = num;
getPrime(state.qBits, finish);
});
forge.util.setImmediate(step);
}

// implement prime number generation using web workers
function getPrime(bits, callback) {
// TODO: consider optimizing by starting workers outside getPrime() ...
// note that in order to clean up they will have to be made internally
// asynchronous which may actually be slower

// start workers immediately
var workers = [];
for(var i = 0; i < numWorkers; ++i) {
// FIXME: fix path or use blob URLs
workers[i] = new Worker(workerScript);
}
var running = numWorkers;

// initialize random number
var num = generateRandom();

// listen for requests from workers and assign ranges to find prime
for(var i = 0; i < numWorkers; ++i) {
workers[i].addEventListener('message', workerMessage);
}

/* Note: The distribution of random numbers is unknown. Therefore, each
web worker is continuously allocated a range of numbers to check for a
random number until one is found.

Every 30 numbers will be checked just 8 times, because prime numbers
have the form:

30k+i, for i < 30 and gcd(30, i)=1 (there are 8 values of i for this)

Therefore, if we want a web worker to run N checks before asking for
a new range of numbers, each range must contain N*30/8 numbers.

For 100 checks (workLoad), this is a range of 375. */

function generateRandom() {
var bits1 = bits - 1;
var num = new BigInteger(bits, state.rng);
// force MSB set
if(!num.testBit(bits1)) {
num.bitwiseTo(BigInteger.ONE.shiftLeft(bits1), op_or, num);
}
// align number on 30k+1 boundary
num.dAddOffset(31 - num.mod(THIRTY).byteValue(), 0);
return num;
}

var found = false;
function workerMessage(e) {
// ignore message, prime already found
if(found) {
return;
}

--running;
var data = e.data;
if(data.found) {
// terminate all workers
for(var i = 0; i < workers.length; ++i) {
workers[i].terminate();
}
found = true;
return callback(null, new BigInteger(data.prime, 16));
}

// overflow, regenerate prime
if(num.bitLength() > bits) {
num = generateRandom();
}

// assign new range to check
var hex = num.toString(16);

// start prime search
e.target.postMessage({
e: state.eInt,
hex: hex,
workLoad: workLoad
});

num.dAddOffset(range, 0);
}
}

function finish(err, num) {
// set q
state.q = num;

// ensure p is larger than q (swap them if not)
if(state.p.compareTo(state.q) < 0) {
var tmp = state.p;
state.p = state.q;
state.q = tmp;
}

// compute phi: (p - 1)(q - 1) (Euler's totient function)
state.p1 = state.p.subtract(BigInteger.ONE);
state.q1 = state.q.subtract(BigInteger.ONE);
state.phi = state.p1.multiply(state.q1);

// ensure e and phi are coprime
if(state.phi.gcd(state.e).compareTo(BigInteger.ONE) !== 0) {
// phi and e aren't coprime, so generate a new p and q
state.p = state.q = null;
generate();
return;
}

// create n, ensure n is has the right number of bits
state.n = state.p.multiply(state.q);
if(state.n.bitLength() !== state.bits) {
// failed, get new q
state.q = null;
getPrime(state.qBits, finish);
return;
}

// set keys
var d = state.e.modInverse(state.phi);
state.keys = {
privateKey: forge.pki.rsa.setPrivateKey(
state.n, state.e, d, state.p, state.q,
d.mod(state.p1), d.mod(state.q1),
state.q.modInverse(state.p)),
publicKey: forge.pki.rsa.setPublicKey(state.n, state.e)
};

callback(null, state.keys);
}
return step();
}

/**
Expand Down
9 changes: 8 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@ test('keypair', function (t) {
t.assert(/BEGIN RSA PRIVATE KEY/.test(pair.private), 'private header');
t.ok(pair.public, 'public key');
t.assert(/BEGIN RSA PUBLIC KEY/.test(pair.public), 'public header');
t.end();

keypair(function(err, pair) {
t.ok(pair.private, 'private key');
t.assert(/BEGIN RSA PRIVATE KEY/.test(pair.private), 'private header');
t.ok(pair.public, 'public key');
t.assert(/BEGIN RSA PUBLIC KEY/.test(pair.public), 'public header');
t.end();
});
});