Compare commits

...

13 Commits
v0.9.0 ... main

23 changed files with 243 additions and 168 deletions

6
.jshintrc Normal file
View File

@ -0,0 +1,6 @@
{
"esversion": 11,
"node": true,
"unused": true,
"curly": true
}

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"bracketSpacing": true,
"printWidth": 80,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": true
}

View File

@ -1,4 +1,4 @@
# @root/keypairs
# [@root/keypairs](https://git.rootprojects.org/root/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).
@ -20,6 +20,8 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
- [ ] Auth0
- [ ] CLI
- See [keypairs-cli](https://npmjs.com/packages/keypairs-cli/)
- [x] Node
- [x] Browsers (Webpack >=5)
<!--
@ -40,7 +42,7 @@ A brief introduction to the APIs:
```js
// generate a new keypair as jwk
// (defaults to EC P-256 when no options are specified)
Keypairs.generate().then(function(pair) {
Keypairs.generate().then(function (pair) {
console.log(pair.private);
console.log(pair.public);
});
@ -49,7 +51,7 @@ Keypairs.generate().then(function(pair) {
```js
// JWK to PEM
// (supports various 'format' and 'encoding' options)
return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function(
return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function (
pem
) {
console.log(pem);
@ -58,14 +60,14 @@ return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function(
```js
// PEM to JWK
return Keypairs.import({ pem: pem }).then(function(jwk) {
return Keypairs.import({ pem: pem }).then(function (jwk) {
console.log(jwk);
});
```
```js
// Thumbprint a JWK (SHA256)
return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) {
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
console.log(thumb);
});
```
@ -86,6 +88,72 @@ return Keypairs.signJwt({
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.
## Webpack 5+ (for Browsers)
This package includes native browser versions of all special functions.
Since Webpack 5 now fully supports vanilla JavaScript and exclusive browser builds out-of-the-box,
it's pretty easy to create a minimal config to use Keypairs in your browser projects:
`webpack.config.js`:
```js
'use strict';
var path = require('path');
module.exports = {
entry: './main.js',
mode: 'development',
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 3001
},
output: {
publicPath: 'http://localhost:3001/'
},
module: {
rules: [{}]
},
plugins: []
};
```
`main.js`:
```js
'use strict';
var Keypairs = require('./keypairs.js');
Keypairs.generate().then(function (pair) {
console.log(pair.private);
console.log(pair.public);
});
```
`index.html`:
```html
<!DOCTYPE html>
<html>
<body>
<script src="./dist/main.js"></script>
</body>
</html>
```
```bash
npm install --save-dev webpack@5
# Or, if webpack 5 is still in beta: npm install --save-dev webpack@next
npm install --save-dev webpack-cli
npx webpack --mode=production
ls dist/main.js
```
## API Overview
- generate (JWK)
@ -124,7 +192,7 @@ Option Examples:
Example:
```js
Keypairs.parse({ key: '...' }).catch(function(e) {
Keypairs.parse({ key: '...' }).catch(function (e) {
// could not be parsed or was a public key
console.warn(e);
return Keypairs.generate();
@ -145,7 +213,7 @@ Option Examples:
Example:
```js
Keypairs.parseOrGenerate({ key: process.env['PRIVATE_KEY'] }).then(function(
Keypairs.parseOrGenerate({ key: process.env['PRIVATE_KEY'] }).then(function (
pair
) {
console.log(pair.public);

View File

@ -23,7 +23,7 @@ if (
namedCurve: format === 'P-384' ? 'P-384' : 'P-256',
encoding: format === 'der' ? 'der' : 'pem'
})
.then(function(key) {
.then(function (key) {
if ('der' === infile || 'der' === format) {
key.private = key.private.toString('binary');
key.public = key.public.toString('binary');
@ -31,7 +31,7 @@ if (
console.log(key.private);
console.log(key.public);
})
.catch(function(err) {
.catch(function (err) {
console.error(err);
process.exit(1);
});
@ -58,10 +58,10 @@ if ('string' === typeof key) {
}
var pub = -1 !== ['public', 'spki', 'pkix'].indexOf(format);
Eckles.import({ pem: key, public: pub || format })
.then(function(jwk) {
.then(function (jwk) {
console.log(JSON.stringify(jwk, null, 2));
})
.catch(function(err) {
.catch(function (err) {
console.error(err);
process.exit(1);
});
@ -71,10 +71,10 @@ if ('string' === typeof key) {
return;
}
Eckles.export({ jwk: key, format: format })
.then(function(pem) {
.then(function (pem) {
console.log(pem);
})
.catch(function(err) {
.catch(function (err) {
console.error(err);
process.exit(2);
});

View File

@ -29,7 +29,7 @@ if (
modulusLength: parseInt(format, 10) || 2048,
encoding: parseInt(format, 10) ? null : format
})
.then(function(key) {
.then(function (key) {
if ('der' === infile || 'der' === format) {
key.private = key.private.toString('binary');
key.public = key.public.toString('binary');
@ -37,7 +37,7 @@ if (
console.info(key.private);
console.info(key.public);
})
.catch(function(err) {
.catch(function (err) {
console.error(err);
process.exit(1);
});
@ -74,10 +74,10 @@ if ('string' === typeof key) {
var pub = -1 !== ['public', 'spki', 'pkix'].indexOf(format);
Rasha.import({ pem: key, public: pub || format })
.then(function(jwk) {
.then(function (jwk) {
console.info(JSON.stringify(jwk, null, 2));
})
.catch(function(err) {
.catch(function (err) {
console.error(err);
process.exit(1);
});
@ -87,14 +87,14 @@ if ('string' === typeof key) {
return;
}
Rasha.export({ jwk: key, format: format })
.then(function(pem) {
.then(function (pem) {
if (sign) {
signMessage(pem, msg);
return;
}
console.info(pem);
})
.catch(function(err) {
.catch(function (err) {
console.error(err);
process.exit(2);
});

View File

@ -29,8 +29,8 @@ EC._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
EC.generate = native.generate;
EC.export = function(opts) {
return Promise.resolve().then(function() {
EC.export = function (opts) {
return Promise.resolve().then(function () {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error('must pass { jwk: jwk } as a JSON object');
}
@ -109,8 +109,8 @@ EC.export = function(opts) {
};
native.export = EC.export;
EC.import = function(opts) {
return Promise.resolve().then(function() {
EC.import = function (opts) {
return Promise.resolve().then(function () {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error('must pass { pem: pem } as a string');
}
@ -178,18 +178,18 @@ EC.import = function(opts) {
};
native.import = EC.import;
EC.pack = function(opts) {
return Promise.resolve().then(function() {
EC.pack = function (opts) {
return Promise.resolve().then(function () {
return EC.export(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.
EC.neuter = function(opts) {
EC.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) {
Object.keys(opts.jwk).forEach(function (k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
@ -204,7 +204,7 @@ EC.neuter = function(opts) {
native.neuter = EC.neuter;
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
EC.__thumbprint = function(jwk) {
EC.__thumbprint = function (jwk) {
// Use the same entropy for SHA as for key
var alg = 'SHA-256';
if (/384/.test(jwk.crv)) {
@ -218,20 +218,20 @@ EC.__thumbprint = function(jwk) {
'","y":"' +
jwk.y +
'"}';
return sha2.sum(alg, payload).then(function(hash) {
return sha2.sum(alg, payload).then(function (hash) {
return Enc.bufToUrlBase64(Uint8Array.from(hash));
});
};
EC.thumbprint = function(opts) {
return Promise.resolve().then(function() {
EC.thumbprint = function (opts) {
return Promise.resolve().then(function () {
var jwk;
if ('EC' === opts.kty) {
jwk = opts;
} else if (opts.jwk) {
jwk = opts.jwk;
} else {
return native.import(opts).then(function(jwk) {
return native.import(opts).then(function (jwk) {
return EC.__thumbprint(jwk);
});
}

View File

@ -9,7 +9,7 @@ var Rasha = require('./rsa.js');
var Eckles = require('./ecdsa.js');
var native = require('./lib/node/keypairs.js');
Keypairs.parse = function(opts) {
Keypairs.parse = function (opts) {
opts = opts || {};
var err;
@ -21,7 +21,7 @@ Keypairs.parse = function(opts) {
try {
jwk = JSON.parse(opts.key);
p = Keypairs.export({ jwk: jwk })
.catch(function(e) {
.catch(function (e) {
pem = opts.key;
err = new Error(
"Not a valid jwk '" +
@ -32,11 +32,11 @@ Keypairs.parse = function(opts) {
err.code = 'EINVALID';
return Promise.reject(err);
})
.then(function() {
.then(function () {
return jwk;
});
} catch (e) {
p = Keypairs.import({ pem: opts.key }).catch(function(e) {
p = Keypairs.import({ pem: opts.key }).catch(function (e) {
err = new Error(
'Could not parse key (type ' +
typeof opts.key +
@ -53,10 +53,10 @@ Keypairs.parse = function(opts) {
p = Promise.resolve(opts.key);
}
return p.then(function(jwk) {
return p.then(function (jwk) {
var pubopts = JSON.parse(JSON.stringify(opts));
pubopts.jwk = jwk;
return Keypairs.publish(pubopts).then(function(pub) {
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) {
@ -75,13 +75,13 @@ Keypairs.parse = function(opts) {
});
};
Keypairs.parseOrGenerate = function(opts) {
Keypairs.parseOrGenerate = function (opts) {
if (!opts.key) {
return Keypairs.generate(opts);
}
opts.private = true;
return Keypairs.parse(opts).catch(function(e) {
return Keypairs.generate(opts).then(function(pair) {
return Keypairs.parse(opts).catch(function (e) {
return Keypairs.generate(opts).then(function (pair) {
pair.parseError = e;
return pair;
});
@ -93,7 +93,7 @@ Keypairs._stance =
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
Keypairs._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
Keypairs.generate = function(opts) {
Keypairs.generate = function (opts) {
opts = opts || {};
var p;
if (!opts.kty) {
@ -107,7 +107,7 @@ Keypairs.generate = function(opts) {
} else if (/^RSA$/i.test(opts.kty)) {
p = Rasha.generate(opts);
} else {
return Promise.Reject(
return Promise.reject(
new Error(
"'" +
opts.kty +
@ -117,8 +117,8 @@ Keypairs.generate = function(opts) {
)
);
}
return p.then(function(pair) {
return Keypairs.thumbprint({ jwk: pair.public }).then(function(thumb) {
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;
@ -126,22 +126,22 @@ Keypairs.generate = function(opts) {
});
};
Keypairs.import = function(opts) {
Keypairs.import = function (opts) {
return Eckles.import(opts)
.catch(function() {
.catch(function () {
return Rasha.import(opts);
})
.then(function(jwk) {
return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) {
.then(function (jwk) {
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
jwk.kid = thumb;
return jwk;
});
});
};
Keypairs.export = function(opts) {
return Eckles.export(opts).catch(function(err) {
return Rasha.export(opts).catch(function() {
Keypairs.export = function (opts) {
return Eckles.export(opts).catch(function (err) {
return Rasha.export(opts).catch(function () {
return Promise.reject(err);
});
});
@ -153,10 +153,10 @@ native.export = Keypairs.export;
* 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 = function(opts) {
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) {
Object.keys(opts.jwk).forEach(function (k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
@ -169,8 +169,8 @@ Keypairs.neuter = function(opts) {
return jwk;
};
Keypairs.thumbprint = function(opts) {
return Promise.resolve().then(function() {
Keypairs.thumbprint = function (opts) {
return Promise.resolve().then(function () {
if (/EC/i.test(opts.jwk.kty)) {
return Eckles.thumbprint(opts);
} else {
@ -179,7 +179,7 @@ Keypairs.thumbprint = function(opts) {
});
};
Keypairs.publish = function(opts) {
Keypairs.publish = function (opts) {
if ('object' !== typeof opts.jwk || !opts.jwk.kty) {
throw new Error('invalid jwk: ' + JSON.stringify(opts.jwk));
}
@ -205,20 +205,20 @@ Keypairs.publish = function(opts) {
if (jwk.kid) {
return Promise.resolve(jwk);
}
return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) {
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
jwk.kid = thumb;
return jwk;
});
};
// JWT a.k.a. JWS with Claims using Compact Serialization
Keypairs.signJwt = function(opts) {
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
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 && false !== header.kid) {
if (!header.kid && !header.jwk && false !== header.kid) {
header.kid = thumb;
}
if (!header.alg && opts.alg) {
@ -260,14 +260,14 @@ Keypairs.signJwt = function(opts) {
protected: header,
header: undefined,
payload: claims
}).then(function(jws) {
}).then(function (jws) {
return [jws.protected, jws.payload, jws.signature].join('.');
});
});
};
Keypairs.signJws = function(opts) {
return Keypairs.thumbprint(opts).then(function(thumb) {
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'");
@ -294,19 +294,19 @@ Keypairs.signJws = function(opts) {
if (!protect.alg) {
protect.alg = alg();
}
// There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
if (false === protect.kid) {
protect.kid = undefined;
} else if (!protect.kid) {
protect.kid = thumb;
// There should be a kid unless it's `false` or there's a `jwk` (a self-signed JWS)
if (!protect.kid) {
if (false === protect.kid) {
protect.kid = undefined;
} else if (!protect.jwk) {
protect.kid = thumb;
}
}
protectedHeader = JSON.stringify(protect);
}
// Not sure how to handle the empty case since ACME POST-as-GET must be empty
//if (!payload) {
// throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)");
//}
// Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
if (
payload &&
@ -325,7 +325,7 @@ Keypairs.signJws = function(opts) {
var payload64 = Enc.bufToUrlBase64(payload);
var msg = protected64 + '.' + payload64;
return native._sign(opts, msg).then(function(buf) {
return native._sign(opts, msg).then(function (buf) {
var signedMsg = {
protected: protected64,
payload: payload64,
@ -339,7 +339,7 @@ Keypairs.signJws = function(opts) {
if (opts.jwk) {
return sign();
} else {
return Keypairs.import({ pem: opts.pem }).then(function(pair) {
return Keypairs.import({ pem: opts.pem }).then(function (pair) {
opts.jwk = pair.private;
return sign();
});
@ -350,7 +350,7 @@ Keypairs.signJws = function(opts) {
// TODO expose consistently
Keypairs.sign = native._sign;
Keypairs._getBits = function(opts) {
Keypairs._getBits = function (opts) {
if (opts.alg) {
return opts.alg.replace(/[a-z\-]/gi, '');
}

View File

@ -4,7 +4,7 @@ var native = module.exports;
// XXX received from caller
var EC = native;
native.generate = function(opts) {
native.generate = function (opts) {
var wcOpts = {};
if (!opts) {
opts = {};
@ -41,10 +41,10 @@ native.generate = function(opts) {
var extractable = true;
return window.crypto.subtle
.generateKey(wcOpts, extractable, ['sign', 'verify'])
.then(function(result) {
.then(function (result) {
return window.crypto.subtle
.exportKey('jwk', result.privateKey)
.then(function(privJwk) {
.then(function (privJwk) {
privJwk.key_ops = undefined;
privJwk.ext = undefined;
return {

View File

@ -2,8 +2,8 @@
var Keypairs = module.exports;
Keypairs._sign = function(opts, payload) {
return Keypairs._import(opts).then(function(privkey) {
Keypairs._sign = function (opts, payload) {
return Keypairs._import(opts).then(function (privkey) {
if ('string' === typeof payload) {
payload = new TextEncoder().encode(payload);
}
@ -17,7 +17,7 @@ Keypairs._sign = function(opts, payload) {
privkey,
payload
)
.then(function(signature) {
.then(function (signature) {
signature = new Uint8Array(signature); // ArrayBuffer -> u8
// This will come back into play for CSRs, but not for JOSE
if ('EC' === opts.jwk.kty && /x509|asn1/i.test(opts.format)) {
@ -30,8 +30,8 @@ Keypairs._sign = function(opts, payload) {
});
};
Keypairs._import = function(opts) {
return Promise.resolve().then(function() {
Keypairs._import = function (opts) {
return Promise.resolve().then(function () {
var ops;
// all private keys just happen to have a 'd'
if (opts.jwk.d) {
@ -55,7 +55,7 @@ Keypairs._import = function(opts) {
true,
ops
)
.then(function(privkey) {
.then(function (privkey) {
delete opts.jwk.ext;
return privkey;
});
@ -64,7 +64,7 @@ Keypairs._import = function(opts) {
// ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
Keypairs._ecdsaJoseSigToAsn1Sig = function(bufsig) {
Keypairs._ecdsaJoseSigToAsn1Sig = function (bufsig) {
// it's easier to do the manipulation in the browser with an array
bufsig = Array.from(bufsig);
var hlen = bufsig.length / 2; // should be even
@ -99,7 +99,7 @@ Keypairs._ecdsaJoseSigToAsn1Sig = function(bufsig) {
);
};
Keypairs._getName = function(opts) {
Keypairs._getName = function (opts) {
if (/EC/i.test(opts.jwk.kty)) {
return 'ECDSA';
} else {

View File

@ -4,7 +4,7 @@ var native = module.exports;
// XXX added by caller: _stance, neuter
var RSA = native;
native.generate = function(opts) {
native.generate = function (opts) {
var wcOpts = {};
if (!opts) {
opts = {};
@ -46,10 +46,10 @@ native.generate = function(opts) {
var extractable = true;
return window.crypto.subtle
.generateKey(wcOpts, extractable, ['sign', 'verify'])
.then(function(result) {
.then(function (result) {
return window.crypto.subtle
.exportKey('jwk', result.privateKey)
.then(function(privJwk) {
.then(function (privJwk) {
return {
private: privJwk,
public: RSA.neuter({ jwk: privJwk })

View File

@ -3,11 +3,13 @@
var sha2 = module.exports;
var encoder = new TextEncoder();
sha2.sum = function(alg, str) {
sha2.sum = function (alg, str) {
var data = str;
if ('string' === typeof data) {
data = encoder.encode(str);
}
var sha = 'SHA-' + String(alg).replace(/^sha-?/i, '');
return window.crypto.subtle.digest(sha, data);
return window.crypto.subtle.digest(sha, data).then(function (buf) {
return new Uint8Array(buf);
});
};

View File

@ -5,8 +5,8 @@ var native = module.exports;
var EC = native;
// TODO SSH
native.generate = function(opts) {
return Promise.resolve().then(function() {
native.generate = function (opts) {
return Promise.resolve().then(function () {
var typ = 'ec';
var format = opts.format;
var encoding = opts.encoding;
@ -48,7 +48,7 @@ native.generate = function(opts) {
pub = { type: 'spki', format: 'pem' };
}
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
return require('crypto').generateKeyPair(
typ,
{
@ -56,7 +56,7 @@ native.generate = function(opts) {
privateKeyEncoding: priv,
publicKeyEncoding: pub
},
function(err, pubkey, privkey) {
function (err, pubkey, privkey) {
if (err) {
reject(err);
}
@ -66,7 +66,7 @@ native.generate = function(opts) {
});
}
);
}).then(function(keypair) {
}).then(function (keypair) {
if ('jwk' === format) {
return Promise.all([
native.import({
@ -78,7 +78,7 @@ native.generate = function(opts) {
format: pub.type,
public: true
})
]).then(function(pair) {
]).then(function (pair) {
return {
private: pair[0],
public: pair[1]
@ -96,12 +96,12 @@ native.generate = function(opts) {
format: format,
public: true
})
.then(function(jwk) {
.then(function (jwk) {
return EC.export({
jwk: jwk,
format: opts.format,
public: true
}).then(function(pub) {
}).then(function (pub) {
return {
private: keypair.private,
public: pub

View File

@ -4,7 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function(bitlen, exp) {
module.exports = function (bitlen, exp) {
var k = require('node-forge').pki.rsa.generateKeyPair({
bits: bitlen || 2048,
e: exp || 0x10001

View File

@ -4,7 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function(bitlen, exp) {
module.exports = function (bitlen, exp) {
var keypair = require('crypto').generateKeyPairSync('rsa', {
modulusLength: bitlen,
publicExponent: exp,

View File

@ -4,7 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function(bitlen, exp) {
module.exports = function (bitlen, exp) {
var ursa;
try {
ursa = require('ursa');
@ -13,10 +13,7 @@ module.exports = function(bitlen, exp) {
}
var keypair = ursa.generatePrivateKey(bitlen, exp);
var result = {
privateKeyPem: keypair
.toPrivatePem()
.toString('ascii')
.trim()
privateKeyPem: keypair.toPrivatePem().toString('ascii').trim()
};
return result;
};

View File

@ -6,7 +6,7 @@
var oldver = false;
module.exports = function(bitlen, exp) {
module.exports = function (bitlen, exp) {
bitlen = parseInt(bitlen, 10) || 2048;
exp = parseInt(exp, 10) || 65537;

View File

@ -3,17 +3,14 @@
var Keypairs = module.exports;
var crypto = require('crypto');
Keypairs._sign = function(opts, payload) {
return Keypairs._import(opts).then(function(pem) {
Keypairs._sign = function (opts, payload) {
return Keypairs._import(opts).then(function (pem) {
payload = Buffer.from(payload);
// node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
// TODO opts.alg = (protect||header).alg
var nodeAlg = 'SHA' + Keypairs._getBits(opts);
var binsig = crypto
.createSign(nodeAlg)
.update(payload)
.sign(pem);
var binsig = crypto.createSign(nodeAlg).update(payload).sign(pem);
if ('EC' === opts.jwk.kty && !/x509|asn1/i.test(opts.format)) {
// ECDSA JWT signatures differ from "normal" ECDSA signatures
@ -25,7 +22,7 @@ Keypairs._sign = function(opts, payload) {
});
};
Keypairs._import = function(opts) {
Keypairs._import = function (opts) {
if (opts.pem && opts.jwk) {
return Promise.resolve(opts.pem);
} else {
@ -34,7 +31,7 @@ Keypairs._import = function(opts) {
}
};
Keypairs._ecdsaAsn1SigToJoseSig = function(binsig) {
Keypairs._ecdsaAsn1SigToJoseSig = function (binsig) {
// should have asn1 sequence header of 0x30
if (0x30 !== binsig[0]) {
throw new Error('Impossible EC SHA head marker');

View File

@ -7,9 +7,9 @@ var RSA = native;
var PEM = require('@root/pem');
var X509 = require('@root/x509');
native.generate = function(opts) {
native.generate = function (opts) {
opts.kty = 'RSA';
return native._generate(opts).then(function(pair) {
return native._generate(opts).then(function (pair) {
var format = opts.format;
var encoding = opts.encoding;
@ -74,23 +74,23 @@ native.generate = function(opts) {
}
var exOpts = { jwk: pair.private, format: format, encoding: encoding };
return RSA.export(exOpts).then(function(priv) {
return RSA.export(exOpts).then(function (priv) {
exOpts.public = true;
if ('pkcs8' === exOpts.format) {
exOpts.format = 'spki';
}
return RSA.export(exOpts).then(function(pub) {
return RSA.export(exOpts).then(function (pub) {
return { private: priv, public: pub };
});
});
});
};
native._generate = function(opts) {
native._generate = function (opts) {
if (!opts) {
opts = {};
}
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
try {
var modlen = opts.modulusLength || 2048;
var exp = opts.publicExponent || 0x10001;

View File

@ -4,14 +4,11 @@
var sha2 = module.exports;
var crypto = require('crypto');
sha2.sum = function(alg, str) {
return Promise.resolve().then(function() {
sha2.sum = function (alg, str) {
return Promise.resolve().then(function () {
var sha = 'sha' + String(alg).replace(/^sha-?/i, '');
// utf8 is the default for strings
var buf = Buffer.from(str);
return crypto
.createHash(sha)
.update(buf)
.digest();
return crypto.createHash(sha).update(buf).digest();
});
};

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "@root/keypairs",
"version": "0.9.0",
"version": "0.10.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@root/keypairs",
"version": "0.9.0",
"version": "0.10.2",
"description": "Lightweight, Zero-Dependency RSA and EC/ECDSA crypto for Node.js and Browsers",
"main": "keypairs.js",
"browser": {
@ -20,7 +20,7 @@
],
"repository": {
"type": "git",
"url": "https://git.rootprojects.org/root/csr.js.git"
"url": "https://github.com/therootcompany/keypairs.js.git"
},
"keywords": [
"ASN.1",

26
rsa.js
View File

@ -20,10 +20,10 @@ RSA.generate = native.generate;
// 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.
RSA.neuter = function(opts) {
RSA.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) {
Object.keys(opts.jwk).forEach(function (k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
@ -38,7 +38,7 @@ RSA.neuter = function(opts) {
native.neuter = RSA.neuter;
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
RSA.__thumbprint = function(jwk) {
RSA.__thumbprint = function (jwk) {
// Use the same entropy for SHA as for key
var len = Math.floor(jwk.n.length * 0.75);
var alg = 'SHA-256';
@ -51,20 +51,20 @@ RSA.__thumbprint = function(jwk) {
}
return sha2
.sum(alg, '{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}')
.then(function(hash) {
.then(function (hash) {
return Enc.bufToUrlBase64(Uint8Array.from(hash));
});
};
RSA.thumbprint = function(opts) {
return Promise.resolve().then(function() {
RSA.thumbprint = function (opts) {
return Promise.resolve().then(function () {
var jwk;
if ('EC' === opts.kty) {
jwk = opts;
} else if (opts.jwk) {
jwk = opts.jwk;
} else {
return RSA.import(opts).then(function(jwk) {
return RSA.import(opts).then(function (jwk) {
return RSA.__thumbprint(jwk);
});
}
@ -72,8 +72,8 @@ RSA.thumbprint = function(opts) {
});
};
RSA.export = function(opts) {
return Promise.resolve().then(function() {
RSA.export = function (opts) {
return Promise.resolve().then(function () {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error('must pass { jwk: jwk }');
}
@ -152,16 +152,16 @@ RSA.export = function(opts) {
};
native.export = RSA.export;
RSA.pack = function(opts) {
RSA.pack = function (opts) {
// wrapped in a promise for API compatibility
// with the forthcoming browser version
// (and potential future native node capability)
return Promise.resolve().then(function() {
return Promise.resolve().then(function () {
return RSA.export(opts);
});
};
RSA._importSync = function(opts) {
RSA._importSync = function (opts) {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error('must pass { pem: pem } as a string');
}
@ -185,7 +185,7 @@ RSA.parse = function parseRsa(opts) {
// wrapped in a promise for API compatibility
// with the forthcoming browser version
// (and potential future native node capability)
return Promise.resolve().then(function() {
return Promise.resolve().then(function () {
return RSA._importSync(opts);
});
};

View File

@ -4,7 +4,7 @@ var Keypairs = require('../');
/* global Promise*/
Keypairs.parseOrGenerate({ key: null })
.then(function(pair) {
.then(function (pair) {
// should NOT have any warning output
if (!pair.private || !pair.public) {
throw new Error('missing key pairs');
@ -12,42 +12,42 @@ Keypairs.parseOrGenerate({ key: null })
return Promise.all([
// Testing Public Part of key
Keypairs.export({ jwk: pair.public }).then(function(pem) {
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) {
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() {
.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) {
.catch(function (e) {
if ('NOERR' === e.code) {
throw e;
}
return true;
})
]).then(function() {
]).then(function () {
return true;
});
}),
// Testing Private Part of Key
Keypairs.export({ jwk: pair.private }).then(function(pem) {
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) {
Keypairs.parse({ key: pem }).then(function (pair) {
if (!pair.private) {
throw new Error('should have private part');
}
@ -56,7 +56,7 @@ Keypairs.parseOrGenerate({ key: null })
}
return true;
}),
Keypairs.parse({ key: pem, public: true }).then(function(
Keypairs.parse({ key: pem, public: true }).then(function (
pair
) {
if (pair.private) {
@ -69,12 +69,12 @@ Keypairs.parseOrGenerate({ key: null })
}
return true;
})
]).then(function() {
]).then(function () {
return true;
});
}),
Keypairs.parseOrGenerate({ key: 'not a key', public: true }).then(
function(pair) {
function (pair) {
// SHOULD have warning output
if (!pair.private || !pair.public) {
throw new Error(
@ -89,18 +89,18 @@ Keypairs.parseOrGenerate({ key: null })
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)');
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;
}
return true;
}),
),
Keypairs.parse({
key: JSON.stringify(pair.private),
public: true
}).then(function(pair) {
}).then(function (pair) {
if (pair.private) {
throw new Error("has private key when it shouldn't");
}
@ -110,14 +110,14 @@ Keypairs.parseOrGenerate({ key: null })
return true;
}),
Keypairs.parse({ key: JSON.stringify(pair.public), private: true })
.then(function() {
.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) {
.catch(function (e) {
if ('NOERR' === e.code) {
throw e;
}
@ -130,7 +130,7 @@ Keypairs.parseOrGenerate({ key: null })
alg: 'ES256',
iss: 'https://example.com/',
exp: '1h'
}).then(function(jwt) {
}).then(function (jwt) {
var parts = jwt.split('.');
var now = Math.round(Date.now() / 1000);
var token = {
@ -144,10 +144,10 @@ Keypairs.parseOrGenerate({ key: null })
}
throw new Error('token was not properly generated');
})
]).then(function(results) {
]).then(function (results) {
if (
results.length &&
results.every(function(v) {
results.every(function (v) {
return true === v;
})
) {
@ -158,7 +158,7 @@ Keypairs.parseOrGenerate({ key: null })
}
});
})
.catch(function(e) {
.catch(function (e) {
console.error('Caught an unexpected (failing) error:');
console.error(e);
process.exit(1);