Compare commits

...

17 Commits

Author SHA1 Message Date
b22f957124 update name: convertIfEcdsa => ecdsaAsn1SigToJoseSig 2019-05-06 03:07:30 -06:00
48bee9204d v1.2.14: use try/catch on PEM import 2019-03-14 11:50:37 -06:00
5dc3795a17 v1.2.13: add support for rsa-compat 2019-03-14 11:38:02 -06:00
3c84a7e1bd v1.2.12: fix EC sig padding issues 2019-03-08 19:03:55 -07:00
65db78a3c5 v1.2.11: convert ECDSA ASN.1 signature to ECDSA JWT type 2019-03-08 16:46:50 -07:00
e04557c84a v1.2.9: bugfix RSA hash type 2019-03-08 15:24:52 -07:00
083cc6d73e v1.2.8: allow non-stringified jwk in parse, ignore undefineds when neutering 2019-03-08 12:57:50 -07:00
885a00c3ae v1.2.7: update docs, handle human readable 'exp' claims, denest required claims 2019-03-08 12:30:32 -07:00
34d0ca9f13 v1.2.6: Make neuter public, replace bin with installer 2019-03-07 00:04:55 -07:00
ad312edfa6 v1.2.5: fix multiple bugs with conversion 2019-03-05 12:23:50 -07:00
a5236f6c2f v1.2.4: remove stray logs 2019-03-05 07:45:55 -07:00
286606720d v1.2.3: bugfix not passing pair 2019-03-05 07:42:44 -07:00
f1b4ccf792 v1.2.3: bugfix double convert 2019-03-05 07:37:43 -07:00
4ab4cbd2ab bugfix readKeypair 2019-03-05 07:35:22 -07:00
4ac6e7f8dd v1.2.0: add CLI stuff 2019-03-05 07:27:17 -07:00
7099943db7 v1.1.0: Add tests, more convenience methods, more docs 2019-03-04 17:16:25 -07:00
738be9b656 v1.0.1: export and update docs 2019-02-27 00:11:50 -07:00
7 changed files with 727 additions and 55 deletions

243
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 Lightweight JavaScript RSA and ECDSA utils that work on Windows, Mac, and Linux
using modern node.js APIs (no need for C compiler). using modern node.js APIs (no need for C compiler).
@ -11,69 +11,242 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
* [x] Generate keypairs * [x] Generate keypairs
* [x] RSA * [x] RSA
* [x] ECDSA (P-256, P-384) * [x] ECDSA (P-256, P-384)
* [x] PEM-to-JWK * [x] PEM-to-JWK (and SSH-to-JWK)
* [x] JWK-to-PEM * [x] JWK-to-PEM (and JWK-to-SSH)
* [x] Create JWTs (and sign JWS)
* [x] SHA256 JWK Thumbprints * [x] SHA256 JWK Thumbprints
* [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/) * [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/)
* [ ] OIDC * [ ] OIDC
* [ ] Auth0 * [ ] Auth0
* [ ] CLI
* See [keypairs-cli](https://npmjs.com/packages/keypairs-cli/)
<!-- <!--
* [ ] sign JWS
* [ ] generate CSR (DER as PEM or base64url) * [ ] generate CSR (DER as PEM or base64url)
--> -->
# Usage # Usage
A brief (albeit somewhat nonsensical) introduction to the APIs: A brief introduction to the APIs:
``` ```
Keypairs.generate().then(function (jwk) { // generate a new keypair as jwk
return Keypairs.export({ jwk: jwk }).then(function (pem) { // (defaults to EC P-256 when no options are specified)
return Keypairs.import({ pem: pem }).then(function (jwk) { Keypairs.generate().then(function (pair) {
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { console.log(pair.private);
console.log(pair.public);
});
```
```
// JWK to PEM
// (supports various 'format' and 'encoding' options)
return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function (pem) {
console.log(pem);
});
```
```
// PEM to JWK
return Keypairs.import({ pem: pem }).then(function (jwk) {
console.log(jwk);
});
```
```
// Thumbprint a JWK (SHA256)
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
console.log(thumb); console.log(thumb);
}); });
}); ```
});
```
// Sign a JWT (aka compact JWS)
return Keypairs.signJwt({
jwk: pair.private
, iss: 'https://example.com'
, exp: '1h'
// optional claims
, claims: {
, sub: 'jon.doe@gmail.com'
}
}); });
``` ```
By default ECDSA keys will be used since they've had native support in node By default ECDSA keys will be used since they've had native support in node
_much_ longer than RSA has, and they're smaller, and faster to generate. _much_ longer than RSA has, and they're smaller, and faster to generate.
## API ## API Overview
Each of these return a Promise. * generate (JWK)
* parse (PEM)
* parseOrGenerate (PEM to JWK)
* 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)` #### Keypairs.generate(options)
* options example `{ kty: 'RSA', modulusLength: 2048 }`
* options example `{ kty: 'ECDSA', namedCurve: 'P-256' }`
* `Keypairs.import(options)`
* options example `{ pem: '...' }`
* `Keypairs.export(options)`
* options example `{ jwk: jwk }`
* options example `{ jwk: jwk, public: true }`
* `Keypairs.thumbprint({ jwk: jwk })`
<!-- Generates a public/private pair of JWKs as `{ private, public }`
* `Keypairs.jws.sign(options)` Option examples:
* options example `{ keypair, header, protected, payload }`
* `Keypairs.csr.generate(options)`
* options example `{ keypair, [ 'example.com' ] }`
--> * RSA `{ kty: 'RSA', modulusLength: 2048 }`
* ECDSA `{ kty: 'ECDSA', namedCurve: 'P-256' }`
# Full Documentation When no options are supplied EC P-256 (also known as `prime256v1` and `secp256r1`) is used by default.
Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs. #### Keypairs.parse(options)
The full RSA documentation is at [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js/) Parses either a JWK (encoded as JSON) or an x509 (encdode as PEM) and gives
back the JWK representation.
The full ECDSA documentation is at [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js/) 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.
#### Keypairs.export(options)
Exports a JWK as a PEM.
Exports PEM in PKCS8 (private) or SPKI (public) by default.
Options
```js
{ jwk: jwk
, public: true
, encoding: 'pem' // or 'der'
, format: 'pkcs8' // or 'ssh', 'pkcs1', 'sec1', 'spki'
}
```
#### Keypairs.publish({ jwk: jwk, exp: '3d', use: 'sig' })
Promises a public key that adheres to the OIDC and Auth0 spec (plus expiry), suitable to be published to a JWKs URL:
```
{ "kty": "EC"
, "crv": "P-256"
, "x": "..."
, "y": "..."
, "kid": "..."
, "use": "sig"
, "exp": 1552074208
}
```
In particular this adds "use" and "exp".
#### Keypairs.thumbprint({ jwk: jwk })
Promises a JWK-spec thumbprint: URL Base64-encoded sha256
#### Keypairs.signJwt({ jwk, header, claims })
Returns a JWT (otherwise known as a protected JWS in "compressed" format).
```js
{ jwk: jwk
// required claims
, iss: 'https://example.com'
, exp: '15m'
// all optional claims
, claims: {
}
}
```
Exp may be human readable duration (i.e. 1h, 15m, 30s) or a datetime in seconds.
Header defaults:
```js
{ kid: thumbprint
, alg: 'xS256'
, typ: 'JWT'
}
```
Payload notes:
* `iat: now` is added by default (set `false` to disable)
* `exp` must be set (set `false` to disable)
* `iss` should be the base URL for JWK lookup (i.e. via OIDC, Auth0)
Notes:
`header` is actually the JWS `protected` value, as all JWTs use protected headers (yay!)
and `claims` are really the JWS `payload`.
#### Keypairs.signJws({ jwk, header, protected, payload })
This is provided for APIs like ACME (Let's Encrypt) that use uncompressed JWS (instead of JWT, which is compressed).
Options:
* `header` not what you think. Leave undefined unless you need this for the spec you're following.
* `protected` is the typical JWT-style header
* `kid` and `alg` will be added by default (these are almost always required), set `false` explicitly to disable
* `payload` can be JSON, a string, or even a buffer (which gets URL Base64 encoded)
* you must set this to something, even if it's an empty string, object, or Buffer
# Additional Documentation
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 })
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/)
Any option you pass to Keypairs will be passed directly to the corresponding API
of either Rasha or Eckles.

12
bin/keypairs.js Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env node
'use strict';
var cmd = "npm install --global keypairs-cli";
console.error(cmd);
require('child_process').exec(cmd, function (err) {
if (err) {
console.error(err);
return;
}
console.info("Run 'keypairs help' to see what you can do!");
});

33
example.js Normal file
View File

@ -0,0 +1,33 @@
'use strict';
var Keypairs = require('./keypairs.js');
var Keyfetch = require('keyfetch');
Keypairs.generate().then(function (keypair) {
return Keypairs.thumbprint({ jwk: keypair.public }).then(function (thumb) {
var iss = 'https://coolaj86.com/';
// shim so that no http request is necessary
keypair.private.kid = thumb;
Keyfetch._setCache(iss, { thumbprint: thumb, jwk: keypair.private });
return Keypairs.signJwt({
jwk: keypair.private
, claims: {
iss: iss
, sub: 'coolaj86@gmail.com'
, exp: Math.round(Date.now()/1000) + (3 * 24 * 60 * 60)
}
});
});
}).then(function (jwt) {
console.log(jwt);
return Keyfetch.verify({ jwt: jwt }).then(function (ok) {
if (!ok) {
throw new Error("SANITY: did not verify (should have failed)");
}
console.log("Verified token");
});
}).catch(function (err) {
console.error(err);
});

View File

@ -2,22 +2,96 @@
var Eckles = require('eckles'); var Eckles = require('eckles');
var Rasha = require('rasha'); var Rasha = require('rasha');
var Keypairs = {}; var Enc = {};
var Keypairs = module.exports;
/*global Promise*/ /*global Promise*/
Keypairs.generate = function (opts) { Keypairs.generate = function (opts) {
opts = opts || {}; opts = opts || {};
var kty = opts.kty || opts.type; var kty = opts.kty || opts.type;
var p;
if ('RSA' === kty) { if ('RSA' === kty) {
return Rasha.generate(opts); p = Rasha.generate(opts);
} else {
p = Eckles.generate(opts);
} }
return Eckles.generate(opts); return p.then(function (pair) {
return Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
pair.private.kid = thumb; // maybe not the same id on the private key?
pair.public.kid = thumb;
return pair;
});
});
};
Keypairs.parse = function (opts) {
opts = opts || {};
var err;
var jwk;
var pem;
var p;
if (!opts.key || !opts.key.kty) {
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);
});
}
} else {
p = Promise.resolve(opts.key);
}
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) { Keypairs.import = function (opts) {
return Eckles.import(opts.pem).catch(function () { return Eckles.import(opts).catch(function () {
return Rasha.import(opts.pem); return Rasha.import(opts);
}).then(function (jwk) {
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
jwk.kid = thumb;
return jwk;
});
}); });
}; };
@ -31,6 +105,39 @@ Keypairs.export = function (opts) {
}); });
}; };
// Chopping off the private parts is now part of the public API.
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
Keypairs.neuter = Keypairs._neuter = function (opts) {
// trying to find the best balance of an immutable copy with custom attributes
var jwk = {};
Object.keys(opts.jwk).forEach(function (k) {
if ('undefined' === typeof opts.jwk[k]) { return; }
// 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]));
});
return jwk;
};
Keypairs.publish = function (opts) {
if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); }
// returns a copy
var jwk = Keypairs.neuter(opts);
if (jwk.exp) {
jwk.exp = setTime(jwk.exp);
} else {
if (opts.exp) { jwk.exp = setTime(opts.exp); }
else if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; }
else if (opts.expiresAt) { 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) { Keypairs.thumbprint = function (opts) {
return Promise.resolve().then(function () { return Promise.resolve().then(function () {
if ('RSA' === opts.jwk.kty) { if ('RSA' === opts.jwk.kty) {
@ -40,3 +147,220 @@ Keypairs.thumbprint = function (opts) {
} }
}); });
}; };
// JWT a.k.a. JWS with Claims using Compact Serialization
Keypairs.signJwt = function (opts) {
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function (thumb) {
var header = opts.header || {};
var claims = JSON.parse(JSON.stringify(opts.claims || {}));
header.typ = 'JWT';
if (!header.kid) { header.kid = thumb; }
if (!header.alg && opts.alg) { header.alg = opts.alg; }
if (!claims.iat && (false === claims.iat || false === opts.iat)) {
claims.iat = undefined;
} else if (!claims.iat) {
claims.iat = Math.round(Date.now()/1000);
}
if (opts.exp) {
claims.exp = setTime(opts.exp);
} else if (!claims.exp && (false === claims.exp || false === opts.exp)) {
claims.exp = undefined;
} else if (!claims.exp) {
throw new Error("opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false");
}
if (opts.iss) { claims.iss = opts.iss; }
if (!claims.iss && (false === claims.iss || false === opts.iss)) {
claims.iss = undefined;
} else if (!claims.iss) {
throw new Error("opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url");
}
return Keypairs.signJws({
jwk: opts.jwk
, pem: opts.pem
, protected: header
, header: undefined
, payload: claims
}).then(function (jws) {
return [ jws.protected, jws.payload, jws.signature ].join('.');
});
});
};
Keypairs.signJws = function (opts) {
return Keypairs.thumbprint(opts).then(function (thumb) {
function alg() {
if (!opts.jwk) {
throw new Error("opts.jwk must exist and must declare 'typ'");
}
return ('RSA' === opts.jwk.kty) ? "RS256" : "ES256";
}
function sign(pem) {
var header = opts.header;
var protect = opts.protected;
var payload = opts.payload;
// Compute JWS signature
var protectedHeader = "";
// Because unprotected headers are allowed, regrettably...
// https://stackoverflow.com/a/46288694
if (false !== protect) {
if (!protect) { protect = {}; }
if (!protect.alg) { protect.alg = alg(); }
// There's a particular request where Let's Encrypt explicitly doesn't use a kid
if (!protect.kid && false !== protect.kid) { protect.kid = thumb; }
protectedHeader = JSON.stringify(protect);
}
// Convert payload to Buffer
if ('string' !== typeof payload && !Buffer.isBuffer(payload)) {
if (!payload) {
throw new Error("opts.payload should be JSON, string, or Buffer (it may be empty, but that must be explicit)");
}
payload = JSON.stringify(payload);
}
if ('string' === typeof payload) {
payload = Buffer.from(payload, 'binary');
}
// node specifies RSA-SHAxxx even whet it's actually ecdsa (it's all encoded x509 shasums anyway)
var nodeAlg = "SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256');
var protected64 = Enc.strToUrlBase64(protectedHeader);
var payload64 = Enc.bufToUrlBase64(payload);
var binsig = require('crypto')
.createSign(nodeAlg)
.update(protect ? (protected64 + "." + payload64) : payload64)
.sign(pem)
;
if ('EC' === opts.jwk.kty) {
// ECDSA JWT signatures differ from "normal" ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
binsig = ecdsaAsn1SigToJoseSig(binsig);
}
var sig = binsig.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
;
return {
header: header
, protected: protected64 || undefined
, payload: payload64
, signature: sig
};
}
function ecdsaAsn1SigToJoseSig(binsig) {
// should have asn1 sequence header of 0x30
if (0x30 !== binsig[0]) { throw new Error("Impossible EC SHA head marker"); }
var index = 2; // first ecdsa "R" header byte
var len = binsig[1];
var lenlen = 0;
// Seek length of length if length is greater than 127 (i.e. two 512-bit / 64-byte R and S values)
if (0x80 & len) {
lenlen = len - 0x80; // should be exactly 1
len = binsig[2]; // should be <= 130 (two 64-bit SHA-512s, plus padding)
index += lenlen;
}
// should be of BigInt type
if (0x02 !== binsig[index]) { throw new Error("Impossible EC SHA R marker"); }
index += 1;
var rlen = binsig[index];
var bits = 32;
if (rlen > 49) {
bits = 64;
} else if (rlen > 33) {
bits = 48;
}
var r = binsig.slice(index + 1, index + 1 + rlen).toString('hex');
var slen = binsig[index + 1 + rlen + 1]; // skip header and read length
var s = binsig.slice(index + 1 + rlen + 1 + 1).toString('hex');
if (2 *slen !== s.length) { throw new Error("Impossible EC SHA S length"); }
// There may be one byte of padding on either
while (r.length < 2*bits) { r = '00' + r; }
while (s.length < 2*bits) { s = '00' + s; }
if (2*(bits+1) === r.length) { r = r.slice(2); }
if (2*(bits+1) === s.length) { s = s.slice(2); }
return Buffer.concat([Buffer.from(r, 'hex'), Buffer.from(s, 'hex')]);
}
if (opts.pem && opts.jwk) {
return sign(opts.pem);
} else {
return Keypairs.export({ jwk: opts.jwk }).then(sign);
}
});
};
function setTime(time) {
if ('number' === typeof time) { return time; }
var t = time.match(/^(\-?\d+)([dhms])$/i);
if (!t || !t[0]) {
throw new Error("'" + time + "' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s");
}
var now = Math.round(Date.now()/1000);
var num = parseInt(t[1], 10);
var unit = t[2];
var mult = 1;
switch(unit) {
// fancy fallthrough, what fun!
case 'd':
mult *= 24;
/*falls through*/
case 'h':
mult *= 60;
/*falls through*/
case 'm':
mult *= 60;
/*falls through*/
case 's':
mult *= 1;
}
return now + (mult * num);
}
Enc.strToUrlBase64 = function (str) {
// node automatically can tell the difference
// between uc2 (utf-8) strings and binary strings
// so we don't have to re-encode the strings
return Buffer.from(str).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
Enc.bufToUrlBase64 = function (buf) {
// allow for Uint8Array as a Buffer
return Buffer.from(buf).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
// For 'rsa-compat' module only
// PLEASE do not use these sync methods, they are deprecated
Keypairs._importSync = function (opts) {
try {
return Eckles.importSync(opts);
} catch(e) {
try {
return Rasha.importSync(opts);
} catch(e) {
console.error("options.pem does not appear to be a valid RSA or ECDSA public or private key");
}
}
};
// PLEASE do not use these, they are deprecated
Keypairs._exportSync = function (opts) {
if ('RSA' === opts.jwk.kty) {
return Rasha.exportSync(opts);
} else {
return Eckles.exportSync(opts);
}
};

14
package-lock.json generated
View File

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

View File

@ -1,11 +1,16 @@
{ {
"name": "keypairs", "name": "keypairs",
"version": "1.0.0", "version": "1.2.14",
"description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM", "description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM using node's native RSA and ECDSA support",
"main": "keypairs.js", "main": "keypairs.js",
"files": [], "files": [
"bin/keypairs.js"
],
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "node test.js"
},
"bin": {
"keypairs-install": "bin/keypairs.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -16,12 +21,16 @@
"RSA", "RSA",
"ECDSA", "ECDSA",
"PEM", "PEM",
"JWK" "JWK",
"keypair",
"crypto",
"sign",
"verify"
], ],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"eckles": "^1.4.0", "eckles": "^1.4.1",
"rasha": "^1.2.1" "rasha": "^1.2.4"
} }
} }

121
test.js Normal file
View File

@ -0,0 +1,121 @@
var Keypairs = require('./');
/* global Promise*/
console.info("This SHOULD result in an error message:");
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;
})
, Keypairs.signJwt({ jwk: pair.private, alg: 'ES512', iss: 'https://example.com/', exp: '1h' }).then(function (jwt) {
var parts = jwt.split('.');
var now = Math.round(Date.now()/1000);
var token = {
header: JSON.parse(Buffer.from(parts[0], 'base64'))
, payload: JSON.parse(Buffer.from(parts[1], 'base64'))
, signature: parts[2] //Buffer.from(parts[2], 'base64')
};
// allow some leeway just in case we happen to hit a 1ms boundary
if (token.payload.exp - now > 60 * 59.99) {
return true;
}
throw new Error("token was not properly generated");
})
]).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);
});