Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes #18

Closed
wants to merge 17 commits into from
Prev Previous commit
Next Next commit
encryption schemes support
  • Loading branch information
rzcoder committed Nov 20, 2014
commit 408b77920e8c82ce353d11fe30cd300003ac28ce
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Questions, comments, bug reports, and pull requests are all welcome.
* Signing now supports `'md5'`, `'ripemd160'`, `'sha1'`, `'sha256'`, `'sha512'` hash algorithms in both environments
and additional `'md4'`, `'sha'`, `'sha224'`, `'sha384'` for nodejs env.
* `options.signingAlgorithm` rename to `options.signingScheme`
* `key.options` now mark as private and unavailable for edit after key created.
* Property `key.options` now mark as private. Added `key.setOptions(options)` method.

### 0.1.54
* Added support for loading PEM key from Buffer (`fs.readFileSync()` output)
Expand Down
81 changes: 46 additions & 35 deletions src/NodeRSA.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ module.exports = (function() {

var DEFAULT_ENCRYPTION_SCHEME = 'pkcs1';
var DEFAULT_SIGNING_SCHEME = 'pkcs1';
var DEFAULT_SIGNING_HASH = 'sha256';


/**
* @param key {string|buffer|object} Key in PEM format, or data for generate key {b: bits, e: exponent}
Expand All @@ -34,8 +36,16 @@ module.exports = (function() {
return new NodeRSA(key, options);
}

this.$options = this.$$setupOptions(options);
this.keyPair = new rsa.Key(this.$options);
this.$options = {
signingScheme: DEFAULT_SIGNING_SCHEME,
signingSchemeOptions: {
hash: DEFAULT_SIGNING_HASH
},
encryptionScheme: DEFAULT_ENCRYPTION_SCHEME,
environment: utils.detectEnvironment()
};
this.keyPair = new rsa.Key();
this.setOptions(options);
this.$cache = {};

if (Buffer.isBuffer(key) || _.isString(key)) {
Expand All @@ -45,48 +55,49 @@ module.exports = (function() {
}
}

NodeRSA.prototype.$$setupOptions = function (options) {
//ToDo: object with getters/setters and validation on change
options = _.merge({
signingScheme: 'sha256',
encryptionScheme: DEFAULT_ENCRYPTION_SCHEME,
environment: utils.detectEnvironment()
}, options || {});

options.signingScheme = options.signingScheme.toLowerCase().split('-');
options.encryptionScheme = options.encryptionScheme.toLowerCase();

if (options.signingScheme.length == 1) {
options.signingOptions = {
hash: options.signingScheme[0]
};
options.signingScheme = DEFAULT_SIGNING_SCHEME;
} else {
options.signingOptions = {
hash: options.signingScheme[1]
};
options.signingScheme = options.signingScheme[0];
/**
* Set and validate options for key instance
* @param options
*/
NodeRSA.prototype.setOptions = function (options) {
options = options || {};
if (options.environment) {
this.$options.environment = options.environment;
}

if (!schemes.isSignature(options.signingScheme)) {
throw Error('Unsupported signing scheme');
}
if (options.signingScheme) {
var signingScheme = options.signingScheme.toLowerCase().split('-');
if (signingScheme.length == 1) {
this.$options.signingSchemeOptions = {
hash: signingScheme[0]
};
this.$options.signingScheme = DEFAULT_SIGNING_SCHEME;
} else {
this.$options.signingSchemeOptions = {
hash: options.signingScheme[1]
};
this.$options.signingScheme = options.signingScheme[0];
}

if (_.indexOf(SUPPORTED_HASH_ALGORITHMS[options.environment], options.signingOptions.hash) == -1) {
throw Error('Unsupported signing algorithm for ' + options.environment + ' environment');
}
if (!schemes.isSignature(this.$options.signingScheme)) {
throw Error('Unsupported signing scheme');
}

if (!schemes.isEncryption(options.encryptionScheme)) {
throw Error('Unsupported encryption scheme');
if (_.indexOf(SUPPORTED_HASH_ALGORITHMS[this.$options.environment], this.$options.signingSchemeOptions.hash) == -1) {
throw Error('Unsupported signing algorithm for ' + this.$options.environment + ' environment');
}
}

options.signingScheme = schemes[options.signingScheme];
options.encryptionScheme = schemes[options.encryptionScheme];
if (options.encryptionScheme) {
this.$options.encryptionScheme = options.encryptionScheme.toLowerCase();
if (!schemes.isEncryption(this.$options.encryptionScheme)) {
throw Error('Unsupported encryption scheme');
}
}

return options;
this.keyPair.setOptions(this.$options);
};


/**
* Generate private/public keys pair
*
Expand Down
18 changes: 12 additions & 6 deletions src/libs/rsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var _ = require('lodash');
var crypt = require('crypto');
var BigInteger = require('./jsbn.js');
var utils = require('../utils.js');
var schemes = require('../schemes/schemes.js');

exports.BigInteger = BigInteger;
module.exports.Key = (function() {
Expand All @@ -58,7 +59,7 @@ module.exports.Key = (function() {
* dmq1 - exponent2 -- d mod (q-1)
* coeff - coefficient -- (inverse of q) mod p
*/
function RSAKey(options) {
function RSAKey() {
this.n = null;
this.e = 0;
this.d = null;
Expand All @@ -67,14 +68,19 @@ module.exports.Key = (function() {
this.dmp1 = null;
this.dmq1 = null;
this.coeff = null;
}

RSAKey.prototype.setOptions = function (options) {
var signingSchemeProvider = schemes[options.signingScheme];
var encryptionSchemeProvider = schemes[options.encryptionScheme];

if (options.encryptionScheme == options.signingScheme) {
this.signingScheme = this.encryptionScheme = options.encryptionScheme.makeScheme(this);
if (signingSchemeProvider === encryptionSchemeProvider) {
this.signingScheme = this.encryptionScheme = encryptionSchemeProvider.makeScheme(this, options);
} else {
this.encryptionScheme = options.encryptionScheme.makeScheme(this);
this.signingScheme = options.signingScheme.makeScheme(this);
this.encryptionScheme = encryptionSchemeProvider.makeScheme(this, options);
this.signingScheme = signingSchemeProvider.makeScheme(this, options);
}
}
};

/**
* Generate a new random private key B bits long, using public expt E
Expand Down
10 changes: 10 additions & 0 deletions src/schemes/schemes.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
module.exports = schemes = {
pkcs1: require('./pkcs1'),

/**
* Check if scheme has padding methods
* @param scheme {string}
* @returns {Boolean}
*/
isEncryption: function (scheme) {
return schemes[scheme] && schemes[scheme].isEncryption;
},

/**
* Check if scheme has sign/verify methods
* @param scheme {string}
* @returns {Boolean}
*/
isSignature: function (scheme) {
return schemes[scheme] && schemes[scheme].isSignature;
}
Expand Down
134 changes: 85 additions & 49 deletions test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ describe("NodeRSA", function(){
{b: 1024} // 'e' should be 65537
];

var signAlgorithms = {
var environments = ['browser', 'node'];
var encryptSchemes = ['pkcs1'];
var signSchemes = ['pkcs1'];
var signHashAlgorithms = {
'node': ['MD4', 'MD5', 'RIPEMD160', 'SHA', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512'],
'browser': ['MD5', 'RIPEMD160', 'SHA1', 'SHA256', 'SHA512']
};

var environments = ['browser', 'node'];

var dataBundle = {
"string": {
data: "ascii + 12345678",
Expand Down Expand Up @@ -61,30 +61,60 @@ describe("NodeRSA", function(){
var publicNodeRSA = null;

describe("Work with keys", function(){

describe("Generating keys", function() {
for (var size in keySizes) {
(function(size){
it("should make key pair " + size.b + "-bit length and public exponent is " + (size.e ? size.e : size.e + " and should be 65537"), function () {
generatedKeys.push(new NodeRSA({b: size.b, e: size.e}));
assert.instanceOf(generatedKeys[generatedKeys.length - 1].keyPair, Object);
assert.equal(generatedKeys[generatedKeys.length - 1].isEmpty(), false);
assert.equal(generatedKeys[generatedKeys.length - 1].getKeySize(), size.b);
assert.equal(generatedKeys[generatedKeys.length - 1].getMaxMessageSize(), (size.b / 8 - 11));
assert.equal(generatedKeys[generatedKeys.length - 1].keyPair.e, size.e || 65537);
});
})(keySizes[size]);
}
describe("Good cases", function() {
it("should make empty key pair", function () {
var key = new NodeRSA(null);
assert.equal(key.isEmpty(), true);
});

it("should make empty key pair with pkcs1 scheme and hash alg", function () {
var key = new NodeRSA(null);
assert.equal(key.isEmpty(), true);
assert.equal(key.$options.signingScheme, 'pkcs1');
assert.equal(key.$options.signingSchemeOptions.hash, 'sha256');
});

it("should make empty key pair", function () {
var key = new NodeRSA(null);
assert.equal(key.isEmpty(), true);
it("should make key pair with pkcs1 scheme and md5 hash alg", function () {
var key = new NodeRSA(null, {signingScheme: 'md5'});
assert.equal(key.$options.signingScheme, 'pkcs1');
assert.equal(key.$options.signingSchemeOptions.hash, 'md5');
});

it("should make key pair with pss scheme and sha512 hash alg", function () {
var key = new NodeRSA(null, {signingScheme: 'pss-sha512'});
assert.equal(key.$options.signingScheme, 'pss');
assert.equal(key.$options.signingSchemeOptions.hash, 'sha512');
});

for (var size in keySizes) {
(function (size) {
it("should make key pair " + size.b + "-bit length and public exponent is " + (size.e ? size.e : size.e + " and should be 65537"), function () {
generatedKeys.push(new NodeRSA({b: size.b, e: size.e}));
assert.instanceOf(generatedKeys[generatedKeys.length - 1].keyPair, Object);
assert.equal(generatedKeys[generatedKeys.length - 1].isEmpty(), false);
assert.equal(generatedKeys[generatedKeys.length - 1].getKeySize(), size.b);
assert.equal(generatedKeys[generatedKeys.length - 1].getMaxMessageSize(), (size.b / 8 - 11));
assert.equal(generatedKeys[generatedKeys.length - 1].keyPair.e, size.e || 65537);
});
})(keySizes[size]);
}
});

it("should make empty key pair with md5 signing option", function () {
var key = new NodeRSA(null, {signingAlgorithm: 'md5'});
assert.equal(key.isEmpty(), true);
assert.equal(key.$options.signingAlgorithm, 'md5');
describe("Bad cases", function() {
it("should throw \"unsupported signing algorithm\" exception", function () {
var key = new NodeRSA(null);
assert.equal(key.isEmpty(), true);
assert.equal(key.$options.signingScheme, 'pkcs1');
assert.equal(key.$options.signingSchemeOptions.hash, 'sha256');

assert.throw(function(){
key.setOptions({
environment: 'browser',
signingScheme: 'md4'
});
}, Error, "Unsupported signing algorithm");
});
});
});

Expand Down Expand Up @@ -213,32 +243,39 @@ describe("NodeRSA", function(){
});
});

describe("Encrypting & decrypting", function(){
describe("Encrypting & decrypting", function () {
describe("Good cases", function () {
var encrypted = {};
var decrypted = {};

for(var i in dataBundle) {
(function(i) {
var key = null;
var suit = dataBundle[i];

it("should encrypt " + i, function () {
key = generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length];
encrypted[i] = key.encrypt(suit.data);
assert(Buffer.isBuffer(encrypted[i]));
assert(encrypted[i].length > 0);
});

it("should decrypt " + i, function () {
decrypted[i] = key.decrypt(encrypted[i], _.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding);
if(Buffer.isBuffer(decrypted[i])) {
assert.equal(suit.data.toString('hex'), decrypted[i].toString('hex'));
} else {
assert(_.isEqual(suit.data, decrypted[i]));
for (var scheme_i in encryptSchemes) {
(function (scheme) {
describe("Encryption scheme: " + scheme, function () {
for (var i in dataBundle) {
(function (i) {
var key = null;
var suit = dataBundle[i];

it("should encrypt " + i, function () {
key = generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length];
key.setOptions({encryptionScheme: scheme});
encrypted[i] = key.encrypt(suit.data);
assert(Buffer.isBuffer(encrypted[i]));
assert(encrypted[i].length > 0);
});

it("should decrypt " + i, function () {
decrypted[i] = key.decrypt(encrypted[i], _.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding);
if (Buffer.isBuffer(decrypted[i])) {
assert.equal(suit.data.toString('hex'), decrypted[i].toString('hex'));
} else {
assert(_.isEqual(suit.data, decrypted[i]));
}
});
})(i);
}
});
})(i);
})(encryptSchemes[scheme_i]);
}
});

Expand All @@ -256,7 +293,6 @@ describe("NodeRSA", function(){
});
});


describe("Signing & verifying", function () {
for(var env in environments) {
(function(env) {
Expand All @@ -280,14 +316,14 @@ describe("NodeRSA", function(){
})(i);
}

for (var alg in signAlgorithms[env]) {
for (var alg in signHashAlgorithms[env]) {
(function (alg) {
it("signing with custom algorithm (" + alg + ")", function () {
var key = new NodeRSA(generatedKeys[5].getPrivatePEM(), {signingAlgorithm: alg, environment: env});
var signed = key.sign('data');
assert(key.verify('data', signed));
});
})(signAlgorithms[env][alg]);
})(signHashAlgorithms[env][alg]);
}

});
Expand Down Expand Up @@ -332,7 +368,7 @@ describe("NodeRSA", function(){
}

describe("Compatibility of different environments", function () {
for (var alg in signAlgorithms['browser']) {
for (var alg in signHashAlgorithms['browser']) {
(function (alg) {
it("signing with custom algorithm (" + alg + ") (equal test)", function () {
var nodeKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), {signingAlgorithm: alg, environment: 'node'});
Expand All @@ -354,7 +390,7 @@ describe("NodeRSA", function(){

assert(nodeKey.verify('data', browserKey.sign('data')));
});
})(signAlgorithms['browser'][alg]);
})(signHashAlgorithms['browser'][alg]);
}
});
});
Expand Down