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

245
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).
@ -11,69 +11,242 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
* [x] Generate keypairs
* [x] RSA
* [x] ECDSA (P-256, P-384)
* [x] PEM-to-JWK
* [x] JWK-to-PEM
* [x] PEM-to-JWK (and SSH-to-JWK)
* [x] JWK-to-PEM (and JWK-to-SSH)
* [x] Create JWTs (and sign JWS)
* [x] SHA256 JWK Thumbprints
* [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/)
* [ ] OIDC
* [ ] Auth0
* [ ] CLI
* See [keypairs-cli](https://npmjs.com/packages/keypairs-cli/)
<!--
* [ ] sign JWS
* [ ] generate CSR (DER as PEM or base64url)
-->
# Usage
A brief (albeit somewhat nonsensical) introduction to the APIs:
A brief introduction to the APIs:
```
Keypairs.generate().then(function (jwk) {
return Keypairs.export({ jwk: jwk }).then(function (pem) {
return Keypairs.import({ pem: pem }).then(function (jwk) {
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
console.log(thumb);
});
});
});
// generate a new keypair as jwk
// (defaults to EC P-256 when no options are specified)
Keypairs.generate().then(function (pair) {
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);
});
```
```
// 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
_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)`
* 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 })`
#### Keypairs.generate(options)
<!--
Generates a public/private pair of JWKs as `{ private, public }`
* `Keypairs.jws.sign(options)`
* options example `{ keypair, header, protected, payload }`
* `Keypairs.csr.generate(options)`
* options example `{ keypair, [ 'example.com' ] }`
Option examples:
-->
* 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 Rasha = require('rasha');
var Keypairs = {};
var Enc = {};
var Keypairs = module.exports;
/*global Promise*/
Keypairs.generate = function (opts) {
opts = opts || {};
var kty = opts.kty || opts.type;
var p;
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) {
return Eckles.import(opts.pem).catch(function () {
return Rasha.import(opts.pem);
return Eckles.import(opts).catch(function () {
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) {
return Promise.resolve().then(function () {
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",
"version": "1.0.0",
"version": "1.2.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=="
}
}
}

View File

@ -1,11 +1,16 @@
{
"name": "keypairs",
"version": "1.0.0",
"description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM",
"version": "1.2.14",
"description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM using node's native RSA and ECDSA support",
"main": "keypairs.js",
"files": [],
"files": [
"bin/keypairs.js"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node test.js"
},
"bin": {
"keypairs-install": "bin/keypairs.js"
},
"repository": {
"type": "git",
@ -16,12 +21,16 @@
"RSA",
"ECDSA",
"PEM",
"JWK"
"JWK",
"keypair",
"crypto",
"sign",
"verify"
],
"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"
}
}

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);
});