Browse Source

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

tags/v1.1.0
AJ ONeal 8 months 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 @@
1
-# Keypairs for node.js
1
+# Keypairs.js
2 2
 
3 3
 Lightweight JavaScript RSA and ECDSA utils that work on Windows, Mac, and Linux
4 4
 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/).
13 13
     * [x] ECDSA (P-256, P-384)
14 14
   * [x] PEM-to-JWK
15 15
   * [x] JWK-to-PEM
16
+  * [x] Create JWTs (and sign JWS)
16 17
   * [x] SHA256 JWK Thumbprints
17 18
   * [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/)
18 19
     * [ ] OIDC
@@ -20,7 +21,6 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
20 21
 
21 22
 <!--
22 23
 
23
-  * [ ] sign JWS
24 24
   * [ ] generate CSR (DER as PEM or base64url)
25 25
 
26 26
 -->
@@ -54,6 +54,16 @@ _much_ longer than RSA has, and they're smaller, and faster to generate.
54 54
 
55 55
 ## API Overview
56 56
 
57
+* generate
58
+* parse
59
+* parseOrGenerate
60
+* import (PEM-to-JWK)
61
+* export (JWK-to-PEM, private or public)
62
+* publish (Private JWK to Public JWK)
63
+* thumbprint (JWK SHA256)
64
+* signJwt
65
+* signJws
66
+
57 67
 #### Keypairs.generate(options)
58 68
 
59 69
 Generates a public/private pair of JWKs as `{ private, public }`
@@ -65,6 +75,50 @@ Option examples:
65 75
 
66 76
 When no options are supplied EC P-256 (also known as `prime256v1` and `secp256r1`) is used by default.
67 77
 
78
+#### Keypairs.parse(options)
79
+
80
+Parses either a JWK (encoded as JSON) or an x509 (encdode as PEM) and gives
81
+back the JWK representation.
82
+
83
+Option Examples:
84
+
85
+* JWK { key: '{ "kty":"EC", ... }' }
86
+* PEM { key: '-----BEGIN PRIVATE KEY-----\n...' }
87
+* Public Key Only { key: '-----BEGIN PRIVATE KEY-----\n...', public: true }
88
+* Must Have Private Key { key: '-----BEGIN PUBLIC KEY-----\n...', private: true }
89
+
90
+Example:
91
+
92
+```js
93
+Keypairs.parse({ key: '...' }).catch(function (e) {
94
+  // could not be parsed or was a public key
95
+  console.warn(e);
96
+  return Keypairs.generate();
97
+});
98
+```
99
+
100
+#### Keypairs.parseOrGenerate({ key, throw, [generate opts]... })
101
+
102
+Parses the key. Logs a warning on failure, marches on.
103
+(a shortcut for the above, with `private: true`)
104
+
105
+Option Examples:
106
+
107
+* parse key if exist, otherwise generate `{ key: process.env["PRIVATE_KEY"] }`
108
+* generated key curve `{ key: null, namedCurve: 'P-256' }`
109
+* generated key modulus `{ key: null, modulusLength: 2048 }`
110
+
111
+Example:
112
+
113
+```js
114
+Keypairs.parseOrGenerate({ key: process.env["PRIVATE_KEY"] }).then(function (pair) {
115
+  console.log(pair.public);
116
+})
117
+```
118
+
119
+Great for when you have a set of shared keys for development and randomly
120
+generated keys in
121
+
68 122
 #### Keypairs.import({ pem: '...' }
69 123
 
70 124
 Takes a PEM in pretty much any format (PKCS1, SEC1, PKCS8, SPKI) and returns a JWK.
@@ -85,6 +139,10 @@ Options
85 139
 }
86 140
 ```
87 141
 
142
+#### Keypairs.publish({ jwk: jwk })
143
+
144
+**Synchronously** strips a key of its private parts and returns the public version.
145
+
88 146
 #### Keypairs.thumbprint({ jwk: jwk })
89 147
 
90 148
 Promises a JWK-spec thumbprint: URL Base64-encoded sha256
@@ -134,11 +192,15 @@ Options:
134 192
 
135 193
 # Additional Documentation
136 194
 
137
-Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs,
138
-but it also includes the additional convenience methods `signJwt` and `signJws`.
195
+Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs for the following:
196
+
197
+* generate(options)
198
+* import({ pem: '---BEGIN...' })
199
+* export({ jwk: { kty: 'EC', ... })
200
+* thumbprint({ jwk: jwk })
139 201
 
140
-That is to say that any option you pass to Keypairs will be passed directly to the corresponding API
141
-of either Rasha or Eckles.
202
+If you want to know the algorithm-specific options that are available for those
203
+you'll want to take a look at the corresponding documentation:
142 204
 
143 205
 * See ECDSA documentation at [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js/)
144 206
 * 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) {
16 16
   return Eckles.generate(opts);
17 17
 };
18 18
 
19
+Keypairs.parse = function (opts) {
20
+  opts = opts || {};
21
+
22
+  var err;
23
+  var jwk;
24
+  var pem;
25
+  var p;
26
+
27
+  try {
28
+    jwk = JSON.parse(opts.key);
29
+    p = Keypairs.export({ jwk: jwk }).catch(function (e) {
30
+      pem = opts.key;
31
+      err = new Error("Not a valid jwk '" + JSON.stringify(jwk) + "':" + e.message);
32
+      err.code = "EINVALID";
33
+      return Promise.reject(err);
34
+    }).then(function () {
35
+      return jwk;
36
+    });
37
+  } catch(e) {
38
+    p = Keypairs.import({ pem: opts.key }).catch(function (e) {
39
+      err = new Error("Could not parse key (type " + typeof opts.key + ") '" + opts.key + "': " + e.message);
40
+      err.code = "EPARSE";
41
+      return Promise.reject(err);
42
+    });
43
+  }
44
+
45
+  return p.then(function (jwk) {
46
+    var pubopts = JSON.parse(JSON.stringify(opts));
47
+    pubopts.jwk = jwk;
48
+    return Keypairs.publish(pubopts).then(function (pub) {
49
+      // 'd' happens to be the name of a private part of both RSA and ECDSA keys
50
+      if (opts.public || opts.publish || !jwk.d) {
51
+        if (opts.private) {
52
+          // TODO test that it can actually sign?
53
+          err = new Error("Not a private key '" + JSON.stringify(jwk) + "'");
54
+          err.code = "ENOTPRIVATE";
55
+          return Promise.reject(err);
56
+        }
57
+        return { public: pub };
58
+      } else {
59
+        return { private: jwk, public: pub };
60
+      }
61
+    });
62
+  });
63
+};
64
+
65
+Keypairs.parseOrGenerate = function (opts) {
66
+  if (!opts.key) { return Keypairs.generate(opts); }
67
+  opts.private = true;
68
+  return Keypairs.parse(opts).catch(function (e) {
69
+    console.warn(e.message);
70
+    return Keypairs.generate(opts);
71
+  });
72
+};
73
+
19 74
 Keypairs.import = function (opts) {
20
-  return Eckles.import(opts.pem).catch(function () {
21
-    return Rasha.import(opts.pem);
75
+  return Eckles.import(opts).catch(function () {
76
+    return Rasha.import(opts);
22 77
   });
23 78
 };
24 79
 
@@ -32,6 +87,27 @@ Keypairs.export = function (opts) {
32 87
   });
33 88
 };
34 89
 
90
+Keypairs.publish = function (opts) {
91
+  if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); }
92
+
93
+  // trying to find the best balance of an immutable copy with custom attributes
94
+  var jwk = {};
95
+  Object.keys(opts.jwk).forEach(function (k) {
96
+    // ignore RSA and EC private parts
97
+    if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; }
98
+    jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
99
+  });
100
+
101
+  if (!jwk.exp) {
102
+    if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; }
103
+    else { jwk.exp = opts.expiresAt; }
104
+  }
105
+  if (!jwk.use && false !== jwk.use) { jwk.use = "sig"; }
106
+
107
+  if (jwk.kid) { return Promise.resolve(jwk); }
108
+  return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { jwk.kid = thumb; return jwk; });
109
+};
110
+
35 111
 Keypairs.thumbprint = function (opts) {
36 112
   return Promise.resolve().then(function () {
37 113
     if ('RSA' === opts.jwk.kty) {

+ 7
- 7
package-lock.json View File

@@ -1,18 +1,18 @@
1 1
 {
2 2
   "name": "keypairs",
3
-  "version": "1.0.1",
3
+  "version": "1.1.0",
4 4
   "lockfileVersion": 1,
5 5
   "requires": true,
6 6
   "dependencies": {
7 7
     "eckles": {
8
-      "version": "1.4.0",
9
-      "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.0.tgz",
10
-      "integrity": "sha512-Bm5dpwhsBuoCHvKCY3gAvP8XFyXH7im8uAu3szykpVNbFBdC+lOuV8vLC8fvTYRZBfFqB+k/P6ud/ZPVO2V2tA=="
8
+      "version": "1.4.1",
9
+      "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
10
+      "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
11 11
     },
12 12
     "rasha": {
13
-      "version": "1.2.1",
14
-      "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.1.tgz",
15
-      "integrity": "sha512-cs4Hu/rVF3/Qucq+V7lxSz449VfHNMVXJaeajAHno9H5FC1PWlmS4NM6IAX5jPKFF0IC2rOdHdf7iNxQuIWZag=="
13
+      "version": "1.2.4",
14
+      "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
15
+      "integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q=="
16 16
     }
17 17
   }
18 18
 }

+ 4
- 4
package.json View File

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

+ 106
- 0
test.js View File

@@ -0,0 +1,106 @@
1
+var Keypairs = require('./');
2
+
3
+/* global Promise*/
4
+Keypairs.parseOrGenerate({ key: '' }).then(function (pair) {
5
+  // should NOT have any warning output
6
+  if (!pair.private || !pair.public) {
7
+    throw new Error("missing key pairs");
8
+  }
9
+
10
+  return Promise.all([
11
+    // Testing Public Part of key
12
+    Keypairs.export({ jwk: pair.public }).then(function (pem) {
13
+      if (!/--BEGIN PUBLIC/.test(pem)) {
14
+        throw new Error("did not export public pem");
15
+      }
16
+      return Promise.all([
17
+        Keypairs.parse({ key: pem }).then(function (pair) {
18
+          if (pair.private) {
19
+            throw new Error("shouldn't have private part");
20
+          }
21
+          return true;
22
+        })
23
+      , Keypairs.parse({ key: pem, private: true }).then(function () {
24
+          var err = new Error("should have thrown an error when private key was required and public pem was given");
25
+          err.code = 'NOERR';
26
+          throw err;
27
+        }).catch(function (e) {
28
+          if ('NOERR' === e.code) { throw e; }
29
+          return true;
30
+        })
31
+      ]).then(function () {
32
+        return true;
33
+      });
34
+    })
35
+    // Testing Private Part of Key
36
+  , Keypairs.export({ jwk: pair.private }).then(function (pem) {
37
+      if (!/--BEGIN .*PRIVATE KEY--/.test(pem)) {
38
+        throw new Error("did not export private pem: " + pem);
39
+      }
40
+      return Promise.all([
41
+        Keypairs.parse({ key: pem }).then(function (pair) {
42
+          if (!pair.private) {
43
+            throw new Error("should have private part");
44
+          }
45
+          if (!pair.public) {
46
+            throw new Error("should have public part also");
47
+          }
48
+          return true;
49
+        })
50
+      , Keypairs.parse({ key: pem, public: true }).then(function (pair) {
51
+          if (pair.private) {
52
+            throw new Error("should NOT have private part");
53
+          }
54
+          if (!pair.public) {
55
+            throw new Error("should have the public part though");
56
+          }
57
+          return true;
58
+        })
59
+      ]).then(function () {
60
+        return true;
61
+      });
62
+    })
63
+  , Keypairs.parseOrGenerate({ key: 'not a key', public: true }).then(function (pair) {
64
+      // SHOULD have warning output
65
+      if (!pair.private || !pair.public) {
66
+        throw new Error("missing key pairs (should ignore 'public')");
67
+      }
68
+      return true;
69
+    })
70
+  , Keypairs.parse({ key: JSON.stringify(pair.private) }).then(function (pair) {
71
+      if (!pair.private || !pair.public) {
72
+        throw new Error("missing key pairs (stringified jwt)");
73
+      }
74
+      return true;
75
+    })
76
+  , Keypairs.parse({ key: JSON.stringify(pair.private), public: true }).then(function (pair) {
77
+      if (pair.private) {
78
+        throw new Error("has private key when it shouldn't");
79
+      }
80
+      if (!pair.public) {
81
+        throw new Error("doesn't have public key when it should");
82
+      }
83
+      return true;
84
+    })
85
+  , Keypairs.parse({ key: JSON.stringify(pair.public), private: true }).then(function () {
86
+      var err = new Error("should have thrown an error when private key was required and public jwk was given");
87
+      err.code = 'NOERR';
88
+      throw err;
89
+    }).catch(function (e) {
90
+      if ('NOERR' === e.code) { throw e; }
91
+      return true;
92
+    })
93
+  ]).then(function (results) {
94
+    if (results.length && results.every(function (v) { return true === v; })) {
95
+      console.info("If a warning prints right above this, it's a pass");
96
+      console.log("PASS");
97
+      process.exit(0);
98
+    } else {
99
+      throw new Error("didn't get all passes (but no errors either)");
100
+    }
101
+  });
102
+}).catch(function (e) {
103
+  console.error("Caught an unexpected (failing) error:");
104
+  console.error(e);
105
+  process.exit(1);
106
+});

Loading…
Cancel
Save