Browse Source

v1.1.0: Add tests, more convenience methods, more docs

undefined
AJ ONeal 1 year ago
parent
commit
7099943db7
5 changed files with 263 additions and 19 deletions
  1. +68
    -6
      README.md
  2. +78
    -2
      keypairs.js
  3. +7
    -7
      package-lock.json
  4. +4
    -4
      package.json
  5. +106
    -0
      test.js

+ 68
- 6
README.md View File

@@ -1,4 +1,4 @@
# Keypairs for node.js
# Keypairs.js

Lightweight JavaScript RSA and ECDSA utils that work on Windows, Mac, and Linux
using modern node.js APIs (no need for C compiler).
@@ -13,6 +13,7 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
* [x] ECDSA (P-256, P-384)
* [x] PEM-to-JWK
* [x] JWK-to-PEM
* [x] Create JWTs (and sign JWS)
* [x] SHA256 JWK Thumbprints
* [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/)
* [ ] OIDC
@@ -20,7 +21,6 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).

<!--

* [ ] sign JWS
* [ ] generate CSR (DER as PEM or base64url)

-->
@@ -54,6 +54,16 @@ _much_ longer than RSA has, and they're smaller, and faster to generate.

## API Overview

* generate
* parse
* parseOrGenerate
* import (PEM-to-JWK)
* export (JWK-to-PEM, private or public)
* publish (Private JWK to Public JWK)
* thumbprint (JWK SHA256)
* signJwt
* signJws

#### Keypairs.generate(options)

Generates a public/private pair of JWKs as `{ private, public }`
@@ -65,6 +75,50 @@ Option examples:

When no options are supplied EC P-256 (also known as `prime256v1` and `secp256r1`) is used by default.

#### Keypairs.parse(options)

Parses either a JWK (encoded as JSON) or an x509 (encdode as PEM) and gives
back the JWK representation.

Option Examples:

* JWK { key: '{ "kty":"EC", ... }' }
* PEM { key: '-----BEGIN PRIVATE KEY-----\n...' }
* Public Key Only { key: '-----BEGIN PRIVATE KEY-----\n...', public: true }
* Must Have Private Key { key: '-----BEGIN PUBLIC KEY-----\n...', private: true }

Example:

```js
Keypairs.parse({ key: '...' }).catch(function (e) {
// could not be parsed or was a public key
console.warn(e);
return Keypairs.generate();
});
```

#### Keypairs.parseOrGenerate({ key, throw, [generate opts]... })

Parses the key. Logs a warning on failure, marches on.
(a shortcut for the above, with `private: true`)

Option Examples:

* parse key if exist, otherwise generate `{ key: process.env["PRIVATE_KEY"] }`
* generated key curve `{ key: null, namedCurve: 'P-256' }`
* generated key modulus `{ key: null, modulusLength: 2048 }`

Example:

```js
Keypairs.parseOrGenerate({ key: process.env["PRIVATE_KEY"] }).then(function (pair) {
console.log(pair.public);
})
```

Great for when you have a set of shared keys for development and randomly
generated keys in

#### Keypairs.import({ pem: '...' }

Takes a PEM in pretty much any format (PKCS1, SEC1, PKCS8, SPKI) and returns a JWK.
@@ -85,6 +139,10 @@ Options
}
```

#### Keypairs.publish({ jwk: jwk })

**Synchronously** strips a key of its private parts and returns the public version.

#### Keypairs.thumbprint({ jwk: jwk })

Promises a JWK-spec thumbprint: URL Base64-encoded sha256
@@ -134,11 +192,15 @@ Options:

# Additional Documentation

Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs,
but it also includes the additional convenience methods `signJwt` and `signJws`.
Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs for the following:

* generate(options)
* import({ pem: '---BEGIN...' })
* export({ jwk: { kty: 'EC', ... })
* thumbprint({ jwk: jwk })

That is to say that any option you pass to Keypairs will be passed directly to the corresponding API
of either Rasha or Eckles.
If you want to know the algorithm-specific options that are available for those
you'll want to take a look at the corresponding documentation:

* See ECDSA documentation at [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js/)
* See RSA documentation at [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js/)


+ 78
- 2
keypairs.js View File

@@ -16,9 +16,64 @@ Keypairs.generate = function (opts) {
return Eckles.generate(opts);
};

Keypairs.parse = function (opts) {
opts = opts || {};

var err;
var jwk;
var pem;
var p;

try {
jwk = JSON.parse(opts.key);
p = Keypairs.export({ jwk: jwk }).catch(function (e) {
pem = opts.key;
err = new Error("Not a valid jwk '" + JSON.stringify(jwk) + "':" + e.message);
err.code = "EINVALID";
return Promise.reject(err);
}).then(function () {
return jwk;
});
} catch(e) {
p = Keypairs.import({ pem: opts.key }).catch(function (e) {
err = new Error("Could not parse key (type " + typeof opts.key + ") '" + opts.key + "': " + e.message);
err.code = "EPARSE";
return Promise.reject(err);
});
}

return p.then(function (jwk) {
var pubopts = JSON.parse(JSON.stringify(opts));
pubopts.jwk = jwk;
return Keypairs.publish(pubopts).then(function (pub) {
// 'd' happens to be the name of a private part of both RSA and ECDSA keys
if (opts.public || opts.publish || !jwk.d) {
if (opts.private) {
// TODO test that it can actually sign?
err = new Error("Not a private key '" + JSON.stringify(jwk) + "'");
err.code = "ENOTPRIVATE";
return Promise.reject(err);
}
return { public: pub };
} else {
return { private: jwk, public: pub };
}
});
});
};

Keypairs.parseOrGenerate = function (opts) {
if (!opts.key) { return Keypairs.generate(opts); }
opts.private = true;
return Keypairs.parse(opts).catch(function (e) {
console.warn(e.message);
return Keypairs.generate(opts);
});
};

Keypairs.import = function (opts) {
return Eckles.import(opts.pem).catch(function () {
return Rasha.import(opts.pem);
return Eckles.import(opts).catch(function () {
return Rasha.import(opts);
});
};

@@ -32,6 +87,27 @@ Keypairs.export = function (opts) {
});
};

Keypairs.publish = function (opts) {
if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); }

// trying to find the best balance of an immutable copy with custom attributes
var jwk = {};
Object.keys(opts.jwk).forEach(function (k) {
// ignore RSA and EC private parts
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; }
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});

if (!jwk.exp) {
if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; }
else { jwk.exp = opts.expiresAt; }
}
if (!jwk.use && false !== jwk.use) { jwk.use = "sig"; }

if (jwk.kid) { return Promise.resolve(jwk); }
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { jwk.kid = thumb; return jwk; });
};

Keypairs.thumbprint = function (opts) {
return Promise.resolve().then(function () {
if ('RSA' === opts.jwk.kty) {


+ 7
- 7
package-lock.json View File

@@ -1,18 +1,18 @@
{
"name": "keypairs",
"version": "1.0.1",
"version": "1.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"eckles": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.0.tgz",
"integrity": "sha512-Bm5dpwhsBuoCHvKCY3gAvP8XFyXH7im8uAu3szykpVNbFBdC+lOuV8vLC8fvTYRZBfFqB+k/P6ud/ZPVO2V2tA=="
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
},
"rasha": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.1.tgz",
"integrity": "sha512-cs4Hu/rVF3/Qucq+V7lxSz449VfHNMVXJaeajAHno9H5FC1PWlmS4NM6IAX5jPKFF0IC2rOdHdf7iNxQuIWZag=="
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
"integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q=="
}
}
}

+ 4
- 4
package.json View File

@@ -1,11 +1,11 @@
{
"name": "keypairs",
"version": "1.0.1",
"version": "1.1.0",
"description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM",
"main": "keypairs.js",
"files": [],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node test.js"
},
"repository": {
"type": "git",
@@ -21,7 +21,7 @@
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0",
"dependencies": {
"eckles": "^1.4.0",
"rasha": "^1.2.1"
"eckles": "^1.4.1",
"rasha": "^1.2.4"
}
}

+ 106
- 0
test.js View File

@@ -0,0 +1,106 @@
var Keypairs = require('./');

/* global Promise*/
Keypairs.parseOrGenerate({ key: '' }).then(function (pair) {
// should NOT have any warning output
if (!pair.private || !pair.public) {
throw new Error("missing key pairs");
}

return Promise.all([
// Testing Public Part of key
Keypairs.export({ jwk: pair.public }).then(function (pem) {
if (!/--BEGIN PUBLIC/.test(pem)) {
throw new Error("did not export public pem");
}
return Promise.all([
Keypairs.parse({ key: pem }).then(function (pair) {
if (pair.private) {
throw new Error("shouldn't have private part");
}
return true;
})
, Keypairs.parse({ key: pem, private: true }).then(function () {
var err = new Error("should have thrown an error when private key was required and public pem was given");
err.code = 'NOERR';
throw err;
}).catch(function (e) {
if ('NOERR' === e.code) { throw e; }
return true;
})
]).then(function () {
return true;
});
})
// Testing Private Part of Key
, Keypairs.export({ jwk: pair.private }).then(function (pem) {
if (!/--BEGIN .*PRIVATE KEY--/.test(pem)) {
throw new Error("did not export private pem: " + pem);
}
return Promise.all([
Keypairs.parse({ key: pem }).then(function (pair) {
if (!pair.private) {
throw new Error("should have private part");
}
if (!pair.public) {
throw new Error("should have public part also");
}
return true;
})
, Keypairs.parse({ key: pem, public: true }).then(function (pair) {
if (pair.private) {
throw new Error("should NOT have private part");
}
if (!pair.public) {
throw new Error("should have the public part though");
}
return true;
})
]).then(function () {
return true;
});
})
, Keypairs.parseOrGenerate({ key: 'not a key', public: true }).then(function (pair) {
// SHOULD have warning output
if (!pair.private || !pair.public) {
throw new Error("missing key pairs (should ignore 'public')");
}
return true;
})
, Keypairs.parse({ key: JSON.stringify(pair.private) }).then(function (pair) {
if (!pair.private || !pair.public) {
throw new Error("missing key pairs (stringified jwt)");
}
return true;
})
, Keypairs.parse({ key: JSON.stringify(pair.private), public: true }).then(function (pair) {
if (pair.private) {
throw new Error("has private key when it shouldn't");
}
if (!pair.public) {
throw new Error("doesn't have public key when it should");
}
return true;
})
, Keypairs.parse({ key: JSON.stringify(pair.public), private: true }).then(function () {
var err = new Error("should have thrown an error when private key was required and public jwk was given");
err.code = 'NOERR';
throw err;
}).catch(function (e) {
if ('NOERR' === e.code) { throw e; }
return true;
})
]).then(function (results) {
if (results.length && results.every(function (v) { return true === v; })) {
console.info("If a warning prints right above this, it's a pass");
console.log("PASS");
process.exit(0);
} else {
throw new Error("didn't get all passes (but no errors either)");
}
});
}).catch(function (e) {
console.error("Caught an unexpected (failing) error:");
console.error(e);
process.exit(1);
});

Loading…
Cancel
Save