Compare commits

...

22 Commits

Author SHA1 Message Date
481015940e remove junk logs 2019-05-14 03:57:02 -06:00
1c67c47131 v1.3.0: integrate node v6 support (rsa-compat backport) 2019-05-14 03:54:27 -06:00
adc8eaec85 v1.2.5: remove junk vim swap file 2019-03-29 11:46:19 -06:00
57b27cb9d8 v1.2.4: typo fix in error message 2019-03-04 17:02:44 -07:00
acdad0065b v1.2.3: remove stray warning 2019-03-04 16:56:17 -07:00
b7508bba6c v1.2.2: remove telemetry and stray logs, lint some 2019-03-04 16:50:38 -07:00
6ae7076185 v1.2.1: update README 2019-02-07 00:13:17 -07:00
83dad00aaa Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/rasha.js 2019-02-07 00:12:45 -07:00
e088eedc8f v1.2.0: add jwk thumbprinting 2019-02-07 00:12:12 -07:00
6617d2c352 Removed buil status for now. 2019-01-24 20:18:48 -08:00
ef29727655 Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/rasha.js 2018-12-15 14:56:16 -07:00
691212ff13 sign a message 2018-12-15 14:55:54 -07:00
cd0533a72b Fixed the env var for testing extended bit lengths.
Some checks are pending
continuous-integration/drone/push Build is passing
2018-12-03 12:31:43 -08:00
c80e07ee7d cleanup 2018-12-01 16:28:44 -07:00
5bddc62731 re-order functions alphabetically 2018-12-01 15:33:43 -07:00
eb02693c1f Updated build status badge. 2018-11-30 13:41:44 -08:00
e3babcd6a9 Removed testing key sizes below 512. Added optional testing of larger keys. 2018-11-30 13:16:56 -08:00
54ccf6fa37 Added test status to README. 2018-11-30 11:30:12 -08:00
e8837a0721 output der as binstr, add comment 2018-11-24 19:47:56 -07:00
7a1df742aa catch generate errors, duh 2018-11-24 19:03:26 -07:00
bb272a9689 correct format hint 2018-11-24 18:36:32 -07:00
c17f0338e1 note on urlsafe base64 2018-11-24 17:37:23 -07:00
18 changed files with 467 additions and 262 deletions

11
.drone.yml Normal file
View File

@ -0,0 +1,11 @@
kind: pipeline
name: default
pipeline:
build:
image: node
environment:
RASHA_TEST_LARGE_KEYS: "true"
commands:
- npm install --ignore-scripts
- npm test

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
all.*
.*.sw*

View File

@ -1,10 +1,12 @@
[Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js) [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js)
========= =========
Sponsored by [Root](https://therootcompany.com). A [Root](https://therootcompany.com) Project.
Built for [ACME.js](https://git.coolaj86.com/coolaj86/acme.js) Built for [ACME.js](https://git.coolaj86.com/coolaj86/acme.js)
and [Greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) and [Greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
| ~550 lines of code | 3kb gzipped | 10kb minified | 18kb with comments | | ~550 lines of code | 3kb gzipped | 10kb minified | 18kb with comments |
RSA tools. Lightweight. Zero Dependencies. Universal compatibility. RSA tools. Lightweight. Zero Dependencies. Universal compatibility.
@ -12,10 +14,12 @@ RSA tools. Lightweight. Zero Dependencies. Universal compatibility.
* [x] Fast and Easy RSA Key Generation * [x] Fast and Easy RSA Key Generation
* [x] PEM-to-JWK * [x] PEM-to-JWK
* [x] JWK-to-PEM * [x] JWK-to-PEM
* [x] JWK thumbprint
* [x] SSH "pub" format * [x] SSH "pub" format
* [ ] ECDSA * [ ] ECDSA
* **Need EC or ECDSA tools?** Check out [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js) * **Need EC or ECDSA tools?** Check out [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js)
## Generate RSA Key ## Generate RSA Key
Achieves the *fastest possible key generation* using node's native RSA bindings to OpenSSL, Achieves the *fastest possible key generation* using node's native RSA bindings to OpenSSL,
@ -32,7 +36,7 @@ Rasha.generate({ format: 'jwk' }).then(function (keypair) {
* `format` defaults to `'jwk'` * `format` defaults to `'jwk'`
* `'pkcs1'` (traditional) * `'pkcs1'` (traditional)
* `'pkcs8'` * `'pkcs8'` <!-- * `'ssh'` -->
* `modulusLength` defaults to 2048 (must not be lower) * `modulusLength` defaults to 2048 (must not be lower)
* generally you shouldn't pick a larger key size - they're slow * generally you shouldn't pick a larger key size - they're slow
* **2048** is more than sufficient * **2048** is more than sufficient
@ -171,6 +175,14 @@ Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQAB
-----END PUBLIC KEY----- -----END PUBLIC KEY-----
``` ```
## JWK Thumbprint
```js
Rasha.thumbprint({ jwk: jwk }).then(function (thumbprint) {
console.log(thumbprint);
});
```
Testing Testing
------- -------

View File

@ -8,20 +8,33 @@ var ASN1 = require('../lib/asn1.js');
var infile = process.argv[2]; var infile = process.argv[2];
var format = process.argv[3]; var format = process.argv[3];
var msg = process.argv[4];
var sign;
if ('sign' === format) {
sign = true;
format = 'pkcs8';
}
if (!infile) { if (!infile) {
infile = 'jwk'; infile = 'jwk';
} }
if (-1 !== [ 'jwk', 'pem', 'json', 'der', 'pkcs1', 'pkcs8', 'spki' ].indexOf(infile)) { if (-1 !== [ 'jwk', 'pem', 'json', 'der', 'pkcs1', 'pkcs8', 'spki' ].indexOf(infile)) {
console.log("Generating new key..."); console.info("Generating new key...");
Rasha.generate({ Rasha.generate({
format: infile format: infile
, modulusLength: parseInt(format, 10) || 2048 , modulusLength: parseInt(format, 10) || 2048
, encoding: parseInt(format, 10) ? null : format , encoding: parseInt(format, 10) ? null : format
}).then(function (key) { }).then(function (key) {
console.log(key.private); if ('der' === infile || 'der' === format) {
console.log(key.public); key.private = key.private.toString('binary');
key.public = key.public.toString('binary');
}
console.info(key.private);
console.info(key.public);
}).catch(function (err) {
console.error(err);
process.exit(1);
}); });
return; return;
} }
@ -33,13 +46,24 @@ try {
// ignore // ignore
} }
var thumbprint = ('thumbprint' === format);
if (thumbprint) {
format = 'public';
}
if ('string' === typeof key) { if ('string' === typeof key) {
if (thumbprint) {
Rasha.thumbprint({ pem: key }).then(console.info);
return;
}
if ('tpl' === format) { if ('tpl' === format) {
var block = PEM.parseBlock(key); var block = PEM.parseBlock(key);
var asn1 = ASN1.parse(block.der); var asn1 = ASN1.parse(block.der);
ASN1.tpl(asn1); ASN1.tpl(asn1);
return; return;
} }
if (sign) { signMessage(key, msg); return; }
var pub = (-1 !== [ 'public', 'spki', 'pkix' ].indexOf(format)); var pub = (-1 !== [ 'public', 'spki', 'pkix' ].indexOf(format));
Rasha.import({ pem: key, public: (pub || format) }).then(function (jwk) { Rasha.import({ pem: key, public: (pub || format) }).then(function (jwk) {
console.info(JSON.stringify(jwk, null, 2)); console.info(JSON.stringify(jwk, null, 2));
@ -48,10 +72,38 @@ if ('string' === typeof key) {
process.exit(1); process.exit(1);
}); });
} else { } else {
if (thumbprint) {
Rasha.thumbprint({ jwk: key }).then(console.info);
return;
}
Rasha.export({ jwk: key, format: format }).then(function (pem) { Rasha.export({ jwk: key, format: format }).then(function (pem) {
if (sign) { signMessage(pem, msg); return; }
console.info(pem); console.info(pem);
}).catch(function (err) { }).catch(function (err) {
console.error(err); console.error(err);
process.exit(2); process.exit(2);
}); });
} }
function signMessage(pem, name) {
var msg;
try {
msg = fs.readFileSync(name);
} catch(e) {
console.warn("[info] input string did not exist as a file, signing the string itself");
msg = Buffer.from(name, 'binary');
}
var crypto = require('crypto');
var sign = crypto.createSign('SHA256');
sign.write(msg)
sign.end()
var buf = sign.sign(pem);
console.info(buf.toString('base64'));
/*
Rasha.sign({ pem: pem, message: msg, alg: 'SHA256' }).then(function (sig) {
}).catch(function () {
console.error(err);
process.exit(3);
});
*/
}

View File

@ -1,2 +1,2 @@
'use strict'; 'use strict';
module.exports = require('./lib/rasha.js'); module.exports = require('./lib/rsa.js');

View File

@ -129,9 +129,7 @@ ASN1.parse = function parseAsn1(buf, depth, ws) {
index += (2 + child.lengthSize + child.length); index += (2 + child.lengthSize + child.length);
//console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length)); //console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
if (index > (2 + asn1.lengthSize + asn1.length)) { if (index > (2 + asn1.lengthSize + asn1.length)) {
console.error(JSON.stringify(asn1, function (k, v) { console.error(JSON.stringify(asn1, toPrettyHex, 2));
if ('value' === k) { return '0x' + Enc.bufToHex(v.data); } return v;
}, 2));
throw new Error("Parse error: child value length (" + child.length throw new Error("Parse error: child value length (" + child.length
+ ") is greater than remaining parent length (" + (asn1.length - index) + ") is greater than remaining parent length (" + (asn1.length - index)
+ " = " + asn1.length + " - " + index + ")"); + " = " + asn1.length + " - " + index + ")");
@ -140,13 +138,13 @@ ASN1.parse = function parseAsn1(buf, depth, ws) {
//console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); //console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
} }
if (index !== (2 + asn1.lengthSize + asn1.length)) { if (index !== (2 + asn1.lengthSize + asn1.length)) {
console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length)) throw new Error("premature end-of-file (" + 'index: ' + index + ' length: ' + (2 + asn1.lengthSize + asn1.length) + ")");
throw new Error("premature end-of-file");
} }
if (iters >= 15) { throw new Error(ASN1.ELOOP); } if (iters >= 15) { throw new Error(ASN1.ELOOP); }
return asn1; return asn1;
}; };
function toPrettyHex(k, v) { if ('value' === k) { return '0x' + Enc.bufToHex(v.data); } return v; }
/* /*
ASN1._stringify = function(asn1) { ASN1._stringify = function(asn1) {
@ -222,14 +220,14 @@ ASN1.tpl = function (asn1) {
} }
write(asn1); write(asn1);
console.log('var opts = {};'); console.info('var opts = {};');
console.log(vars.join('\n') + '\n'); console.info(vars.join('\n') + '\n');
console.log(); console.info();
console.log('function buildSchema(opts) {'); console.info('function buildSchema(opts) {');
console.log(sp + 'return Enc.hexToBuf(' + str.slice(3) + ');'); console.info(sp + 'return Enc.hexToBuf(' + str.slice(3) + ');');
console.log('}'); console.info('}');
console.log(); console.info();
console.log('buildSchema(opts);'); console.info('buildSchema(opts);');
}; };
module.exports = ASN1; module.exports = ASN1;

51
lib/crypto.js Normal file
View File

@ -0,0 +1,51 @@
'use strict';
/*global Promise*/
var PEM = require('./pem.js');
var x509 = require('./x509.js');
var ASN1 = require('./asn1.js');
// Hacky-do, wrappy-do
module.exports.generate = function (opts) {
if (!opts) { opts = {}; }
return new Promise(function (resolve, reject) {
try {
var modlen = opts.modulusLength || 2048;
var exp = opts.publicExponent || 0x10001;
var pair = require('./generate-privkey.js')(modlen,exp);
if (pair.private) { resolve(pair); return; }
pair = toJwks(pair);
resolve({ private: pair.private , public: pair.public });
} catch(e) {
reject(e);
}
});
};
// PKCS1 to JWK only
function toJwks(oldpair) {
var block = PEM.parseBlock(oldpair.privateKeyPem);
var asn1 = ASN1.parse(block.bytes);
var jwk = { kty: 'RSA', n: null, e: null };
jwk = x509.parsePkcs1(block.bytes, asn1, jwk);
return { private: jwk, public: neuter(jwk) };
}
// Copied from rsa.js to prevent circular dep
var privates = [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ];
function neuter(priv) {
var pub = {};
Object.keys(priv).forEach(function (key) {
if (!privates.includes(key)) {
pub[key] = priv[key];
}
});
return pub;
}
if (require.main === module) {
module.exports.generate().then(function (pair) {
console.info(JSON.stringify(pair.private, null, 2));
console.info(JSON.stringify(pair.public, null, 2));
});
}

View File

@ -2,7 +2,36 @@
var Enc = module.exports; var Enc = module.exports;
Enc.bufToHex = function toHex(u8) { Enc.base64ToBuf = function (str) {
// always convert from urlsafe base64, just in case
//return Buffer.from(Enc.urlBase64ToBase64(str)).toString('base64');
// node handles urlBase64 automatically
return Buffer.from(str, 'base64');
};
Enc.base64ToHex = function (b64) {
return Enc.bufToHex(Enc.base64ToBuf(b64));
};
Enc.bufToBase64 = function (u8) {
// we want to maintain api compatability with browser APIs,
// so we assume that this could be a Uint8Array
return Buffer.from(u8).toString('base64');
};
/*
Enc.bufToUint8 = function bufToUint8(buf) {
return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
};
*/
Enc.bufToUrlBase64 = function (u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
Enc.bufToHex = function (u8) {
var hex = []; var hex = [];
var i, h; var i, h;
var len = (u8.byteLength || u8.length); var len = (u8.byteLength || u8.length);
@ -24,7 +53,7 @@ Enc.hexToBuf = function (hex) {
return Buffer.from(hex, 'hex'); return Buffer.from(hex, 'hex');
}; };
Enc.numToHex = function numToHex(d) { Enc.numToHex = function (d) {
d = d.toString(16); d = d.toString(16);
if (d.length % 2) { if (d.length % 2) {
return '0' + d; return '0' + d;
@ -32,37 +61,17 @@ Enc.numToHex = function numToHex(d) {
return d; return d;
}; };
Enc.base64ToHex = function base64ToHex(b64) {
return Enc.bufToHex(Enc.base64ToBuf(b64));
};
Enc.bufToBase64 = function toHex(u8) {
// we want to maintain api compatability with browser APIs,
// so we assume that this could be a Uint8Array
return Buffer.from(u8).toString('base64');
};
/* /*
Enc.bufToUint8 = function bufToUint8(buf) { Enc.strToBase64 = function (str) {
return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)); // 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');
}; };
*/ */
Enc.bufToUrlBase64 = function toHex(u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
Enc.strToHex = function strToHex(str) {
return Buffer.from(str).toString('hex');
};
Enc.strToBuf = function strToBuf(str) {
return Buffer.from(str);
};
/* /*
Enc.strToBin = function strToBin(str) { Enc.strToBin = function (str) {
var escstr = encodeURIComponent(str); var escstr = encodeURIComponent(str);
// replaces any uri escape sequence, such as %0A, // replaces any uri escape sequence, such as %0A,
// with binary escape, such as 0x0A // with binary escape, such as 0x0A
@ -74,17 +83,16 @@ Enc.strToBin = function strToBin(str) {
}; };
*/ */
/* Enc.strToBuf = function (str) {
Enc.strToBase64 = function strToBase64(str) { return Buffer.from(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 Enc.strToHex = function (str) {
return Buffer.from(str).toString('base64'); return Buffer.from(str).toString('hex');
}; };
*/
/* /*
Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { Enc.urlBase64ToBase64 = function (str) {
var r = str % 4; var r = str % 4;
if (2 === r) { if (2 === r) {
str += '=='; str += '==';
@ -94,9 +102,3 @@ Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) {
return str.replace(/-/g, '+').replace(/_/g, '/'); return str.replace(/-/g, '+').replace(/_/g, '/');
}; };
*/ */
Enc.base64ToBuf = function base64ToBuf(str) {
// always convert from urlsafe base64, just in case
//return Buffer.from(Enc.urlBase64ToBase64(str)).toString('base64');
return Buffer.from(str, 'base64');
};

View File

@ -0,0 +1,53 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function (bitlen, exp) {
var k = require('node-forge').pki.rsa
.generateKeyPair({ bits: bitlen || 2048, e: exp || 0x10001 }).privateKey;
var jwk = {
kty: "RSA"
, n: _toUrlBase64(k.n)
, e: _toUrlBase64(k.e)
, d: _toUrlBase64(k.d)
, p: _toUrlBase64(k.p)
, q: _toUrlBase64(k.q)
, dp: _toUrlBase64(k.dP)
, dq: _toUrlBase64(k.dQ)
, qi: _toUrlBase64(k.qInv)
};
return {
private: jwk
, public: {
kty: jwk.kty
, n: jwk.n
, e: jwk.e
}
};
};
function _toUrlBase64(fbn) {
var hex = fbn.toRadix(16);
if (hex.length % 2) {
// Invalid hex string
hex = '0' + hex;
}
while ('00' === hex.slice(0, 2)) {
hex = hex.slice(2);
}
return Buffer.from(hex, 'hex').toString('base64')
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g,"")
;
}
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.private);
console.warn(keypair.public);
//console.info(keypair.privateKeyJwk);
//console.warn(keypair.publicKeyJwk);
}

View File

@ -0,0 +1,23 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function (bitlen, exp) {
var keypair = require('crypto').generateKeyPairSync(
'rsa'
, { modulusLength: bitlen
, publicExponent: exp
, privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }
}
);
var result = { privateKeyPem: keypair.privateKey.trim() };
return result;
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

View File

@ -0,0 +1,22 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function (bitlen, exp) {
var ursa;
try {
ursa = require('ursa');
} catch(e) {
ursa = require('ursa-optional');
}
var keypair = ursa.generatePrivateKey(bitlen, exp);
var result = { privateKeyPem: keypair.toPrivatePem().toString('ascii').trim() };
return result;
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

64
lib/generate-privkey.js Normal file
View File

@ -0,0 +1,64 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
var oldver = false;
module.exports = function (bitlen, exp) {
bitlen = parseInt(bitlen, 10) || 2048;
exp = parseInt(exp, 10) || 65537;
try {
return require('./generate-privkey-node.js')(bitlen, exp);
} catch(e) {
if (!/generateKeyPairSync is not a function/.test(e.message)) {
throw e;
}
try {
return require('./generate-privkey-ursa.js')(bitlen, exp);
} catch(e) {
if (e.code !== 'MODULE_NOT_FOUND') {
console.error("[rsa-compat] Unexpected error when using 'ursa':");
console.error(e);
}
if (!oldver) {
oldver = true;
console.warn("[WARN] rsa-compat: Your version of node does not have crypto.generateKeyPair()");
console.warn("[WARN] rsa-compat: Please update to node >= v10.12 or 'npm install --save ursa node-forge'");
console.warn("[WARN] rsa-compat: Using node-forge as a fallback may be unacceptably slow.");
if (/arm|mips/i.test(require('os').arch)) {
console.warn("================================================================");
console.warn(" WARNING");
console.warn("================================================================");
console.warn("");
console.warn("WARNING: You are generating an RSA key using pure JavaScript on");
console.warn(" a VERY SLOW cpu. This could take DOZENS of minutes!");
console.warn("");
console.warn(" We recommend installing node >= v10.12, or 'gcc' and 'ursa'");
console.warn("");
console.warn("EXAMPLE:");
console.warn("");
console.warn(" sudo apt-get install build-essential && npm install ursa");
console.warn("");
console.warn("================================================================");
}
}
try {
return require('./generate-privkey-forge.js')(bitlen, exp);
} catch(e) {
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
console.error("[ERROR] rsa-compat: could not generate a private key.");
console.error("None of crypto.generateKeyPair, ursa, nor node-forge are present");
}
}
}
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

View File

@ -3,43 +3,24 @@
var PEM = module.exports; var PEM = module.exports;
var Enc = require('./encoding.js'); var Enc = require('./encoding.js');
PEM.RSA_OBJID = '06 09 2A864886F70D010101'
.replace(/\s+/g, '').toLowerCase();
PEM.parseBlock = function pemToDer(pem) { PEM.parseBlock = function pemToDer(pem) {
var typ; var lines = pem.trim().split(/\n/);
var pub; var end = lines.length - 1;
var hex; var head = lines[0].match(/-----BEGIN (.*)-----/);
var der = Enc.base64ToBuf(pem.split(/\n/).filter(function (line, i) { var foot = lines[end].match(/-----END (.*)-----/);
if (0 === i) {
if (/ PUBLIC /.test(line)) {
pub = true;
} else if (/ PRIVATE /.test(line)) {
pub = false;
}
if (/ RSA /.test(line)) {
typ = 'RSA';
}
}
return !/---/.test(line);
}).join(''));
if (!typ) { if (head) {
hex = Enc.bufToHex(der); lines = lines.slice(1, end);
if (-1 !== hex.indexOf(PEM.RSA_OBJID)) { head = head[1];
typ = 'RSA'; if (head !== foot[1]) {
throw new Error("headers and footers do not match");
} }
} }
if (!typ) {
console.warn("Definitely not an RSA PKCS#8 because there's no RSA Object ID in the DER body.");
console.warn("Probably not an RSA PKCS#1 because 'RSA' wasn't in the PEM type string.");
}
return { kty: typ, pub: pub, der: der }; return { type: head, bytes: Enc.base64ToBuf(lines.join('')) };
}; };
PEM.packBlock = function (opts) { PEM.packBlock = function (opts) {
// TODO allow for headers?
return '-----BEGIN ' + opts.type + '-----\n' return '-----BEGIN ' + opts.type + '-----\n'
+ Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n' + Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n'
+ '-----END ' + opts.type + '-----' + '-----END ' + opts.type + '-----'

View File

@ -5,19 +5,31 @@ var SSH = require('./ssh.js');
var PEM = require('./pem.js'); var PEM = require('./pem.js');
var x509 = require('./x509.js'); var x509 = require('./x509.js');
var ASN1 = require('./asn1.js'); var ASN1 = require('./asn1.js');
var Enc = require('./encoding.js');
var Crypto = require('./crypto.js');
/*global Promise*/ /*global Promise*/
RSA.generate = function (opts) { RSA.generate = function (opts) {
return Promise.resolve().then(function () { opts.kty = "RSA";
var typ = 'rsa'; return Crypto.generate(opts).then(function (pair) {
var format = opts.format; var format = opts.format;
var encoding = opts.encoding; var encoding = opts.encoding;
// The easy way
if ('json' === format && !encoding) {
format = 'jwk';
encoding = 'json';
}
if (('jwk' === format || !format) && ('json' === encoding || !encoding)) { return pair; }
if ('jwk' === format || 'json' === encoding) {
throw new Error("format '" + format + "' is incompatible with encoding '" + encoding + "'");
}
// The... less easy way
/*
var priv; var priv;
var pub; var pub;
if (!format) {
format = 'jwk';
}
if ('spki' === format || 'pkcs8' === format) { if ('spki' === format || 'pkcs8' === format) {
format = 'pkcs8'; format = 'pkcs8';
pub = 'spki'; pub = 'spki';
@ -31,13 +43,8 @@ RSA.generate = function (opts) {
encoding = 'der'; encoding = 'der';
} }
if ('jwk' === format || 'json' === format) {
format = 'jwk';
encoding = 'json';
} else {
priv = format; priv = format;
pub = pub || format; pub = pub || format;
}
if (!encoding) { if (!encoding) {
encoding = 'pem'; encoding = 'pem';
@ -51,29 +58,19 @@ RSA.generate = function (opts) {
priv = { type: 'pkcs1', format: 'pem' }; priv = { type: 'pkcs1', format: 'pem' };
pub = { type: 'pkcs1', format: 'pem' }; pub = { type: 'pkcs1', format: 'pem' };
} }
*/
return new Promise(function (resolve, reject) { if (('pem' === format || 'der' === format) && !encoding) {
return require('crypto').generateKeyPair(typ, { encoding = format;
modulusLength: opts.modulusLength || 2048 format = 'pkcs1';
, publicExponent: opts.publicExponent || 0x10001
, privateKeyEncoding: priv
, publicKeyEncoding: pub
}, function (err, pubkey, privkey) {
if (err) { reject(err); }
resolve({
private: privkey
, public: pubkey
});
});
}).then(function (keypair) {
if ('jwk' !== format) {
return keypair;
} }
return { var exOpts = { jwk: pair.private, format: format, encoding: encoding };
private: RSA.importSync({ pem: keypair.private, format: priv.format }) return RSA.export(exOpts).then(function (priv) {
, public: RSA.importSync({ pem: keypair.public, format: pub.format, public: true }) exOpts.public = true;
}; if ('pkcs8' === exOpts.format) { exOpts.format = 'spki'; }
return RSA.export(exOpts).then(function (pub) {
return { private: priv, public: pub };
});
}); });
}); });
}; };
@ -90,18 +87,18 @@ RSA.importSync = function (opts) {
var pem = opts.pem; var pem = opts.pem;
var block = PEM.parseBlock(pem); var block = PEM.parseBlock(pem);
//var hex = toHex(u8); //var hex = toHex(u8);
var asn1 = ASN1.parse(block.der); var asn1 = ASN1.parse(block.bytes);
var meta = x509.guess(block.der, asn1); var meta = x509.guess(block.bytes, asn1);
if ('pkcs1' === meta.format) { if ('pkcs1' === meta.format) {
jwk = x509.parsePkcs1(block.der, asn1, jwk); jwk = x509.parsePkcs1(block.bytes, asn1, jwk);
} else { } else {
jwk = x509.parsePkcs8(block.der, asn1, jwk); jwk = x509.parsePkcs8(block.bytes, asn1, jwk);
} }
if (opts.public) { if (opts.public) {
jwk = RSA.nueter(jwk); jwk = RSA.neuter(jwk);
} }
return jwk; return jwk;
}; };
@ -138,7 +135,7 @@ RSA.exportSync = function (opts) {
var format = opts.format; var format = opts.format;
var pub = opts.public; var pub = opts.public;
if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) {
jwk = RSA.nueter(jwk); jwk = RSA.neuter(jwk);
} }
if ('RSA' !== jwk.kty) { if ('RSA' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'RSA' for RSA keys"); throw new Error("options.jwk.kty must be 'RSA' for RSA keys");
@ -192,13 +189,37 @@ RSA.pack = function (opts) {
RSA.toPem = RSA.export = RSA.pack; RSA.toPem = RSA.export = RSA.pack;
// snip the _private_ parts... hAHAHAHA! // snip the _private_ parts... hAHAHAHA!
RSA.nueter = function (jwk) { var privates = [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ];
// (snip rather than new object to keep potential extra data) // fix misspelling without breaking the API
// otherwise we could just do this: RSA.neuter = RSA.nueter = function (priv) {
// return { kty: jwk.kty, n: jwk.n, e: jwk.e }; var pub = {};
[ 'p', 'q', 'd', 'dp', 'dq', 'qi' ].forEach(function (key) { Object.keys(priv).forEach(function (key) {
if (key in jwk) { jwk[key] = undefined; } if (!privates.includes(key)) {
return jwk; pub[key] = priv[key];
}
});
return pub;
};
RSA.__thumbprint = function (jwk) {
var buf = require('crypto').createHash('sha256')
// alphabetically sorted keys [ 'e', 'kty', 'n' ]
.update('{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}')
.digest()
;
return Enc.bufToUrlBase64(buf);
};
RSA.thumbprint = function (opts) {
return Promise.resolve().then(function () {
var jwk;
if ('RSA' === opts.kty) {
jwk = opts;
} else if (opts.jwk) {
jwk = opts.jwk;
} else {
jwk = RSA.importSync(opts);
}
return RSA.__thumbprint(jwk);
}); });
return jwk;
}; };

View File

@ -1,111 +0,0 @@
'use strict';
// We believe in a proactive approach to sustainable open source.
// As part of that we make it easy for you to opt-in to following our progress
// and we also stay up-to-date on telemetry such as operating system and node
// version so that we can focus our efforts where they'll have the greatest impact.
//
// Want to learn more about our Terms, Privacy Policy, and Mission?
// Check out https://therootcompany.com/legal/
var os = require('os');
var crypto = require('crypto');
var https = require('https');
var pkg = require('../package.json');
// to help focus our efforts in the right places
var data = {
package: pkg.name
, version: pkg.version
, node: process.version
, arch: process.arch || os.arch()
, platform: process.platform || os.platform()
, release: os.release()
};
function addCommunityMember(opts) {
setTimeout(function () {
var req = https.request({
hostname: 'api.therootcompany.com'
, port: 443
, path: '/api/therootcompany.com/public/community'
, method: 'POST'
, headers: { 'Content-Type': 'application/json' }
}, function (resp) {
// let the data flow, so we can ignore it
resp.on('data', function () {});
//resp.on('data', function (chunk) { console.log(chunk.toString()); });
resp.on('error', function () { /*ignore*/ });
//resp.on('error', function (err) { console.error(err); });
});
var obj = JSON.parse(JSON.stringify(data));
obj.action = 'updates';
try {
obj.ppid = ppid(obj.action);
} catch(e) {
// ignore
//console.error(e);
}
obj.name = opts.name || undefined;
obj.address = opts.email;
obj.community = 'node.js@therootcompany.com';
req.write(JSON.stringify(obj, 2, null));
req.end();
req.on('error', function () { /*ignore*/ });
//req.on('error', function (err) { console.error(err); });
}, 50);
}
function ping(action) {
setTimeout(function () {
var req = https.request({
hostname: 'api.therootcompany.com'
, port: 443
, path: '/api/therootcompany.com/public/ping'
, method: 'POST'
, headers: { 'Content-Type': 'application/json' }
}, function (resp) {
// let the data flow, so we can ignore it
resp.on('data', function () { });
//resp.on('data', function (chunk) { console.log(chunk.toString()); });
resp.on('error', function () { /*ignore*/ });
//resp.on('error', function (err) { console.error(err); });
});
var obj = JSON.parse(JSON.stringify(data));
obj.action = action;
try {
obj.ppid = ppid(obj.action);
} catch(e) {
// ignore
//console.error(e);
}
req.write(JSON.stringify(obj, 2, null));
req.end();
req.on('error', function (/*e*/) { /*console.error('req.error', e);*/ });
}, 50);
}
// to help identify unique installs without getting
// the personally identifiable info that we don't want
function ppid(action) {
var parts = [ action, data.package, data.version, data.node, data.arch, data.platform, data.release ];
var ifaces = os.networkInterfaces();
Object.keys(ifaces).forEach(function (ifname) {
if (/^en/.test(ifname) || /^eth/.test(ifname) || /^wl/.test(ifname)) {
if (ifaces[ifname] && ifaces[ifname].length) {
parts.push(ifaces[ifname][0].mac);
}
}
});
return crypto.createHash('sha1').update(parts.join(',')).digest('base64');
}
module.exports.ping = ping;
module.exports.joinCommunity = addCommunityMember;
if (require.main === module) {
ping('install');
//addCommunityMember({ name: "AJ ONeal", email: 'coolaj86@gmail.com' });
}

View File

@ -1,7 +1,5 @@
'use strict'; 'use strict';
// TODO fun idea: create a template from an existing file
var x509 = module.exports; var x509 = module.exports;
var ASN1 = require('./asn1.js'); var ASN1 = require('./asn1.js');
var Enc = require('./encoding.js'); var Enc = require('./encoding.js');

View File

@ -1,6 +1,6 @@
{ {
"name": "rasha", "name": "rasha",
"version": "1.1.0", "version": "1.3.0",
"description": "💯 PEM-to-JWK and JWK-to-PEM for RSA keys in a lightweight, zero-dependency library focused on perfect universal compatibility.", "description": "💯 PEM-to-JWK and JWK-to-PEM for RSA keys in a lightweight, zero-dependency library focused on perfect universal compatibility.",
"homepage": "https://git.coolaj86.com/coolaj86/rasha.js", "homepage": "https://git.coolaj86.com/coolaj86/rasha.js",
"main": "index.js", "main": "index.js",
@ -16,7 +16,6 @@
"lib": "lib" "lib": "lib"
}, },
"scripts": { "scripts": {
"postinstall": "node lib/telemetry.js event:install",
"test": "bash test.sh" "test": "bash test.sh"
}, },
"repository": { "repository": {
@ -36,5 +35,8 @@
"PEM-to-SSH" "PEM-to-SSH"
], ],
"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",
"trulyOptionalDependencies": {
"node-forge": "^0.8.2"
}
} }

38
test.sh
View File

@ -123,13 +123,21 @@ jwktopem ""
echo "" echo ""
echo "testing node key generation" echo "testing node key generation"
echo "defaults"
node bin/rasha.js > /dev/null node bin/rasha.js > /dev/null
echo "jwk"
node bin/rasha.js jwk > /dev/null node bin/rasha.js jwk > /dev/null
echo "json 2048"
node bin/rasha.js json 2048 > /dev/null node bin/rasha.js json 2048 > /dev/null
echo "der"
node bin/rasha.js der > /dev/null node bin/rasha.js der > /dev/null
echo "pkcs8 der"
node bin/rasha.js pkcs8 der > /dev/null node bin/rasha.js pkcs8 der > /dev/null
echo "pem"
node bin/rasha.js pem > /dev/null node bin/rasha.js pem > /dev/null
echo "pkcs1"
node bin/rasha.js pkcs1 pem > /dev/null node bin/rasha.js pkcs1 pem > /dev/null
echo "spki"
node bin/rasha.js spki > /dev/null node bin/rasha.js spki > /dev/null
echo "PASS" echo "PASS"
@ -137,23 +145,39 @@ echo ""
echo "" echo ""
echo "Re-running tests with random keys of varying sizes" echo "Re-running tests with random keys of varying sizes"
echo "" echo ""
rndkey 32 # minimum key size
rndkey 64 # commented out sizes below 512, since they are below minimum size on some systems.
rndkey 128 # rndkey 32 # minimum key size
rndkey 256 # rndkey 64
# rndkey 128
# rndkey 256
rndkey 512 rndkey 512
rndkey 768 rndkey 768
rndkey 1024 rndkey 1024
rndkey 2048 # first secure key size rndkey 2048 # first secure key size
#rndkey 3072
#rndkey 4096 # largest reasonable key size if [ "${RASHA_TEST_LARGE_KEYS}" == "true" ]; then
echo "Pass" rndkey 3072
rndkey 4096 # largest reasonable key size
else
echo "" echo ""
echo "Note:" echo "Note:"
echo "Keys larger than 2048 have been tested and work, but are omitted from automated tests to save time." echo "Keys larger than 2048 have been tested and work, but are omitted from automated tests to save time."
echo "Set RASHA_TEST_LARGE_KEYS=true to enable testing of keys up to 4096."
fi
echo ""
echo "Pass"
rm fixtures/*.1.* rm fixtures/*.1.*
echo ""
echo "Testing Thumbprints"
node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs1.pem thumbprint
node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.json thumbprint
echo "PASS"
echo "" echo ""
echo "" echo ""
echo "PASSED:" echo "PASSED:"