Compare commits

..

No commits in common. "master" and "v1.1.0" have entirely different histories.

18 changed files with 263 additions and 468 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,12 +1,10 @@
[Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js) [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js)
========= =========
A [Root](https://therootcompany.com) Project. 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)
| ~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.
@ -14,12 +12,10 @@ 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,
@ -36,7 +32,7 @@ Rasha.generate({ format: 'jwk' }).then(function (keypair) {
* `format` defaults to `'jwk'` * `format` defaults to `'jwk'`
* `'pkcs1'` (traditional) * `'pkcs1'` (traditional)
* `'pkcs8'` <!-- * `'ssh'` --> * `'pkcs8'`
* `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
@ -175,14 +171,6 @@ 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,33 +8,20 @@ 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.info("Generating new key..."); console.log("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) {
if ('der' === infile || 'der' === format) { console.log(key.private);
key.private = key.private.toString('binary'); console.log(key.public);
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;
} }
@ -46,24 +33,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 +48,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

@ -129,7 +129,9 @@ 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, toPrettyHex, 2)); console.error(JSON.stringify(asn1, function (k, v) {
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 + ")");
@ -138,13 +140,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)) {
throw new Error("premature end-of-file (" + 'index: ' + index + ' length: ' + (2 + asn1.lengthSize + asn1.length) + ")"); console.warn('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) {
@ -220,14 +222,14 @@ ASN1.tpl = function (asn1) {
} }
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(sp + 'return Enc.hexToBuf(' + str.slice(3) + ');');
console.info('}'); console.log('}');
console.info(); console.log();
console.info('buildSchema(opts);'); console.log('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,36 +2,7 @@
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); var len = (u8.byteLength || u8.length);
@ -53,7 +24,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 +32,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 +74,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 +94,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,31 +5,19 @@ 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) {
opts.kty = "RSA"; return Promise.resolve().then(function () {
return Crypto.generate(opts).then(function (pair) { var typ = 'rsa';
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';
@ -43,8 +31,13 @@ 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';
@ -58,19 +51,29 @@ RSA.generate = function (opts) {
priv = { type: 'pkcs1', format: 'pem' }; priv = { type: 'pkcs1', format: 'pem' };
pub = { type: 'pkcs1', format: 'pem' }; pub = { type: 'pkcs1', format: 'pem' };
} }
*/
if (('pem' === format || 'der' === format) && !encoding) { return new Promise(function (resolve, reject) {
encoding = format; return require('crypto').generateKeyPair(typ, {
format = 'pkcs1'; modulusLength: opts.modulusLength || 2048
, 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;
} }
var exOpts = { jwk: pair.private, format: format, encoding: encoding }; return {
return RSA.export(exOpts).then(function (priv) { private: RSA.importSync({ pem: keypair.private, format: priv.format })
exOpts.public = true; , public: RSA.importSync({ pem: keypair.public, format: pub.format, public: true })
if ('pkcs8' === exOpts.format) { exOpts.format = 'spki'; } };
return RSA.export(exOpts).then(function (pub) {
return { private: priv, public: pub };
});
}); });
}); });
}; };
@ -87,18 +90,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 +138,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 +192,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.1.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,6 +16,7 @@
"lib": "lib" "lib": "lib"
}, },
"scripts": { "scripts": {
"postinstall": "node lib/telemetry.js event:install",
"test": "bash test.sh" "test": "bash test.sh"
}, },
"repository": { "repository": {
@ -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"
}
} }

38
test.sh
View File

@ -123,21 +123,13 @@ 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"
@ -145,39 +137,23 @@ 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
# commented out sizes below 512, since they are below minimum size on some systems. rndkey 64
# rndkey 32 # minimum key size rndkey 128
# rndkey 64 rndkey 256
# 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
if [ "${RASHA_TEST_LARGE_KEYS}" == "true" ]; then #rndkey 4096 # largest reasonable key size
rndkey 3072 echo "Pass"
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:"