Compare commits

..

2 Commits

18 changed files with 264 additions and 620 deletions

View File

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

2
.gitignore vendored
View File

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

View File

@ -1,59 +1,32 @@
[Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js) Mirror of [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js)
========= =========
A [Root](https://therootcompany.com) Project. Official Repository: <https://git.coolaj86.com/coolaj86/rasha.js>
Rasha.js
========
Sponsored by [Root](https://therootcompany.com).
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)
RSA tools. Lightweight. Zero Dependencies. Universal compatibility.
| ~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.
* [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
* **Need EC or ECDSA tools?** Check out [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js)
This project is fully functional and tested (and the code is pretty clean).
## Generate RSA Key It is considered to be complete, but if you find a bug please open an issue.
Achieves the *fastest possible key generation* using node's native RSA bindings to OpenSSL,
then converts to JWK for ease-of-use.
```
Rasha.generate({ format: 'jwk' }).then(function (keypair) {
console.log(keypair.private);
console.log(keypair.public);
});
```
**options**
* `format` defaults to `'jwk'`
* `'pkcs1'` (traditional)
* `'pkcs8'` <!-- * `'ssh'` -->
* `modulusLength` defaults to 2048 (must not be lower)
* generally you shouldn't pick a larger key size - they're slow
* **2048** is more than sufficient
* 3072 is way, way overkill and takes a few seconds to generate
* 4096 can take about a minute to generate and is just plain wasteful
**advanced options**
These options are provided for debugging and should not be used.
* `publicExponent` defaults to 65537 (`0x10001`)
## PEM-to-JWK ## PEM-to-JWK
* [x] PKCS#1 (traditional) * [x] PKCS#1 (traditional)
* [x] PKCS#8, SPKI/PKIX * [x] PKCS#8, SPKI/PKIX
* [x] 2048-bit, 3072-bit, 4096-bit (and ostensibily all others) * [x] 2048-bit, 4096-bit (and ostensibily all others)
* [x] SSH (RFC4716), (RFC 4716/SSH2) * [x] SSH (RFC4716), (RFC 4716/SSH2)
```js ```js
@ -175,20 +148,12 @@ Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQAB
-----END PUBLIC KEY----- -----END PUBLIC KEY-----
``` ```
## JWK Thumbprint
```js
Rasha.thumbprint({ jwk: jwk }).then(function (thumbprint) {
console.log(thumbprint);
});
```
Testing Testing
------- -------
All cases are tested in `test.sh`. All cases are tested in `test.sh`.
You can compare these keys to the ones that you get from OpenSSL, OpenSSH/ssh-keygen, and WebCrypto: You can compare these keys to the ones that you get from OpenSSL, ssh-keygen, and WebCrypto:
```bash ```bash
# Generate 2048-bit RSA Keypair # Generate 2048-bit RSA Keypair
@ -221,7 +186,9 @@ Goals of this project
Legal Legal
----- -----
[Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js) | [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js)
MPL-2.0 |
Licensed MPL-2.0
[Terms of Use](https://therootcompany.com/legal/#terms) | [Terms of Use](https://therootcompany.com/legal/#terms) |
[Privacy Policy](https://therootcompany.com/legal/#privacy) [Privacy Policy](https://therootcompany.com/legal/#privacy)

View File

@ -8,36 +8,7 @@ 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) {
infile = 'jwk';
}
if (-1 !== [ 'jwk', 'pem', 'json', 'der', 'pkcs1', 'pkcs8', 'spki' ].indexOf(infile)) {
console.info("Generating new key...");
Rasha.generate({
format: infile
, modulusLength: parseInt(format, 10) || 2048
, encoding: parseInt(format, 10) ? null : format
}).then(function (key) {
if ('der' === infile || 'der' === format) {
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;
}
var key = fs.readFileSync(infile, 'ascii'); var key = fs.readFileSync(infile, 'ascii');
try { try {
@ -46,24 +17,13 @@ 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));
@ -72,38 +32,10 @@ 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/rsa.js'); module.exports = require('./lib/rasha.js');

View File

@ -76,14 +76,13 @@ ASN1.BitStr = function BITSTR() {
// //
ASN1.ELOOP = "uASN1.js Error: iterated over 15+ elements (probably a malformed file)"; ASN1.ELOOP = "uASN1.js Error: iterated over 15+ elements (probably a malformed file)";
ASN1.EDEEP = "uASN1.js Error: element nested 20+ layers deep (probably a malformed file)"; ASN1.EDEEP = "uASN1.js Error: element nested 10+ layers deep (probably a malformed file)";
// Container Types are Sequence 0x30, Octect String 0x04, Array? (0xA0, 0xA1) // Container Types are Sequence 0x30, Octect String 0x04, Array? (0xA0, 0xA1)
// Value Types are Integer 0x02, Bit String 0x03, Null 0x05, Object ID 0x06, // Value Types are Integer 0x02, Bit String 0x03, Null 0x05, Object ID 0x06,
// Sometimes Bit String is used as a container (RSA Pub Spki) // Sometimes Bit String is used as a container (RSA Pub Spki)
ASN1.VTYPES = [ 0x02, 0x03, 0x05, 0x06, 0x0c, 0x82 ]; ASN1.VTYPES = [ 0x02, 0x03, 0x05, 0x06 ];
ASN1.parse = function parseAsn1(buf, depth, ws) { ASN1.parse = function parseAsn1(buf, depth) {
if (!ws) { ws = ''; } if (depth >= 10) { throw new Error(ASN1.EDEEP); }
if (depth >= 20) { throw new Error(ASN1.EDEEP); }
var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1) var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] }; var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
@ -109,9 +108,8 @@ ASN1.parse = function parseAsn1(buf, depth, ws) {
adjust = -1; adjust = -1;
} }
} }
adjustedLen = asn1.length + adjust;
//console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); adjustedLen = asn1.length + adjust;
// this is a primitive value type // this is a primitive value type
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) { if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
@ -120,31 +118,16 @@ ASN1.parse = function parseAsn1(buf, depth, ws) {
} }
asn1.children = []; asn1.children = [];
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0); while (iters < 15 && index <= asn1.length) {
while (iters < 15 && index < (2 + asn1.length + asn1.lengthSize)) {
iters += 1; iters += 1;
child = ASN1.parse(buf.slice(index, index + adjustedLen), (depth || 0) + 1, ws + ' '); child = ASN1.parse(buf.slice(index, index + adjustedLen), (depth || 0) + 1);
// The numbers don't match up exactly and I don't remember why...
// probably something with adjustedLen or some such, but the tests pass
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));
if (index > (2 + asn1.lengthSize + asn1.length)) {
console.error(JSON.stringify(asn1, toPrettyHex, 2));
throw new Error("Parse error: child value length (" + child.length
+ ") is greater than remaining parent length (" + (asn1.length - index)
+ " = " + asn1.length + " - " + index + ")");
}
asn1.children.push(child); asn1.children.push(child);
//console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
}
if (index !== (2 + asn1.lengthSize + asn1.length)) {
throw new Error("premature end-of-file (" + 'index: ' + index + ' length: ' + (2 + asn1.lengthSize + asn1.length) + ")");
} }
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) {
@ -153,7 +136,7 @@ ASN1._stringify = function(asn1) {
var ws = ''; var ws = '';
function write(asn1) { function write(asn1) {
console.log(ws, 'ch', Enc.numToHex(asn1.type), asn1.length); console.log(ws, 'ch', Buffer.from([asn1.type]).toString('hex'), asn1.length);
if (!asn1.children) { if (!asn1.children) {
return; return;
} }
@ -170,8 +153,7 @@ ASN1._stringify = function(asn1) {
ASN1.tpl = function (asn1) { ASN1.tpl = function (asn1) {
//console.log(JSON.stringify(asn1, null, 2)); //console.log(JSON.stringify(asn1, null, 2));
//console.log(asn1); //console.log(asn1);
var sp = ' '; var ws = '\t';
var ws = sp;
var i = 0; var i = 0;
var vars = []; var vars = [];
var str = ws; var str = ws;
@ -194,8 +176,8 @@ ASN1.tpl = function (asn1) {
if (!asn1.children) { if (!asn1.children) {
if (0x05 !== asn1.type) { if (0x05 !== asn1.type) {
if (0x06 !== asn1.type) { if (0x06 !== asn1.type) {
val = asn1.value || new Uint8Array(0); val = Buffer.from(asn1.value || '');
vars.push("\n// 0x" + Enc.numToHex(val.byteLength) + " (" + val.byteLength + " bytes)\nopts.tpl" + i + " = '" vars.push("// 0x" + Enc.numToHex(val.byteLength) + " (" + val.byteLength + " bytes)\nopts.tpl" + i + " = '"
+ Enc.bufToHex(val) + "';"); + Enc.bufToHex(val) + "';");
if (0x02 !== asn1.type && 0x03 !== asn1.type) { if (0x02 !== asn1.type && 0x03 !== asn1.type) {
str += ", "; str += ", ";
@ -204,30 +186,26 @@ ASN1.tpl = function (asn1) {
} else { } else {
str += ", '" + Enc.bufToHex(asn1.value) + "'"; str += ", '" + Enc.bufToHex(asn1.value) + "'";
} }
} else {
console.warn("XXXXXXXXXXXXXXXXXXXXX");
} }
str += ")"; str += ")";
return ; return ;
} }
asn1.children.forEach(function (a, j) { asn1.children.forEach(function (a, j) {
i += 1; i += 1;
ws += sp; ws += '\t';
write(a, j); write(a, j);
ws = ws.slice(sp.length); ws = ws.slice(1);
}); });
str += "\n" + ws + ")"; str += "\n" + ws + ")";
} }
write(asn1); write(asn1);
console.info('var opts = {};'); console.log('var opts = {};');
console.info(vars.join('\n') + '\n'); console.log(vars.join('\n') + '\n');
console.info(); console.log();
console.info('function buildSchema(opts) {'); console.log('function buildSchema(opts) {');
console.info(sp + 'return Enc.hexToBuf(' + str.slice(3) + ');'); console.log('\treturn Enc.hexToBuf(' + str.slice(3) + ');');
console.info('}'); console.log('}');
console.info();
console.info('buildSchema(opts);');
}; };
module.exports = ASN1; module.exports = ASN1;

View File

@ -1,51 +0,0 @@
'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,41 +2,11 @@
var Enc = module.exports; var Enc = module.exports;
Enc.base64ToBuf = function (str) { Enc.bufToHex = function toHex(u8) {
// 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);
for (i = 0; i < len; i += 1) { for (i = 0; i < u8.byteLength; i += 1) {
h = u8[i].toString(16); h = u8[i].toString(16);
if (2 !== h.length) { h = '0' + h; } if (2 !== h.length) { h = '0' + h; }
hex.push(h); hex.push(h);
@ -53,7 +23,7 @@ Enc.hexToBuf = function (hex) {
return Buffer.from(hex, 'hex'); return Buffer.from(hex, 'hex');
}; };
Enc.numToHex = function (d) { Enc.numToHex = function numToHex(d) {
d = d.toString(16); d = d.toString(16);
if (d.length % 2) { if (d.length % 2) {
return '0' + d; return '0' + d;
@ -61,17 +31,37 @@ Enc.numToHex = function (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.strToBase64 = function (str) { Enc.bufToUint8 = function bufToUint8(buf) {
// node automatically can tell the difference return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
// 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 (str) { Enc.strToBin = function strToBin(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
@ -83,16 +73,17 @@ Enc.strToBin = function (str) {
}; };
*/ */
Enc.strToBuf = function (str) { /*
return Buffer.from(str); Enc.strToBase64 = function strToBase64(str) {
}; // node automatically can tell the difference
// between uc2 (utf-8) strings and binary strings
Enc.strToHex = function (str) { // so we don't have to re-encode the strings
return Buffer.from(str).toString('hex'); return Buffer.from(str).toString('base64');
}; };
*/
/* /*
Enc.urlBase64ToBase64 = function (str) { Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) {
var r = str % 4; var r = str % 4;
if (2 === r) { if (2 === r) {
str += '=='; str += '==';
@ -102,3 +93,9 @@ Enc.urlBase64ToBase64 = function (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

@ -1,53 +0,0 @@
// 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

@ -1,23 +0,0 @@
// 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

@ -1,22 +0,0 @@
// 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);
}

View File

@ -1,64 +0,0 @@
// 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,24 +3,43 @@
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 lines = pem.trim().split(/\n/); var typ;
var end = lines.length - 1; var pub;
var head = lines[0].match(/-----BEGIN (.*)-----/); var hex;
var foot = lines[end].match(/-----END (.*)-----/); var der = Enc.base64ToBuf(pem.split(/\n/).filter(function (line, i) {
if (0 === i) {
if (head) { if (/ PUBLIC /.test(line)) {
lines = lines.slice(1, end); pub = true;
head = head[1]; } else if (/ PRIVATE /.test(line)) {
if (head !== foot[1]) { pub = false;
throw new Error("headers and footers do not match"); }
if (/ RSA /.test(line)) {
typ = 'RSA';
} }
} }
return !/---/.test(line);
}).join(''));
return { type: head, bytes: Enc.base64ToBuf(lines.join('')) }; if (!typ) {
hex = Enc.bufToHex(der);
if (-1 !== hex.indexOf(PEM.RSA_OBJID)) {
typ = 'RSA';
}
}
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 };
}; };
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,76 +5,8 @@ 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) {
opts.kty = "RSA";
return Crypto.generate(opts).then(function (pair) {
var format = opts.format;
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 pub;
if ('spki' === format || 'pkcs8' === format) {
format = 'pkcs8';
pub = 'spki';
}
if ('pem' === format) {
format = 'pkcs1';
encoding = 'pem';
} else if ('der' === format) {
format = 'pkcs1';
encoding = 'der';
}
priv = format;
pub = pub || format;
if (!encoding) {
encoding = 'pem';
}
if (priv) {
priv = { type: priv, format: encoding };
pub = { type: pub, format: encoding };
} else {
// jwk
priv = { type: 'pkcs1', format: 'pem' };
pub = { type: 'pkcs1', format: 'pem' };
}
*/
if (('pem' === format || 'der' === format) && !encoding) {
encoding = format;
format = 'pkcs1';
}
var exOpts = { jwk: pair.private, format: format, encoding: encoding };
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 { private: priv, public: pub };
});
});
});
};
RSA.importSync = function (opts) { RSA.importSync = function (opts) {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) { if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error("must pass { pem: pem } as a string"); throw new Error("must pass { pem: pem } as a string");
@ -87,18 +19,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.bytes); var asn1 = ASN1.parse(block.der);
var meta = x509.guess(block.bytes, asn1); var meta = x509.guess(block.der, asn1);
if ('pkcs1' === meta.format) { if ('pkcs1' === meta.format) {
jwk = x509.parsePkcs1(block.bytes, asn1, jwk); jwk = x509.parsePkcs1(block.der, asn1, jwk);
} else { } else {
jwk = x509.parsePkcs8(block.bytes, asn1, jwk); jwk = x509.parsePkcs8(block.der, asn1, jwk);
} }
if (opts.public) { if (opts.public) {
jwk = RSA.neuter(jwk); jwk = RSA.nueter(jwk);
} }
return jwk; return jwk;
}; };
@ -135,7 +67,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.neuter(jwk); jwk = RSA.nueter(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");
@ -189,37 +121,13 @@ 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!
var privates = [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ]; RSA.nueter = function (jwk) {
// fix misspelling without breaking the API // (snip rather than new object to keep potential extra data)
RSA.neuter = RSA.nueter = function (priv) { // otherwise we could just do this:
var pub = {}; // return { kty: jwk.kty, n: jwk.n, e: jwk.e };
Object.keys(priv).forEach(function (key) { [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ].forEach(function (key) {
if (!privates.includes(key)) { if (key in jwk) { jwk[key] = undefined; }
pub[key] = priv[key]; return jwk;
}
});
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;
}; };

111
lib/telemetry.js Normal file
View File

@ -0,0 +1,111 @@
'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,5 +1,7 @@
'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.3.0", "version": "1.0.2",
"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,8 @@
"lib": "lib" "lib": "lib"
}, },
"scripts": { "scripts": {
"test": "bash test.sh" "postinstall": "node lib/telemetry.js event:install",
"test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -35,8 +36,5 @@
"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"
}
} }

80
test.sh
View File

@ -1,14 +1,13 @@
#!/bin/bash #!/bin/bash
# cause errors to hard-fail
# (and diff non-0 exit status will cause failure)
set -e set -e
pemtojwk() { pemtojwk() {
keyid=$1 keyid=$1
if [ -z "$keyid" ]; then if [ -z "$keyid" ]; then
echo ""
echo "" echo ""
echo "Testing PEM-to-JWK PKCS#1" echo "Testing PEM-to-JWK PKCS#1"
echo ""
fi fi
# #
node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs1.${keyid}pem \ node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs1.${keyid}pem \
@ -18,14 +17,13 @@ pemtojwk() {
node bin/rasha.js ./fixtures/pub-rsa-2048.pkcs1.${keyid}pem \ node bin/rasha.js ./fixtures/pub-rsa-2048.pkcs1.${keyid}pem \
> ./fixtures/pub-rsa-2048.jwk.1.json > ./fixtures/pub-rsa-2048.jwk.1.json
diff ./fixtures/pub-rsa-2048.jwk.${keyid}json ./fixtures/pub-rsa-2048.jwk.1.json diff ./fixtures/pub-rsa-2048.jwk.${keyid}json ./fixtures/pub-rsa-2048.jwk.1.json
if [ -z "$keyid" ]; then
echo "Pass"
fi
if [ -z "$keyid" ]; then if [ -z "$keyid" ]; then
echo ""
echo "" echo ""
echo "Testing PEM-to-JWK PKCS#8" echo "Testing PEM-to-JWK PKCS#8"
echo ""
fi fi
# #
node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs8.${keyid}pem \ node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs8.${keyid}pem \
@ -35,16 +33,15 @@ pemtojwk() {
node bin/rasha.js ./fixtures/pub-rsa-2048.spki.${keyid}pem \ node bin/rasha.js ./fixtures/pub-rsa-2048.spki.${keyid}pem \
> ./fixtures/pub-rsa-2048.jwk.1.json > ./fixtures/pub-rsa-2048.jwk.1.json
diff ./fixtures/pub-rsa-2048.jwk.${keyid}json ./fixtures/pub-rsa-2048.jwk.1.json diff ./fixtures/pub-rsa-2048.jwk.${keyid}json ./fixtures/pub-rsa-2048.jwk.1.json
if [ -z "$keyid" ]; then
echo "Pass"
fi
} }
jwktopem() { jwktopem() {
keyid=$1 keyid=$1
if [ -z "$keyid" ]; then if [ -z "$keyid" ]; then
echo ""
echo "" echo ""
echo "Testing JWK-to-PEM PKCS#1" echo "Testing JWK-to-PEM PKCS#1"
echo ""
fi fi
# #
node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json pkcs1 \ node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json pkcs1 \
@ -54,13 +51,13 @@ jwktopem() {
node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json pkcs1 \ node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json pkcs1 \
> ./fixtures/pub-rsa-2048.pkcs1.1.pem > ./fixtures/pub-rsa-2048.pkcs1.1.pem
diff ./fixtures/pub-rsa-2048.pkcs1.${keyid}pem ./fixtures/pub-rsa-2048.pkcs1.1.pem diff ./fixtures/pub-rsa-2048.pkcs1.${keyid}pem ./fixtures/pub-rsa-2048.pkcs1.1.pem
if [ -z "$keyid" ]; then
echo "Pass"
fi
if [ -z "$keyid" ]; then if [ -z "$keyid" ]; then
echo ""
echo "" echo ""
echo "Testing JWK-to-PEM PKCS#8" echo "Testing JWK-to-PEM PKCS#8"
echo ""
fi fi
# #
node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json pkcs8 \ node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json pkcs8 \
@ -70,13 +67,13 @@ jwktopem() {
node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json spki \ node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json spki \
> ./fixtures/pub-rsa-2048.spki.1.pem > ./fixtures/pub-rsa-2048.spki.1.pem
diff ./fixtures/pub-rsa-2048.spki.${keyid}pem ./fixtures/pub-rsa-2048.spki.1.pem diff ./fixtures/pub-rsa-2048.spki.${keyid}pem ./fixtures/pub-rsa-2048.spki.1.pem
if [ -z "$keyid" ]; then
echo "Pass"
fi
if [ -z "$keyid" ]; then if [ -z "$keyid" ]; then
echo ""
echo "" echo ""
echo "Testing JWK-to-SSH" echo "Testing JWK-to-SSH"
echo ""
fi fi
# #
node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json ssh > ./fixtures/pub-rsa-2048.ssh.1.pub node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json ssh > ./fixtures/pub-rsa-2048.ssh.1.pub
@ -84,9 +81,6 @@ jwktopem() {
# #
node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json ssh > ./fixtures/pub-rsa-2048.ssh.1.pub node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json ssh > ./fixtures/pub-rsa-2048.ssh.1.pub
diff ./fixtures/pub-rsa-2048.ssh.${keyid}pub ./fixtures/pub-rsa-2048.ssh.1.pub diff ./fixtures/pub-rsa-2048.ssh.${keyid}pub ./fixtures/pub-rsa-2048.ssh.1.pub
if [ -z "$keyid" ]; then
echo "Pass"
fi
} }
rndkey() { rndkey() {
@ -122,65 +116,29 @@ pemtojwk ""
jwktopem "" jwktopem ""
echo "" echo ""
echo "testing node key generation"
echo "defaults"
node bin/rasha.js > /dev/null
echo "jwk"
node bin/rasha.js jwk > /dev/null
echo "json 2048"
node bin/rasha.js json 2048 > /dev/null
echo "der"
node bin/rasha.js der > /dev/null
echo "pkcs8 der"
node bin/rasha.js pkcs8 der > /dev/null
echo "pem"
node bin/rasha.js pem > /dev/null
echo "pkcs1"
node bin/rasha.js pkcs1 pem > /dev/null
echo "spki"
node bin/rasha.js spki > /dev/null
echo "PASS"
echo "" echo ""
echo "Testing different size random keys"
echo "" echo ""
echo "Re-running tests with random keys of varying sizes" rndkey 32 # minimum key size
echo "" rndkey 64
rndkey 128
# commented out sizes below 512, since they are below minimum size on some systems. rndkey 256
# rndkey 32 # minimum key size
# 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
if [ "${RASHA_TEST_LARGE_KEYS}" == "true" ]; then
rndkey 3072 rndkey 3072
rndkey 4096 # largest reasonable key size 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 4096 work as well, but they take minutes to generate, so we stop here."
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:"
echo "• All inputs produced valid outputs" echo "• All inputs produced valid outputs"
echo "• All outputs matched known-good values" echo "• All outputs matched known-good values"
echo "• All random tests passed reciprosity" echo ""