diff --git a/.gitignore b/.gitignore index 263ba4d..d7ad7b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store .idea +.tmp node_modules/ \ No newline at end of file diff --git a/README.md b/README.md index 8917243..26c9025 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Based on jsbn library from Tom Wu http://www-cs-students.stanford.edu/~tjw/jsbn/ * Generating keys * Supports long messages for encrypt/decrypt * Signing and verifying - + ## Example @@ -48,22 +48,48 @@ var NodeRSA = require('node-rsa'); var key = new NodeRSA([key], [options]); ``` + **key** - parameters of a generated key or the key in PEM format.
**options** - additional settings - * **environment** - working environment, `'browser'` or `'node'`. Default autodetect. - * **signingAlgorithm** - hash algorithm used for signing and verifying. Can be `'sha1'`, `'sha256'`, `'md5'`. Default `'sha256'`. -#### "Empty" key +#### Options +You can specify some options when key create (by second constructor argument) or over `key.setOptions()` method. + +* **environment** - working environment, `'browser'` or `'node'`. Default autodetect. +* **encryptionScheme** - padding scheme for encrypt/decrypt. Can be `'pkcs1_oaep'` or `'pkcs1'`. Default `'pkcs1_oaep'`. +* **signingScheme** - scheme used for signing and verifying. Can be `'pkcs1'` or `'pss'` or 'scheme-hash' format string (eg `'pss-sha1'`). Default `'pkcs1-sha256'`, or, if chosen pss: `'pss-sha1'`. + +**Advanced options:**
+You also can specify advanced options for some schemes like this: +``` +options = { + encryptionScheme: { + scheme: 'pkcs1_oaep', //scheme + hash: 'md5', //hash using for scheme + mgf: function(...) {...} //mask generation function + }, + signingScheme: { + scheme: 'pss', //scheme + hash: 'sha1', //hash using for scheme + saltLength: 20 //salt length for pss sign + } +} +``` + +This lib supporting next hash algorithms: `'md5'`, `'ripemd160'`, `'sha1'`, `'sha256'`, `'sha512'` in browser and node environment and additional `'md4'`, `'sha'`, `'sha224'`, `'sha384'` in node only. + + +#### Creating "empty" key ```javascript var key = new NodeRSA(); ``` -### Generate new key 512bit-length and with public exponent 65537 +#### Generate new key 512bit-length and with public exponent 65537 ```javascript var key = new NodeRSA({b: 512}); ``` -### Load key from PEM string +#### Load key from PEM string ```javascript var key = new NodeRSA('-----BEGIN RSA PRIVATE KEY-----\n'+ @@ -155,6 +181,16 @@ Questions, comments, bug reports, and pull requests are all welcome. ## Changelog +### 0.2.0 + * Added PKCS1_OAEP encrypting/decrypting support + * **PKCS1_OAEP now default scheme, you need to specify 'encryptingScheme' option to 'pkcs1' for compatibility with 0.1.x version of NodeRSA** + * Added PSS signing/verifying support + * 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` + * Added `encryptingScheme` option + * 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) * Added `isEmpty()` method diff --git a/gruntfile.js b/gruntfile.js index dbacacf..5cf07bf 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -1,11 +1,10 @@ -module.exports = function(grunt) { +module.exports = function (grunt) { grunt.initConfig({ jshint: { - options: { - }, + options: {}, default: { files: { - src: ['src/**/*.js', '!src/libs/**/*'] + src: ['gruntfile.js', 'src/**/*.js', '!src/libs/jsbn.js'] } }, libs: { @@ -19,7 +18,7 @@ module.exports = function(grunt) { options: { reporter: 'List' }, - all: { src: ['test/**/*.js'] } + all: {src: ['test/**/*.js']} } }); @@ -27,9 +26,8 @@ module.exports = function(grunt) { 'simplemocha': 'grunt-simple-mocha' }); - grunt.registerTask('lint', ['jshint:default']); grunt.registerTask('test', ['simplemocha']); grunt.registerTask('default', ['lint', 'test']); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/package.json b/package.json index cc7654c..d3be58c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-rsa", - "version": "0.1.54", + "version": "0.2.0", "description": "Node.js RSA library", "main": "src/NodeRSA.js", "scripts": { @@ -18,7 +18,10 @@ "encryption", "decryption", "sign", - "verify" + "verify", + "pkcs1", + "oaep", + "pss" ], "author": "rzcoder", "license": "BSD", diff --git a/src/NodeRSA.js b/src/NodeRSA.js index 32a09fe..d672a86 100644 --- a/src/NodeRSA.js +++ b/src/NodeRSA.js @@ -12,27 +12,46 @@ var crypt = require('crypto'); var ber = require('asn1').Ber; var _ = require('lodash'); var utils = require('./utils'); +var schemes = require('./schemes/schemes.js'); var PUBLIC_RSA_OID = '1.2.840.113549.1.1.1'; -module.exports = (function() { +module.exports = (function () { + var SUPPORTED_HASH_ALGORITHMS = { + node: ['md4', 'md5', 'ripemd160', 'sha', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'], + browser: ['md5', 'ripemd160', 'sha1', 'sha256', 'sha512'] + }; + + var DEFAULT_ENCRYPTION_SCHEME = 'pkcs1_oaep'; + var DEFAULT_SIGNING_SCHEME = 'pkcs1'; + /** * @param key {string|buffer|object} Key in PEM format, or data for generate key {b: bits, e: exponent} * @constructor */ function NodeRSA(key, options) { - if (! this instanceof NodeRSA) { + if (!this instanceof NodeRSA) { return new NodeRSA(key, options); } + this.$options = { + signingScheme: DEFAULT_SIGNING_SCHEME, + signingSchemeOptions: { + hash: 'sha256', + saltLength: null + }, + encryptionScheme: DEFAULT_ENCRYPTION_SCHEME, + encryptionSchemeOptions: { + hash: 'sha1', + label: null + }, + environment: utils.detectEnvironment(), + rsaUtils: this + }; this.keyPair = new rsa.Key(); + this.setOptions(options); this.$cache = {}; - this.options = _.merge({ - signingAlgorithm: 'sha256', - environment: utils.detectEnvironment() - }, options || {}); - if (Buffer.isBuffer(key) || _.isString(key)) { this.loadFromPEM(key); } else if (_.isObject(key)) { @@ -40,6 +59,73 @@ module.exports = (function() { } } + /** + * 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 (options.signingScheme) { + if (_.isString(options.signingScheme)) { + var signingScheme = options.signingScheme.toLowerCase().split('-'); + if (signingScheme.length == 1) { + if (_.indexOf(SUPPORTED_HASH_ALGORITHMS.node, signingScheme[0]) > -1) { + this.$options.signingSchemeOptions = { + hash: signingScheme[0] + }; + this.$options.signingScheme = DEFAULT_SIGNING_SCHEME; + } else { + this.$options.signingScheme = signingScheme[0]; + this.$options.signingSchemeOptions = { + hash: null + }; + } + } else { + this.$options.signingSchemeOptions = { + hash: signingScheme[1] + }; + this.$options.signingScheme = signingScheme[0]; + } + } else if (_.isObject(options.signingScheme)) { + this.$options.signingScheme = options.signingScheme.scheme || DEFAULT_SIGNING_SCHEME; + this.$options.signingSchemeOptions = _.omit(options.signingScheme, 'scheme'); + } + + if (!schemes.isSignature(this.$options.signingScheme)) { + throw Error('Unsupported signing scheme'); + } + if (this.$options.signingSchemeOptions.hash && + _.indexOf(SUPPORTED_HASH_ALGORITHMS[this.$options.environment], this.$options.signingSchemeOptions.hash) == -1) { + throw Error('Unsupported hashing algorithm for ' + this.$options.environment + ' environment'); + } + } + + if (options.encryptionScheme) { + if (_.isString(options.encryptionScheme)) { + this.$options.encryptionScheme = options.encryptionScheme.toLowerCase(); + this.$options.encryptionSchemeOptions = {}; + } else if (_.isObject(options.encryptionScheme)) { + this.$options.encryptionScheme = options.encryptionScheme.scheme || DEFAULT_ENCRYPTION_SCHEME; + this.$options.encryptionSchemeOptions = _.omit(options.encryptionScheme, 'scheme'); + } + + if (!schemes.isEncryption(this.$options.encryptionScheme)) { + throw Error('Unsupported encryption scheme'); + } + + if (this.$options.encryptionSchemeOptions.hash && + _.indexOf(SUPPORTED_HASH_ALGORITHMS[this.$options.environment], this.$options.encryptionSchemeOptions.hash) == -1) { + throw Error('Unsupported hashing algorithm for ' + this.$options.environment + ' environment'); + } + } + + this.keyPair.setOptions(this.$options); + }; + /** * Generate private/public keys pair * @@ -47,7 +133,7 @@ module.exports = (function() { * @param exp {int} public exponent. Default 65537. * @returns {NodeRSA} */ - NodeRSA.prototype.generateKeyPair = function(bits, exp) { + NodeRSA.prototype.generateKeyPair = function (bits, exp) { bits = bits || 2048; exp = exp || 65537; @@ -64,7 +150,7 @@ module.exports = (function() { * Load key from PEM string * @param pem {string} */ - NodeRSA.prototype.loadFromPEM = function(pem) { + NodeRSA.prototype.loadFromPEM = function (pem) { if (Buffer.isBuffer(pem)) { pem = pem.toString('utf8'); } @@ -84,10 +170,10 @@ module.exports = (function() { * * @param privatePEM {string} */ - NodeRSA.prototype.$loadFromPrivatePEM = function(privatePEM, encoding) { + NodeRSA.prototype.$loadFromPrivatePEM = function (privatePEM, encoding) { var pem = privatePEM - .replace('-----BEGIN RSA PRIVATE KEY-----','') - .replace('-----END RSA PRIVATE KEY-----','') + .replace('-----BEGIN RSA PRIVATE KEY-----', '') + .replace('-----END RSA PRIVATE KEY-----', '') .replace(/\s+|\n\r|\n|\r$/gm, ''); var reader = new ber.Reader(new Buffer(pem, 'base64')); @@ -111,10 +197,10 @@ module.exports = (function() { * * @param publicPEM {string} */ - NodeRSA.prototype.$loadFromPublicPEM = function(publicPEM, encoding) { + NodeRSA.prototype.$loadFromPublicPEM = function (publicPEM, encoding) { var pem = publicPEM - .replace('-----BEGIN PUBLIC KEY-----','') - .replace('-----END PUBLIC KEY-----','') + .replace('-----BEGIN PUBLIC KEY-----', '') + .replace('-----END PUBLIC KEY-----', '') .replace(/\s+|\n\r|\n|\r$/gm, ''); var reader = new ber.Reader(new Buffer(pem, 'base64')); @@ -136,7 +222,7 @@ module.exports = (function() { /** * Check if key pair contains private key */ - NodeRSA.prototype.isPrivate = function() { + NodeRSA.prototype.isPrivate = function () { return this.keyPair.n && this.keyPair.e && this.keyPair.d || false; }; @@ -144,14 +230,14 @@ module.exports = (function() { * Check if key pair contains public key * @param strict {boolean} - public key only, return false if have private exponent */ - NodeRSA.prototype.isPublic = function(strict) { + NodeRSA.prototype.isPublic = function (strict) { return this.keyPair.n && this.keyPair.e && !(strict && this.keyPair.d) || false; }; /** * Check if key pair doesn't contains any data */ - NodeRSA.prototype.isEmpty = function(strict) { + NodeRSA.prototype.isEmpty = function (strict) { return !(this.keyPair.n || this.keyPair.e || this.keyPair.d); }; @@ -163,13 +249,17 @@ module.exports = (function() { * @param source_encoding {string} - optional. Encoding for given string. Default utf8. * @returns {string|Buffer} */ - NodeRSA.prototype.encrypt = function(buffer, encoding, source_encoding) { - var res = this.keyPair.encrypt(this.$getDataForEcrypt(buffer, source_encoding)); + NodeRSA.prototype.encrypt = function (buffer, encoding, source_encoding) { + try { + var res = this.keyPair.encrypt(this.$getDataForEcrypt(buffer, source_encoding)); - if (encoding == 'buffer' || !encoding) { - return res; - } else { - return res.toString(encoding); + if (encoding == 'buffer' || !encoding) { + return res; + } else { + return res.toString(encoding); + } + } catch (e) { + throw Error('Error during encryption. Original error: ' + e); } }; @@ -180,9 +270,17 @@ module.exports = (function() { * @param encoding - encoding for result string, can also take 'json' or 'buffer' for the automatic conversion of this type * @returns {Buffer|object|string} */ - NodeRSA.prototype.decrypt = function(buffer, encoding) { - buffer = _.isString(buffer) ? new Buffer(buffer, 'base64') : buffer; - return this.$getDecryptedData(this.keyPair.decrypt(buffer), encoding); + NodeRSA.prototype.decrypt = function (buffer, encoding) { + try { + buffer = _.isString(buffer) ? new Buffer(buffer, 'base64') : buffer; + var res = this.keyPair.decrypt(buffer); + if (res === null) { + throw Error('Key decrypt method returns null.'); + } + return this.$getDecryptedData(res, encoding); + } catch (e) { + throw Error('Error during decryption (probably incorrect key). Original error: ' + e); + } }; /** @@ -193,24 +291,16 @@ module.exports = (function() { * @param source_encoding {string} - optional. Encoding for given string. Default utf8. * @returns {string|Buffer} */ - NodeRSA.prototype.sign = function(buffer, encoding, source_encoding) { + NodeRSA.prototype.sign = function (buffer, encoding, source_encoding) { if (!this.isPrivate()) { throw Error("It is not private key"); } + var res = this.keyPair.sign(this.$getDataForEcrypt(buffer, source_encoding)); - if (this.options.environment == 'browser') { - var res = this.keyPair.sign(this.$getDataForEcrypt(buffer, source_encoding), this.options.signingAlgorithm.toLowerCase()); - if (encoding && encoding != 'buffer') { - return res.toString(encoding); - } else { - return res; - } - } else { - encoding = (!encoding || encoding == 'buffer' ? null : encoding); - var signer = crypt.createSign('RSA-' + this.options.signingAlgorithm.toUpperCase()); - signer.update(this.$getDataForEcrypt(buffer, source_encoding)); - return signer.sign(this.getPrivatePEM(), encoding); + if (encoding && encoding != 'buffer') { + res = res.toString(encoding); } + return res; }; /** @@ -222,27 +312,18 @@ module.exports = (function() { * @param signature_encoding - optional. Encoding of given signature. May be 'buffer', 'binary', 'hex' or 'base64'. Default 'buffer'. * @returns {*} */ - NodeRSA.prototype.verify = function(buffer, signature, source_encoding, signature_encoding) { + NodeRSA.prototype.verify = function (buffer, signature, source_encoding, signature_encoding) { if (!this.isPublic()) { throw Error("It is not public key"); } - signature_encoding = (!signature_encoding || signature_encoding == 'buffer' ? null : signature_encoding); - - if (this.options.environment == 'browser') { - return this.keyPair.verify(this.$getDataForEcrypt(buffer, source_encoding), signature, signature_encoding, this.options.signingAlgorithm.toLowerCase()); - } else { - var verifier = crypt.createVerify('RSA-' + this.options.signingAlgorithm.toUpperCase()); - verifier.update(this.$getDataForEcrypt(buffer, source_encoding)); - return verifier.verify(this.getPublicPEM(), signature, signature_encoding); - } + return this.keyPair.verify(this.$getDataForEcrypt(buffer, source_encoding), signature, signature_encoding); }; NodeRSA.prototype.getPrivatePEM = function () { if (!this.isPrivate()) { throw Error("It is not private key"); } - return this.$cache.privatePEM; }; @@ -250,7 +331,6 @@ module.exports = (function() { if (!this.isPublic()) { throw Error("It is not public key"); } - return this.$cache.publicPEM; }; @@ -269,7 +349,7 @@ module.exports = (function() { * @param encoding {string} - optional. Encoding for given string. Default utf8. * @returns {Buffer} */ - NodeRSA.prototype.$getDataForEcrypt = function(buffer, encoding) { + NodeRSA.prototype.$getDataForEcrypt = function (buffer, encoding) { if (_.isString(buffer) || _.isNumber(buffer)) { return new Buffer('' + buffer, encoding || 'utf8'); } else if (Buffer.isBuffer(buffer)) { @@ -287,7 +367,7 @@ module.exports = (function() { * @param encoding - optional. Encoding for result output. May be 'buffer', 'json' or any of Node.js Buffer supported encoding. * @returns {*} */ - NodeRSA.prototype.$getDecryptedData = function(buffer, encoding) { + NodeRSA.prototype.$getDecryptedData = function (buffer, encoding) { encoding = encoding || 'buffer'; if (encoding == 'buffer') { @@ -303,7 +383,7 @@ module.exports = (function() { * private * Recalculating properties */ - NodeRSA.prototype.$recalculateCache = function() { + NodeRSA.prototype.$recalculateCache = function () { this.$cache.privatePEM = this.$makePrivatePEM(); this.$cache.publicPEM = this.$makePublicPEM(); }; @@ -312,7 +392,7 @@ module.exports = (function() { * private * @returns {string} private PEM string */ - NodeRSA.prototype.$makePrivatePEM = function() { + NodeRSA.prototype.$makePrivatePEM = function () { if (!this.isPrivate()) { return null; } @@ -349,7 +429,7 @@ module.exports = (function() { * private * @returns {string} public PEM string */ - NodeRSA.prototype.$makePublicPEM = function() { + NodeRSA.prototype.$makePublicPEM = function () { if (!this.isPublic()) { return null; } diff --git a/src/libs/jsbn.js b/src/libs/jsbn.js index eee75a2..3bca543 100644 --- a/src/libs/jsbn.js +++ b/src/libs/jsbn.js @@ -37,6 +37,7 @@ */ var crypt = require('crypto'); +var _ = require('lodash'); // Bits per digit var dbits; @@ -815,9 +816,26 @@ function bnToByteArray() { * @param trim {boolean} slice buffer if first element == 0 * @returns {Buffer} */ -function bnToBuffer(trim) { +function bnToBuffer(trimOrSize) { var res = new Buffer(this.toByteArray()); - return trim && res[0] === 0 ? res.slice(1) : res; + if (trimOrSize === true && res[0] === 0) { + res = res.slice(1); + } else if (_.isNumber(trimOrSize)) { + if (res.length > trimOrSize) { + for (var i = 0; i < res.length - trimOrSize; i++) { + if (res[i] !== 0) { + return null; + } + } + return res.slice(res.length - trimOrSize); + } else if (res.length < trimOrSize) { + var padded = new Buffer(trimOrSize); + padded.fill(0, 0, trimOrSize - res.length); + res.copy(padded, trimOrSize - res.length); + return padded; + } + } + return res; } function bnEquals(a) { diff --git a/src/libs/rsa.js b/src/libs/rsa.js index 7dbb9e1..245bb29 100644 --- a/src/libs/rsa.js +++ b/src/libs/rsa.js @@ -39,16 +39,11 @@ * 2014 rzcoder */ +var _ = require('lodash'); var crypt = require('crypto'); -var BigInteger = require("./jsbn.js"); +var BigInteger = require('./jsbn.js'); var utils = require('../utils.js'); -var _ = require('lodash'); - -var SIGNINFOHEAD = { - sha1: new Buffer('3021300906052b0e03021a05000414','hex'), - sha256: new Buffer('3031300d060960864801650304020105000420','hex'), - md5: new Buffer('3020300c06082a864886f70d020505000410','hex') -}; +var schemes = require('../schemes/schemes.js'); exports.BigInteger = BigInteger; module.exports.Key = (function() { @@ -75,6 +70,18 @@ module.exports.Key = (function() { this.coeff = null; } + RSAKey.prototype.setOptions = function (options) { + var signingSchemeProvider = schemes[options.signingScheme]; + var encryptionSchemeProvider = schemes[options.encryptionScheme]; + + if (signingSchemeProvider === encryptionSchemeProvider) { + this.signingScheme = this.encryptionScheme = encryptionSchemeProvider.makeScheme(this, options); + } else { + 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 * @param B @@ -169,15 +176,17 @@ module.exports.Key = (function() { * @returns {*} */ RSAKey.prototype.$doPrivate = function (x) { - if (this.p || this.q) + if (this.p || this.q) { return x.modPow(this.d, this.n); + } // TODO: re-calculate any missing CRT params var xp = x.mod(this.p).modPow(this.dmp1, this.p); var xq = x.mod(this.q).modPow(this.dmq1, this.q); - while (xp.compareTo(xq) < 0) + while (xp.compareTo(xq) < 0) { xp = xp.add(this.p); + } return xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq); }; @@ -200,7 +209,6 @@ module.exports.Key = (function() { RSAKey.prototype.encrypt = function (buffer) { var buffers = []; var results = []; - var bufferSize = buffer.length; var buffersCount = Math.ceil(bufferSize / this.maxMessageLength) || 1; // total buffers count for encrypt var dividedSize = Math.ceil(bufferSize / buffersCount || 1); // each buffer size @@ -216,23 +224,18 @@ module.exports.Key = (function() { for(var i in buffers) { var buf = buffers[i]; - var m = this.$$pkcs1pad2(buf); - - if (m === null) { - return null; - } - + var m = new BigInteger(this.encryptionScheme.encPad(buf)); var c = this.$doPublic(m); if (c === null) { return null; } - var encryptedBuffer = c.toBuffer(true); - + var encryptedBuffer = c.toBuffer(this.encryptedDataLength); + /*var encryptedBuffer = c.toBuffer(true); while (encryptedBuffer.length < this.encryptedDataLength) { encryptedBuffer = Buffer.concat([new Buffer([0]), encryptedBuffer]); - } + }*/ results.push(encryptedBuffer); } @@ -246,14 +249,13 @@ module.exports.Key = (function() { * @returns {Buffer} */ RSAKey.prototype.decrypt = function (buffer) { - if (buffer.length % this.encryptedDataLength > 0) + if (buffer.length % this.encryptedDataLength > 0) { throw Error('Incorrect data or key'); + } var result = []; - var offset = 0; var length = 0; - var buffersCount = buffer.length / this.encryptedDataLength; for (var i = 0; i < buffersCount; i++) { @@ -261,46 +263,19 @@ module.exports.Key = (function() { length = offset + this.encryptedDataLength; var c = new BigInteger(buffer.slice(offset, Math.min(length, buffer.length))); - var m = this.$doPrivate(c); - - if (m === null) { - return null; - } - - result.push(this.$$pkcs1unpad2(m)); + result.push(this.encryptionScheme.encUnPad(m.toBuffer(this.encryptedDataLength))); } return Buffer.concat(result); }; - RSAKey.prototype.sign = function (buffer, hashAlgorithm) { - var hasher = crypt.createHash(hashAlgorithm); - hasher.update(buffer); - var hash = this.$$pkcs1(hasher.digest(), hashAlgorithm); - var encryptedBuffer = this.$doPrivate(new BigInteger(hash)).toBuffer(true); - - while (encryptedBuffer.length < this.encryptedDataLength) { - encryptedBuffer = Buffer.concat([new Buffer([0]), encryptedBuffer]); - } - - return encryptedBuffer; - + RSAKey.prototype.sign = function (buffer) { + return this.signingScheme.sign.apply(this.signingScheme, arguments); }; - RSAKey.prototype.verify = function (buffer, signature, signature_encoding, hashAlgorithm) { - - if (signature_encoding) { - signature = new Buffer(signature, signature_encoding); - } - - var hasher = crypt.createHash(hashAlgorithm); - hasher.update(buffer); - - var hash = this.$$pkcs1(hasher.digest(), hashAlgorithm); - var m = this.$doPublic(new BigInteger(signature)); - - return m.toBuffer().toString('hex') == hash.toString('hex'); + RSAKey.prototype.verify = function (buffer, signature, signature_encoding) { + return this.signingScheme.verify.apply(this.signingScheme, arguments); }; Object.defineProperty(RSAKey.prototype, 'keySize', { @@ -312,7 +287,7 @@ module.exports.Key = (function() { }); Object.defineProperty(RSAKey.prototype, 'maxMessageLength', { - get: function() { return this.encryptedDataLength - 11; } + get: function() { return this.encryptionScheme.maxMessageLength(); } }); /** @@ -329,93 +304,6 @@ module.exports.Key = (function() { this.cache.keyByteLength = (this.cache.keyBitLength + 6) >> 3; }; - /** - * PKCS#1 pad input buffer to max data length - * @param hashBuf - * @param hashAlgorithm - * @param n - * @returns {*} - */ - RSAKey.prototype.$$pkcs1 = function (hashBuf, hashAlgorithm, n) { - if(!SIGNINFOHEAD[hashAlgorithm]) - throw Error('Unsupported hash algorithm'); - - var data = Buffer.concat([SIGNINFOHEAD[hashAlgorithm], hashBuf]); - - if (data.length + 10 > this.encryptedDataLength) { - throw Error('Key is too short for signing algorithm (' + hashAlgorithm + ')'); - } - - var filled = new Buffer(this.encryptedDataLength - data.length - 1); - filled.fill(0xff, 0, filled.length - 1); - filled[0] = 1; - filled[filled.length - 1] = 0; - - var res = Buffer.concat([filled, data]); - - return res; - }; - - /** - * PKCS#1 (type 2, random) pad input buffer to encryptedDataLength bytes, and return a bigint - * @param buffer - * @returns {*} - */ - RSAKey.prototype.$$pkcs1pad2 = function (buffer) { - if (buffer.length > this.maxMessageLength) { - throw new Error("Message too long for RSA (n=" + this.encryptedDataLength + ", l=" + buffer.length + ")"); - } - - // TO-DO: make n-length buffer - var ba = Array.prototype.slice.call(buffer, 0); - - // random padding - ba.unshift(0); - var rand = crypt.randomBytes(this.encryptedDataLength - ba.length - 2); - for(var i = 0; i < rand.length; i++) { - var r = rand[i]; - while (r === 0) { // non-zero only - r = crypt.randomBytes(1)[0]; - } - ba.unshift(r); - } - ba.unshift(2); - ba.unshift(0); - - return new BigInteger(ba); - }; - - /** - * Undo PKCS#1 (type 2, random) padding and, if valid, return the plaintext - * @param d - * @returns {Buffer} - */ - RSAKey.prototype.$$pkcs1unpad2 = function (d) { - var b = d.toByteArray(); - var i = 0; - while (i < b.length && b[i] === 0) { - ++i; - } - - if (b.length - i != this.encryptedDataLength - 1 || b[i] != 2) { - return null; - } - - ++i; - while (b[i] !== 0) { - if (++i >= b.length) { - return null; - } - } - - var c = 0; - var res = new Buffer(b.length - i - 1); - while (++i < b.length) { - res[c++] = b[i] & 255; - } - return res; - }; - return RSAKey; })(); diff --git a/src/schemes/oaep.js b/src/schemes/oaep.js new file mode 100644 index 0000000..867233b --- /dev/null +++ b/src/schemes/oaep.js @@ -0,0 +1,180 @@ +/** + * PKCS_OAEP signature scheme + */ + +var BigInteger = require('../libs/jsbn'); +var crypt = require('crypto'); + +module.exports = { + isEncryption: true, + isSignature: false +}; + +module.exports.digestLength = { + md4: 16, + md5: 16, + ripemd160: 20, + rmd160: 20, + sha: 20, + sha1: 20, + sha224: 28, + sha256: 32, + sha384: 48, + sha512: 64 +}; + +var DEFAULT_HASH_FUNCTION = 'sha1'; + +/* + * OAEP Mask Generation Function 1 + * Generates a buffer full of pseudorandom bytes given seed and maskLength. + * Giving the same seed, maskLength, and hashFunction will result in the same exact byte values in the buffer. + * + * https://tools.ietf.org/html/rfc3447#appendix-B.2.1 + * + * Parameters: + * seed [Buffer] The pseudo random seed for this function + * maskLength [int] The length of the output + * hashFunction [String] The hashing function to use. Will accept any valid crypto hash. Default "sha1" + * Supports "sha1" and "sha256". + * To add another algorythm the algorythem must be accepted by crypto.createHash, and then the length of the output of the hash function (the digest) must be added to the digestLength object below. + * Most RSA implementations will be expecting sha1 + */ +module.exports.eme_oaep_mgf1 = function (seed, maskLength, hashFunction) { + hashFunction = hashFunction || DEFAULT_HASH_FUNCTION; + var hLen = module.exports.digestLength[hashFunction]; + var count = Math.ceil(maskLength / hLen); + var T = new Buffer(hLen * count); + var c = new Buffer(4); + for (var i = 0; i < count; ++i) { + var hash = crypt.createHash(hashFunction); + hash.write(seed); + c.writeUInt32BE(i, 0); + hash.end(c); + hash.read().copy(T, i * hLen); + } + return T.slice(0, maskLength); +}; + +module.exports.makeScheme = function (key, options) { + function Scheme(key, options) { + this.key = key; + this.options = options; + } + + Scheme.prototype.maxMessageLength = function () { + return this.key.encryptedDataLength - 2 * module.exports.digestLength[this.options.encryptionSchemeOptions.hash || DEFAULT_HASH_FUNCTION] - 2; + }; + + /** + * Pad input + * alg: PKCS1_OAEP + * + * https://tools.ietf.org/html/rfc3447#section-7.1.1 + */ + Scheme.prototype.encPad = function (buffer) { + var hash = this.options.encryptionSchemeOptions.hash || DEFAULT_HASH_FUNCTION; + var mgf = this.options.encryptionSchemeOptions.mgf || module.exports.eme_oaep_mgf1; + var label = this.options.encryptionSchemeOptions.label || new Buffer(0); + var emLen = this.key.encryptedDataLength; + + var hLen = module.exports.digestLength[hash]; + + // Make sure we can put message into an encoded message of emLen bytes + if (buffer.length > emLen - 2 * hLen - 2) { + throw new Error("Message is too long to encode into an encoded message with a length of " + emLen + " bytes, increase" + + "emLen to fix this error (minimum value for given parameters and options: " + (emLen - 2 * hLen - 2) + ")"); + } + + var lHash = crypt.createHash(hash); + lHash.update(label); + lHash = lHash.digest(); + + var PS = new Buffer(emLen - buffer.length - 2 * hLen - 1); // Padding "String" + PS.fill(0); // Fill the buffer with octets of 0 + PS[PS.length - 1] = 1; + + var DB = Buffer.concat([lHash, PS, buffer]); + var seed = crypt.randomBytes(hLen); + + // mask = dbMask + var mask = mgf(seed, DB.length, hash); + // XOR DB and dbMask together. + for (var i = 0; i < DB.length; i++) { + DB[i] ^= mask[i]; + } + // DB = maskedDB + + // mask = seedMask + mask = mgf(DB, hLen, hash); + // XOR seed and seedMask together. + for (i = 0; i < seed.length; i++) { + seed[i] ^= mask[i]; + } + // seed = maskedSeed + + var em = new Buffer(1 + seed.length + DB.length); + em[0] = 0; + seed.copy(em, 1); + DB.copy(em, 1 + seed.length); + + return em; + }; + + /** + * Unpad input + * alg: PKCS1_OAEP + * + * Note: This method works within the buffer given and modifies the values. It also returns a slice of the EM as the return Message. + * If the implementation requires that the EM parameter be unmodified then the implementation should pass in a clone of the EM buffer. + * + * https://tools.ietf.org/html/rfc3447#section-7.1.2 + */ + Scheme.prototype.encUnPad = function (buffer) { + var hash = this.options.encryptionSchemeOptions.hash || DEFAULT_HASH_FUNCTION; + var mgf = this.options.encryptionSchemeOptions.mgf || module.exports.eme_oaep_mgf1; + var label = this.options.encryptionSchemeOptions.label || new Buffer(0); + + var hLen = module.exports.digestLength[hash]; + + // Check to see if buffer is a properly encoded OAEP message + if (buffer.length < 2 * hLen + 2) { + throw new Error("Error decoding message, the supplied message is not long enough to be a valid OAEP encoded message"); + } + + var seed = buffer.slice(1, hLen + 1); // seed = maskedSeed + var DB = buffer.slice(1 + hLen); // DB = maskedDB + + var mask = mgf(DB, hLen, hash); // seedMask + // XOR maskedSeed and seedMask together to get the original seed. + for (var i = 0; i < seed.length; i++) { + seed[i] ^= mask[i]; + } + + mask = mgf(seed, DB.length, hash); // dbMask + // XOR DB and dbMask together to get the original data block. + for (i = 0; i < DB.length; i++) { + DB[i] ^= mask[i]; + } + + var lHash = crypt.createHash(hash); + lHash.update(label); + lHash = lHash.digest(); + + var lHashEM = DB.slice(0, hLen); + if (lHashEM.toString("hex") != lHash.toString("hex")) { + throw new Error("Error decoding message, the lHash calculated from the label provided and the lHash in the encrypted data do not match."); + } + + // Filter out padding + i = hLen; + while (DB[i++] === 0 && i < DB.length); + if (DB[i - 1] != 1) { + throw new Error("Error decoding message, there is no padding message separator byte"); + } + + return DB.slice(i); // Message + }; + + return new Scheme(key, options); +}; \ No newline at end of file diff --git a/src/schemes/pkcs1.js b/src/schemes/pkcs1.js new file mode 100644 index 0000000..efd86d2 --- /dev/null +++ b/src/schemes/pkcs1.js @@ -0,0 +1,175 @@ +/** + * PKCS1 padding and signature scheme + */ + +var BigInteger = require('../libs/jsbn'); +var crypt = require('crypto'); +var SIGN_INFO_HEAD = { + md2: new Buffer('3020300c06082a864886f70d020205000410', 'hex'), + md5: new Buffer('3020300c06082a864886f70d020505000410', 'hex'), + sha1: new Buffer('3021300906052b0e03021a05000414', 'hex'), + sha224: new Buffer('302d300d06096086480165030402040500041c', 'hex'), + sha256: new Buffer('3031300d060960864801650304020105000420', 'hex'), + sha384: new Buffer('3041300d060960864801650304020205000430', 'hex'), + sha512: new Buffer('3051300d060960864801650304020305000440', 'hex'), + ripemd160: new Buffer('3021300906052b2403020105000414', 'hex'), + rmd160: new Buffer('3021300906052b2403020105000414', 'hex') +}; + +var SIGN_ALG_TO_HASH_ALIASES = { + 'ripemd160': 'rmd160' +}; + +var DEFAULT_HASH_FUNCTION = 'sha256'; + +module.exports = { + isEncryption: true, + isSignature: true +}; + +module.exports.makeScheme = function (key, options) { + function Scheme(key, options) { + this.key = key; + this.options = options; + } + + Scheme.prototype.maxMessageLength = function () { + return this.key.encryptedDataLength - 11; + }; + + /** + * Pad input Buffer to encryptedDataLength bytes, and return new Buffer + * alg: PKCS#1 (type 2, random) + * @param buffer + * @returns {Buffer} + */ + Scheme.prototype.encPad = function (buffer) { + if (buffer.length > this.key.maxMessageLength) { + throw new Error("Message too long for RSA (n=" + this.key.encryptedDataLength + ", l=" + buffer.length + ")"); + } + + // TODO: make n-length buffer + var ba = Array.prototype.slice.call(buffer, 0); + + // random padding + ba.unshift(0); + var rand = crypt.randomBytes(this.key.encryptedDataLength - ba.length - 2); + for (var i = 0; i < rand.length; i++) { + var r = rand[i]; + while (r === 0) { // non-zero only + r = crypt.randomBytes(1)[0]; + } + ba.unshift(r); + } + ba.unshift(2); + ba.unshift(0); + + return ba; + }; + + /** + * Unpad input Buffer and, if valid, return the Buffer object + * alg: PKCS#1 (type 2, random) + * @param buffer + * @returns {Buffer} + */ + Scheme.prototype.encUnPad = function (buffer) { + //var buffer = buffer.toByteArray(); + var i = 0; + + while (i < buffer.length && buffer[i] === 0) { + ++i; + } + + if (buffer.length - i != this.key.encryptedDataLength - 1 || buffer[i] != 2) { + return null; + } + + ++i; + while (buffer[i] !== 0) { + if (++i >= buffer.length) { + return null; + } + } + + var c = 0; + var res = new Buffer(buffer.length - i - 1); + while (++i < buffer.length) { + res[c++] = buffer[i] & 255; + } + + return res; + }; + + Scheme.prototype.sign = function (buffer) { + var hashAlgorithm = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION; + if (this.options.environment == 'browser') { + hashAlgorithm = SIGN_ALG_TO_HASH_ALIASES[hashAlgorithm] || hashAlgorithm; + + var hasher = crypt.createHash(hashAlgorithm); + hasher.update(buffer); + var hash = this.pkcs1pad(hasher.digest(), hashAlgorithm); + var res = this.key.$doPrivate(new BigInteger(hash)).toBuffer(this.key.encryptedDataLength); + + return res; + } else { + var signer = crypt.createSign('RSA-' + hashAlgorithm.toUpperCase()); + signer.update(buffer); + return signer.sign(this.options.rsaUtils.getPrivatePEM()); + } + }; + + Scheme.prototype.verify = function (buffer, signature, signature_encoding) { + var hashAlgorithm = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION; + if (this.options.environment == 'browser') { + hashAlgorithm = SIGN_ALG_TO_HASH_ALIASES[hashAlgorithm] || hashAlgorithm; + + if (signature_encoding) { + signature = new Buffer(signature, signature_encoding); + } + + var hasher = crypt.createHash(hashAlgorithm); + hasher.update(buffer); + var hash = this.pkcs1pad(hasher.digest(), hashAlgorithm); + var m = this.key.$doPublic(new BigInteger(signature)); + + return m.toBuffer().toString('hex') == hash.toString('hex'); + } else { + var verifier = crypt.createVerify('RSA-' + hashAlgorithm.toUpperCase()); + verifier.update(buffer); + return verifier.verify(this.options.rsaUtils.getPublicPEM(), signature, signature_encoding); + } + }; + + /** + * PKCS#1 pad input buffer to max data length + * @param hashBuf + * @param hashAlgorithm + * @returns {*} + */ + Scheme.prototype.pkcs1pad = function (hashBuf, hashAlgorithm) { + var digest = SIGN_INFO_HEAD[hashAlgorithm]; + if (!digest) { + throw Error('Unsupported hash algorithm'); + } + + var data = Buffer.concat([digest, hashBuf]); + + if (data.length + 10 > this.key.encryptedDataLength) { + throw Error('Key is too short for signing algorithm (' + hashAlgorithm + ')'); + } + + var filled = new Buffer(this.key.encryptedDataLength - data.length - 1); + filled.fill(0xff, 0, filled.length - 1); + filled[0] = 1; + filled[filled.length - 1] = 0; + + var res = Buffer.concat([filled, data]); + + return res; + }; + + return new Scheme(key, options); +}; + + diff --git a/src/schemes/pss.js b/src/schemes/pss.js new file mode 100644 index 0000000..c37be42 --- /dev/null +++ b/src/schemes/pss.js @@ -0,0 +1,187 @@ +/** + * PSS signature scheme + */ + +var BigInteger = require('../libs/jsbn'); +var crypt = require('crypto'); + +module.exports = { + isEncryption: false, + isSignature: true +}; + +var DEFAULT_HASH_FUNCTION = 'sha1'; +var DEFAULT_SALT_LENGTH = 20; + +module.exports.makeScheme = function (key, options) { + var OAEP = require('./schemes').pkcs1_oaep; + + /** + * @param key + * options [Object] An object that contains the following keys that specify certain options for encoding. + * └>signingSchemeOptions + * ├>hash [String] Hash function to use when encoding and generating masks. Must be a string accepted by node's crypto.createHash function. (default = "sha1") + * ├>mgf [function] The mask generation function to use when encoding. (default = mgf1SHA1) + * └>sLen [uint] The length of the salt to generate. (default = 20) + * @constructor + */ + function Scheme(key, options) { + this.key = key; + this.options = options; + } + + Scheme.prototype.sign = function (buffer) { + var encoded = this.emsa_pss_encode(buffer, this.key.keySize - 1); + var res = this.key.$doPrivate(new BigInteger(encoded)).toBuffer(this.key.encryptedDataLength); + return res; + }; + + Scheme.prototype.verify = function (buffer, signature, signature_encoding) { + if (signature_encoding) { + signature = new Buffer(signature, signature_encoding); + } + signature = new BigInteger(signature); + + var emLen = Math.ceil((this.key.keySize - 1) / 8); + var m = this.key.$doPublic(signature).toBuffer(emLen); + + return this.emsa_pss_verify(buffer, m, this.key.keySize - 1); + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-9.1.1 + * + * M [Buffer] Message to encode + * emBits [uint] Maximum length of output in bits. Must be at least 8hLen + 8sLen + 9 (hLen = Hash digest length in bytes | sLen = length of salt in bytes) + * @returns {Buffer} The encoded message + */ + Scheme.prototype.emsa_pss_encode = function (M, emBits) { + var hash = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION; + var mgf = this.options.signingSchemeOptions.mgf || OAEP.eme_oaep_mgf1; + var sLen = this.options.signingSchemeOptions.saltLength || DEFAULT_SALT_LENGTH; + + var hLen = OAEP.digestLength[hash]; + var emLen = Math.ceil(emBits / 8); + + if (emLen < hLen + sLen + 2) { + throw new Error("Output length passed to emBits(" + emBits + ") is too small for the options " + + "specified(" + hash + ", " + sLen + "). To fix this issue increase the value of emBits. (minimum size: " + + (8 * hLen + 8 * sLen + 9) + ")" + ); + } + + var mHash = crypt.createHash(hash); + mHash.update(M); + mHash = mHash.digest(); + + var salt = crypt.randomBytes(sLen); + + var Mapostrophe = new Buffer(8 + hLen + sLen); + Mapostrophe.fill(0, 0, 8); + mHash.copy(Mapostrophe, 8); + salt.copy(Mapostrophe, 8 + mHash.length); + + var H = crypt.createHash(hash); + H.update(Mapostrophe); + H = H.digest(); + + var PS = new Buffer(emLen - salt.length - hLen - 2); + PS.fill(0); + + var DB = new Buffer(PS.length + 1 + salt.length); + PS.copy(DB); + DB[PS.length] = 0x01; + salt.copy(DB, PS.length + 1); + + var dbMask = mgf(H, DB.length, hash); + + // XOR DB and dbMask together + var maskedDB = new Buffer(DB.length); + for (var i = 0; i < dbMask.length; i++) { + maskedDB[i] = DB[i] ^ dbMask[i]; + } + + var bits = emBits - 8 * (emLen - 1); + var mask = 255 << 8 - bits >> 8 - bits; + maskedDB[0] &= ((maskedDB[0] ^ mask) & maskedDB[0]); + + var EM = new Buffer(maskedDB.length + H.length + 1); + maskedDB.copy(EM, 0); + H.copy(EM, maskedDB.length); + EM[EM.length - 1] = 0xbc; + + return EM; + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-9.1.2 + * + * M [Buffer] Message + * EM [Buffer] Signature + * emBits [uint] Length of EM in bits. Must be at least 8hLen + 8sLen + 9 to be a valid signature. (hLen = Hash digest length in bytes | sLen = length of salt in bytes) + * @returns {Boolean} True if signature(EM) matches message(M) + */ + Scheme.prototype.emsa_pss_verify = function (M, EM, emBits) { + var hash = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION; + var mgf = this.options.signingSchemeOptions.mgf || OAEP.eme_oaep_mgf1; + var sLen = this.options.signingSchemeOptions.saltLength || DEFAULT_SALT_LENGTH; + + var hLen = OAEP.digestLength[hash]; + var emLen = Math.ceil(emBits / 8); + + if (emLen < hLen + sLen + 2 || EM[EM.length - 1] != 0xbc) { + return false; + } + + var DB = new Buffer(emLen - hLen - 1); + EM.copy(DB, 0, 0, emLen - hLen - 1); + + var mask = 0; + for (var i = 0, bits = 8 * emLen - emBits; i < bits; i++) { + mask |= 1 << (7 - i); + } + + if ((DB[0] & mask) !== 0) { + return false; + } + + var H = EM.slice(emLen - hLen - 1, emLen - 1); + var dbMask = mgf(H, DB.length, hash); + + // Unmask DB + for (i = 0; i < DB.length; i++) { + DB[i] ^= dbMask[i]; + } + + mask = 0; + for (i = 0, bits = emBits - 8 * (emLen - 1); i < bits; i++) { + mask |= 1 << i; + } + DB[0] &= mask; + + // Filter out padding + while (DB[i++] === 0 && i < DB.length); + if (DB[i - 1] != 1) { + return false; + } + + var salt = DB.slice(DB.length - sLen); + + var mHash = crypt.createHash(hash); + mHash.end(M); + mHash = mHash.read(); + + var Mapostrophe = new Buffer(8 + hLen + sLen); + Mapostrophe.fill(0, 0, 8); + mHash.copy(Mapostrophe, 8); + salt.copy(Mapostrophe, 8 + mHash.length); + + var Hapostrophe = crypt.createHash(hash); + Hapostrophe.end(Mapostrophe); + Hapostrophe = Hapostrophe.read(); + + return H.toString("hex") === Hapostrophe.toString("hex"); + }; + + return new Scheme(key, options); +}; \ No newline at end of file diff --git a/src/schemes/schemes.js b/src/schemes/schemes.js new file mode 100644 index 0000000..0b81a51 --- /dev/null +++ b/src/schemes/schemes.js @@ -0,0 +1,23 @@ +module.exports = schemes = { + pkcs1: require('./pkcs1'), + pkcs1_oaep: require('./oaep'), + pss: require('./pss'), + + /** + * 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; + } +}; diff --git a/src/utils.js b/src/utils.js index b7411ca..6e9edda 100644 --- a/src/utils.js +++ b/src/utils.js @@ -19,7 +19,7 @@ module.exports.linebrk = function (str, maxLen) { return res + str.substring(i, str.length); }; -module.exports.detectEnvironment = function() { +module.exports.detectEnvironment = function () { if (process && process.title != 'browser') { return 'node'; } else if (window) { @@ -43,8 +43,8 @@ module.exports.get32IntFromBuffer = function (buffer, offset) { return buffer.readUInt32BE(offset); } else { var res = 0; - for (var i = offset + size, d = 0; i > offset; i--, d+=2) { - res += buffer[i-1] * Math.pow(16, d); + for (var i = offset + size, d = 0; i > offset; i--, d += 2) { + res += buffer[i - 1] * Math.pow(16, d); } return res; } diff --git a/test/tests.js b/test/tests.js index 2b4a505..cec4bbe 100644 --- a/test/tests.js +++ b/test/tests.js @@ -7,7 +7,6 @@ var assert = require("chai").assert; var _ = require("lodash"); var NodeRSA = require("../src/NodeRSA"); - describe("NodeRSA", function(){ var keySizes = [ {b: 512, e: 3}, @@ -18,9 +17,13 @@ describe("NodeRSA", function(){ {b: 1024} // 'e' should be 65537 ]; - var signAlgorithms = ['md5', 'sha1', 'sha256']; - var environments = ['browser', 'node']; + var encryptSchemes = ['pkcs1', 'pkcs1_oaep']; + var signingSchemes = ['pkcs1', 'pss']; + var signHashAlgorithms = { + 'node': ['MD4', 'MD5', 'RIPEMD160', 'SHA', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512'], + 'browser': ['MD5', 'RIPEMD160', 'SHA1', 'SHA256', 'SHA512'] + }; var dataBundle = { "string": { @@ -29,7 +32,7 @@ describe("NodeRSA", function(){ }, "unicode string": { data: "ascii + юникод スラ ⑨", - encoding: "utf8" + encoding: "utf8" }, "empty string": { data: "", @@ -57,13 +60,82 @@ describe("NodeRSA", function(){ var privateNodeRSA = null; var publicNodeRSA = null; - describe("Work with keys", function(){ + describe("Setup options", function(){ + it("should make empty key pair with default options", 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.equal(key.$options.signingSchemeOptions.saltLength, null); + + assert.equal(key.$options.encryptionScheme, 'pkcs1_oaep'); + assert.equal(key.$options.encryptionSchemeOptions.hash, 'sha1'); + assert.equal(key.$options.encryptionSchemeOptions.label, null); + }); + + it("should make key pair with pkcs1-md5 signing scheme", 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-sha512 signing scheme", function () { + var key = new NodeRSA(null, {signingScheme: 'pss-sha512'}); + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, 'sha512'); + }); + + it("should make key pair with pkcs1 encryption scheme, and pss-sha1 signing scheme", function () { + var key = new NodeRSA(null, {encryptionScheme: 'pkcs1', signingScheme: 'pss'}); + assert.equal(key.$options.encryptionScheme, 'pkcs1'); + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, null); + }); + + it("advanced options change", function () { + var key = new NodeRSA(null); + key.setOptions({ + encryptionScheme: { + scheme: 'pkcs1_oaep', + hash: 'sha512', + label: 'horay' + }, + signingScheme: { + scheme: 'pss', + hash: 'md5', + saltLength: 15 + } + }); + + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, 'md5'); + assert.equal(key.$options.signingSchemeOptions.saltLength, 15); + assert.equal(key.$options.encryptionScheme, 'pkcs1_oaep'); + assert.equal(key.$options.encryptionSchemeOptions.hash, 'sha512'); + assert.equal(key.$options.encryptionSchemeOptions.label, 'horay'); + }); + + it("should throw \"unsupported hashing 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 hashing algorithm"); + }); + }); + + /*describe("Work with keys", function() { describe("Generating keys", function() { for (var size in keySizes) { - (function(size){ + (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})); + generatedKeys.push(new NodeRSA({b: size.b, e: size.e}, {encryptionScheme: 'pkcs1'})); 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); @@ -72,17 +144,6 @@ describe("NodeRSA", function(){ }); })(keySizes[size]); } - - it("should make empty key pair", function () { - var key = new NodeRSA(null); - assert.equal(key.isEmpty(), true); - }); - - 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("PEM", function(){ @@ -210,150 +271,216 @@ describe("NodeRSA", 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])); + describe("Encrypting & decrypting", function () { + for (var scheme_i in encryptSchemes) { + (function (scheme) { + describe("Encryption scheme: " + scheme, 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]; + 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); - } - }); - describe("Bad cases", function () { - it("unsupported data types", function(){ - assert.throw(function(){ generatedKeys[0].encrypt(null); }, Error, "Unexpected data type"); - assert.throw(function(){ generatedKeys[0].encrypt(undefined); }, Error, "Unexpected data type"); - assert.throw(function(){ generatedKeys[0].encrypt(true); }, Error, "Unexpected data type"); - }); - - it("incorrect key for decrypting", function(){ - var encrypted = generatedKeys[0].encrypt('data'); - assert.notEqual('data', generatedKeys[1].decrypt(encrypted)); - }); - }); + describe("Bad cases", function () { + it("unsupported data types", function(){ + assert.throw(function(){ generatedKeys[0].encrypt(null); }, Error, "Unexpected data type"); + assert.throw(function(){ generatedKeys[0].encrypt(undefined); }, Error, "Unexpected data type"); + assert.throw(function(){ generatedKeys[0].encrypt(true); }, Error, "Unexpected data type"); + }); + + it("incorrect key for decrypting", function(){ + var encrypted = generatedKeys[0].encrypt('data'); + assert.throw(function(){ generatedKeys[1].decrypt(encrypted); }, Error, "Error during decryption"); + }); + }); + }); + })(encryptSchemes[scheme_i]); + } }); - describe("Signing & verifying", function () { - for(var env in environments) { - (function(env) { - describe("Good cases in " + env + " environment", function () { - var signed = {}; - var key = null; - - for (var i in dataBundle) { - (function (i) { - var suit = dataBundle[i]; - it("should sign " + i, function () { - key = new NodeRSA(generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].getPrivatePEM(), {environment: env}); - signed[i] = key.sign(suit.data); - assert(Buffer.isBuffer(signed[i])); - assert(signed[i].length > 0); + for (var scheme_i in signingSchemes) { + (function (scheme) { + describe("Signing scheme: " + scheme, function () { + if (scheme == 'pkcs1') { + var envs = environments; + } else { + var envs = ['node']; + } + for (var env in envs) { + (function (env) { + describe("Good cases" + (envs.length > 1 ? " in " + env + " environment" : ""), function () { + var signed = {}; + var key = null; + + for (var i in dataBundle) { + (function (i) { + var suit = dataBundle[i]; + it("should sign " + i, function () { + key = new NodeRSA(generatedKeys[generatedKeys.length - 1].getPrivatePEM(), { + signingScheme: scheme + '-sha256', + environment: env + }); + signed[i] = key.sign(suit.data); + assert(Buffer.isBuffer(signed[i])); + assert(signed[i].length > 0); + }); + + it("should verify " + i, function () { + if(!key.verify(suit.data, signed[i])) { + key.verify(suit.data, signed[i]); + } + assert(key.verify(suit.data, signed[i])); + }); + })(i); + } + + for (var alg in signHashAlgorithms[env]) { + (function (alg) { + it("signing with custom algorithm (" + alg + ")", function () { + var key = new NodeRSA(generatedKeys[generatedKeys.length - 1].getPrivatePEM(), { + signingScheme: scheme + '-' + alg, + environment: env + }); + var signed = key.sign('data'); + if(!key.verify('data', signed)) { + key.verify('data', signed); + } + assert(key.verify('data', signed)); + }); + })(signHashAlgorithms[env][alg]); + } }); - it("should verify " + i, function () { - assert(key.verify(suit.data, signed[i])); + describe("Bad cases" + (envs.length > 1 ? " in " + env + " environment" : ""), function () { + it("incorrect data for verifying", function () { + var key = new NodeRSA(generatedKeys[0].getPrivatePEM(), { + signingScheme: scheme + '-sha256', + environment: env + }); + var signed = key.sign('data1'); + assert(!key.verify('data2', signed)); + }); + + it("incorrect key for signing", function () { + var key = new NodeRSA(generatedKeys[0].getPublicPEM(), { + signingScheme: scheme + '-sha256', + environment: env + }); + assert.throw(function () { + key.sign('data'); + }, Error, "It is not private key"); + }); + + it("incorrect key for verifying", function () { + var key1 = new NodeRSA(generatedKeys[0].getPrivatePEM(), { + signingScheme: scheme + '-sha256', + environment: env + }); + var key2 = new NodeRSA(generatedKeys[1].getPublicPEM(), { + signingScheme: scheme + '-sha256', + environment: env + }); + var signed = key1.sign('data'); + assert(!key2.verify('data', signed)); + }); + + it("incorrect key for verifying (empty)", function () { + var key = new NodeRSA(null, {environment: env}); + + assert.throw(function () { + key.verify('data', 'somesignature'); + }, Error, "It is not public key"); + }); + + it("different algorithms", function () { + var singKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), { + signingScheme: scheme + '-md5', + environment: env + }); + var verifyKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), { + signingScheme: scheme + '-sha1', + environment: env + }); + var signed = singKey.sign('data'); + assert(!verifyKey.verify('data', signed)); + }); }); - })(i); + })(envs[env]); } - for (var alg in signAlgorithms) { - (function (alg) { - it("signing with custom algorithm (" + alg + ")", function () { - var key = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: alg, environment: env}); - var signed = key.sign('data'); - assert(key.verify('data', signed)); - }); - })(signAlgorithms[alg]); + if (scheme !== 'pkcs1') { + return; } - }); - - describe("Bad cases in " + env + " environment", function () { - it("incorrect data for verifying", function () { - var key = new NodeRSA(generatedKeys[0].getPrivatePEM(), {environment: env}); - var signed = key.sign('data1'); - assert(!key.verify('data2', signed)); - }); - - it("incorrect key for signing", function () { - var key = new NodeRSA(generatedKeys[0].getPublicPEM(), {environment: env}); - assert.throw(function () { - key.sign('data'); - }, Error, "It is not private key"); - }); - - it("incorrect key for verifying", function () { - var key1 = new NodeRSA(generatedKeys[0].getPrivatePEM(), {environment: env}); - var key2 = new NodeRSA(generatedKeys[1].getPublicPEM(), {environment: env}); - var signed = key1.sign('data'); - assert(!key2.verify('data', signed)); - }); - - it("incorrect key for verifying (empty)", function () { - var key = new NodeRSA(null, {environment: env}); - - assert.throw(function () { - key.verify('data', 'somesignature'); - }, Error, "It is not public key"); - }); - - it("different algorithms", function () { - var singKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: 'md5', environment: env}); - var verifyKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: 'sha1', environment: env}); - var signed = singKey.sign('data'); - assert(!verifyKey.verify('data', signed)); + describe("Compatibility of different environments", function () { + for (var alg in signHashAlgorithms['browser']) { + (function (alg) { + it("signing with custom algorithm (" + alg + ") (equal test)", function () { + var nodeKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + signingScheme: scheme + '-' + alg, + environment: 'node' + }); + var browserKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + signingScheme: scheme + '-' + alg, + environment: 'browser' + }); + + assert.equal(nodeKey.sign('data', 'hex'), browserKey.sign('data', 'hex')); + }); + + it("sign in node & verify in browser (" + alg + ")", function () { + var nodeKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + signingScheme: scheme + '-' + alg, + environment: 'node' + }); + var browserKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + signingScheme: scheme + '-' + alg, + environment: 'browser' + }); + + assert(browserKey.verify('data', nodeKey.sign('data'))); + }); + + it("sign in browser & verify in node (" + alg + ")", function () { + var nodeKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + signingScheme: scheme + '-' + alg, + environment: 'node' + }); + var browserKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + signingScheme: scheme + '-' + alg, + environment: 'browser' + }); + + assert(nodeKey.verify('data', browserKey.sign('data'))); + }); + })(signHashAlgorithms['browser'][alg]); + } }); }); - })(environments[env]); + })(signingSchemes[scheme_i]); } - - describe("Compatibility of different environments", function () { - for (var alg in signAlgorithms) { - (function (alg) { - it("signing with custom algorithm (" + alg + ")", function () { - var nodeKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: alg, environment: 'node'}); - var browserKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: alg, environment: 'browser'}); - - assert.equal(nodeKey.sign('data', 'hex'), browserKey.sign('data', 'hex')); - }); - - it("sign in node & verify in browser (" + alg + ")", function () { - var nodeKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: alg, environment: 'node'}); - var browserKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: alg, environment: 'browser'}); - - assert(browserKey.verify('data', nodeKey.sign('data'))); - }); - - it("sign in browser & verify in node (" + alg + ")", function () { - var nodeKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: alg, environment: 'node'}); - var browserKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), {signingAlgorithm: alg, environment: 'browser'}); - - assert(nodeKey.verify('data', browserKey.sign('data'))); - }); - })(signAlgorithms[alg]); - } - - }); - }); + });*/ }); \ No newline at end of file